diff --git a/AUTHORS.md b/AUTHORS.md index 09ab78412..e6ff67293 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -49,6 +49,7 @@ Programmers Cédric Mocquillon Chris Boyce (slothlife) Chris Robinson (KittyCat) + Coleman Smith (olcoal) Cory F. Cohen (cfcohen) Cris Mihalache (Mirceam) crussell187 @@ -122,7 +123,6 @@ Programmers Lordrea Łukasz Gołębiewski (lukago) Lukasz Gromanowski (lgro) - Manuel Edelmann (vorenon) Marc Bouvier (CramitDeFrog) Marcin Hulist (Gohan) Mark Siewert (mark76) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7257e2338..fa3572282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,31 @@ 0.47.0 ------ + Bug #832: OpenMW-CS: Handle deleted references Bug #1662: Qt4 and Windows binaries crash if there's a non-ASCII character in a file path/config path + Bug #1901: Actors colliding behaviour is different from vanilla Bug #1952: Incorrect particle lighting Bug #2069: Fireflies in Fireflies invade Morrowind look wrong Bug #2311: Targeted scripts are not properly supported on non-unique RefIDs Bug #2473: Unable to overstock merchants Bug #2798: Mutable ESM records Bug #2976 [reopened]: Issues combining settings from the command line and both config files + Bug #3137: Walking into a wall prevents jumping + Bug #3372: Projectiles and magic bolts go through moving targets Bug #3676: NiParticleColorModifier isn't applied properly Bug #3714: Savegame fails to load due to conflict between SpellState and MagicEffects Bug #3789: Crash in visitEffectSources while in battle Bug #3862: Random container contents behave differently than vanilla Bug #3929: Leveled list merchant containers respawn on barter Bug #4021: Attributes and skills are not stored as floats + Bug #4039: Multiple followers should have the same following distance Bug #4055: Local scripts don't inherit variables from their base record Bug #4083: Door animation freezes when colliding with actors + Bug #4247: Cannot walk up stairs in Ebonheart docks + Bug #4447: Actor collision capsule shape allows looking through some walls + Bug #4465: Collision shape overlapping causes twitching + Bug #4476: Abot Gondoliers: player hangs in air during scenic travel + Bug #4568: Too many actors in one spot can push other actors out of bounds Bug #4623: Corprus implementation is incorrect Bug #4631: Setting MSAA level too high doesn't fall back to highest supported level Bug #4764: Data race in osg ParticleSystem @@ -23,6 +33,7 @@ Bug #5101: Hostile followers travel with the player Bug #5108: Savegame bloating due to inefficient fog textures format Bug #5165: Active spells should use real time intead of timestamps + Bug #5300: NPCs don't switch from torch to shield when starting combat Bug #5358: ForceGreeting always resets the dialogue window completely Bug #5363: Enchantment autocalc not always 0/1 Bug #5364: Script fails/stops if trying to startscript an unknown script @@ -37,6 +48,7 @@ Bug #5415: Environment maps in ebony cuirass and HiRez Armors Indoril cuirass don't work Bug #5416: Junk non-node records before the root node are not handled gracefully Bug #5422: The player loses all spells when resurrected + Bug #5423: Guar follows actors too closely Bug #5424: Creatures do not headtrack player Bug #5425: Poison effect only appears for one frame Bug #5427: GetDistance unknown ID error is misleading @@ -61,20 +73,33 @@ Bug #5604: Only one valid NIF root node is loaded from a single file Bug #5611: Usable items with "0 Uses" should be used only once Bug #5622: Can't properly interact with the console when in pause menu + Bug #5627: Bookart not shown if it isn't followed by
statement Bug #5633: Damage Spells in effect before god mode is enabled continue to hurt the player character and can kill them Bug #5639: Tooltips cover Messageboxes Bug #5644: Summon effects running on the player during game initialization cause crashes Bug #5656: Sneaking characters block hits while standing Bug #5661: Region sounds don't play at the right interval + Bug #5675: OpenMW-cs. FRMR subrecords are saved with the wrong MastIdx + Bug #5681: Player character can clip or pass through bridges instead of colliding against them Bug #5688: Water shader broken indoors with enable indoor shadows = false + Bug #5695: ExplodeSpell for actors doesn't target the ground + Bug #5703: OpenMW-CS menu system crashing on XFCE + Bug #5706: AI sequences stop looping after the saved game is reloaded + Bug #5731: Editor: skirts are invisible on characters + Bug #5758: Paralyzed actors behavior is inconsistent with vanilla + Bug #5762: Movement solver is insufficiently robust Feature #390: 3rd person look "over the shoulder" + Feature #1536: Show more information about level on menu Feature #2386: Distant Statics in the form of Object Paging Feature #2404: Levelled List can not be placed into a container + Feature #2686: Timestamps in openmw.log Feature #4894: Consider actors as obstacles for pathfinding Feature #5043: Head Bobbing + Feature #5199: Improve Scene Colors Feature #5297: Add a search function to the "Datafiles" tab of the OpenMW launcher Feature #5362: Show the soul gems' trapped soul in count dialog Feature #5445: Handle NiLines + Feature #5456: Basic collada animation support Feature #5457: Realistic diagonal movement Feature #5486: Fixes trainers to choose their training skills based on their base skill points Feature #5519: Code Patch tab in launcher @@ -89,6 +114,7 @@ Feature #5649: Skyrim SE compressed BSA format support Feature #5672: Make stretch menu background configuration more accessible Feature #5692: Improve spell/magic item search to factor in magic effect names + Feature #5730: Add graphic herbalism option to the launcher and documents Task #5480: Drop Qt4 support Task #5520: Improve cell name autocompleter implementation diff --git a/CHANGELOG_PR.md b/CHANGELOG_PR.md index 0a039ec3f..69ad0cc1b 100644 --- a/CHANGELOG_PR.md +++ b/CHANGELOG_PR.md @@ -18,6 +18,7 @@ Known Issues: New Features: - Dialogue to split item stacks now displays the name of the trapped soul for stacks of soul gems (#5362) +- Basics of Collada animations are now supported via osgAnimation plugin (#5456) New Editor Features: - ? @@ -33,8 +34,11 @@ Bug Fixes: - Morrowind legacy madness: Using a key on a trapped door/container now only disarms the trap if the door/container is locked (#5370) Editor Bug Fixes: +- Deleted and moved objects within a cell are now saved properly (#832) - Verifier no longer checks for alleged 'race' entries in clothing body parts (#5400) +- Loading mods now keeps the master index (#5675) +- Flicker and crashing on XFCE4 fixed (#5703) Miscellaneous: - Prevent save-game bloating by using an appropriate fog texture format (#5108) -- Ensure that 'Enchantment autocalc" flag is treated as flag in OpenMW-CS and in our esm tools (#5363) \ No newline at end of file +- Ensure that 'Enchantment autocalc" flag is treated as flag in OpenMW-CS and in our esm tools (#5363) diff --git a/CI/before_install.android.sh b/CI/before_install.android.sh index 791377dd9..0243a9609 100755 --- a/CI/before_install.android.sh +++ b/CI/before_install.android.sh @@ -1,4 +1,4 @@ #!/bin/sh -ex -curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201018.zip -o ~/openmw-android-deps.zip +curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201129.zip -o ~/openmw-android-deps.zip unzip -o ~/openmw-android-deps -d /usr/lib/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index 0ef67f47e..0c26801cc 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -73,7 +73,7 @@ CONFIGURATIONS=() TEST_FRAMEWORK="" GOOGLE_INSTALL_ROOT="" INSTALL_PREFIX="." -BULLET_DOUBLE="" +BULLET_DOUBLE=true BULLET_DBL="" BULLET_DBL_DISPLAY="Single precision" diff --git a/CMakeLists.txt b/CMakeLists.txt index 989068e43..65887c86c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.1.0) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Detect OS +include(cmake/OSIdentity.cmake) + # for link time optimization, remove if cmake version is >= 3.9 if(POLICY CMP0069) cmake_policy(SET CMP0069 NEW) @@ -21,7 +24,7 @@ option(BUILD_NIFTEST "Build nif file tester" ON) option(BUILD_DOCS "Build documentation." OFF ) option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest" OFF) -option(BULLET_USE_DOUBLES "Use double precision for Bullet" OFF) +option(BULLET_USE_DOUBLES "Use double precision for Bullet" ON) option(BUILD_OPENMW_MP "Build OpenMW-MP" ON) option(BUILD_BROWSER "Build tes3mp Server Browser" ON) option(BUILD_MASTER "Build tes3mp Master Server" OFF) @@ -620,6 +623,12 @@ if (WIN32) 5031 # #pragma warning(pop): likely mismatch, popping warning state pushed in different file (config_begin.hpp, config_end.hpp) ) endif() + + if( "${MyGUI_VERSION}" VERSION_LESS_EQUAL "3.4.0" ) + set(WARNINGS_DISABLE ${WARNINGS_DISABLE} + 4866 # compiler may not enforce left-to-right evaluation order for call + ) + endif() foreach(d ${WARNINGS_DISABLE}) set(WARNINGS "${WARNINGS} /wd${d}") diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index b35e78639..6f59ade7a 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -10,11 +10,9 @@ #include -Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg, - Config::GameSettings &gameSettings, +Launcher::AdvancedPage::AdvancedPage(Config::GameSettings &gameSettings, Settings::Manager &engineSettings, QWidget *parent) : QWidget(parent) - , mCfgMgr(cfg) , mGameSettings(gameSettings) , mEngineSettings(engineSettings) { @@ -153,6 +151,7 @@ bool Launcher::AdvancedPage::loadSettings() if (showOwnedIndex >= 0 && showOwnedIndex <= 3) showOwnedComboBox->setCurrentIndex(showOwnedIndex); loadSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); + loadSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); } // Bug fixes @@ -279,6 +278,7 @@ void Launcher::AdvancedPage::saveSettings() if (showOwnedCurrentIndex != mEngineSettings.getInt("show owned", "Game")) mEngineSettings.setInt("show owned", "Game", showOwnedCurrentIndex); saveSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); + saveSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); } // Bug fixes diff --git a/apps/launcher/advancedpage.hpp b/apps/launcher/advancedpage.hpp index ef2f4740f..a373fae43 100644 --- a/apps/launcher/advancedpage.hpp +++ b/apps/launcher/advancedpage.hpp @@ -9,7 +9,6 @@ #include -namespace Files { struct ConfigurationManager; } namespace Config { class GameSettings; } namespace Launcher @@ -19,7 +18,7 @@ namespace Launcher Q_OBJECT public: - AdvancedPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, + AdvancedPage(Config::GameSettings &gameSettings, Settings::Manager &engineSettings, QWidget *parent = nullptr); bool loadSettings(); @@ -35,7 +34,6 @@ namespace Launcher void slotViewOverShoulderToggled(bool checked); private: - Files::ConfigurationManager &mCfgMgr; Config::GameSettings &mGameSettings; Settings::Manager &mEngineSettings; QCompleter mCellNameCompleter; diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 92984847e..3d200e6d2 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -88,11 +88,7 @@ namespace Launcher QStringList previousSelectedFiles; QString mDataLocal; - void setPluginsCheckstates(Qt::CheckState state); - void buildView(); - void setupConfig(); - void readConfig(); void setProfile (int index, bool savePrevious); void setProfile (const QString &previous, const QString ¤t, bool savePrevious); void removeProfile (const QString &profile); diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index d7e7eabc5..d1cf3aa6f 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -29,9 +29,8 @@ QString getAspect(int x, int y) return QString(QString::number(xaspect) + ":" + QString::number(yaspect)); } -Launcher::GraphicsPage::GraphicsPage(Files::ConfigurationManager &cfg, Settings::Manager &engineSettings, QWidget *parent) +Launcher::GraphicsPage::GraphicsPage(Settings::Manager &engineSettings, QWidget *parent) : QWidget(parent) - , mCfgMgr(cfg) , mEngineSettings(engineSettings) { setObjectName ("GraphicsPage"); diff --git a/apps/launcher/graphicspage.hpp b/apps/launcher/graphicspage.hpp index 55178e0d7..35f711500 100644 --- a/apps/launcher/graphicspage.hpp +++ b/apps/launcher/graphicspage.hpp @@ -20,7 +20,7 @@ namespace Launcher Q_OBJECT public: - GraphicsPage(Files::ConfigurationManager &cfg, Settings::Manager &engineSettings, QWidget *parent = nullptr); + GraphicsPage(Settings::Manager &engineSettings, QWidget *parent = nullptr); void saveSettings(); bool loadSettings(); @@ -35,7 +35,6 @@ namespace Launcher void slotShadowDistLimitToggled(bool checked); private: - Files::ConfigurationManager &mCfgMgr; Settings::Manager &mEngineSettings; QVector mResolutionsPerScreen; diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 85f59e32b..82c185e56 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -126,9 +126,9 @@ void Launcher::MainDialog::createPages() mPlayPage = new PlayPage(this); mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); - mGraphicsPage = new GraphicsPage(mCfgMgr, mEngineSettings, this); + mGraphicsPage = new GraphicsPage(mEngineSettings, this); mSettingsPage = new SettingsPage(mCfgMgr, mGameSettings, mLauncherSettings, this); - mAdvancedPage = new AdvancedPage(mCfgMgr, mGameSettings, mEngineSettings, this); + mAdvancedPage = new AdvancedPage(mGameSettings, mEngineSettings, this); // Set the combobox of the play page to imitate the combobox on the datafilespage mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); diff --git a/apps/launcher/utils/cellnameloader.cpp b/apps/launcher/utils/cellnameloader.cpp index 6d1ed2f49..e7f6e83e7 100644 --- a/apps/launcher/utils/cellnameloader.cpp +++ b/apps/launcher/utils/cellnameloader.cpp @@ -45,4 +45,5 @@ QString CellNameLoader::getCellName(ESM::ESMReader &esmReader) cell.loadNameAndData(esmReader, isDeleted); return QString::fromStdString(cell.mName); -} \ No newline at end of file +} + diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index 39aae48bd..588be9ccb 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -210,6 +210,19 @@ void CSMPrefs::State::declare() setTooltip("Size of the orthographic frustum, greater value will allow the camera to see more of the world."). setRange(10, 10000); declareDouble ("object-marker-alpha", "Object Marker Transparency", 0.5).setPrecision(2).setRange(0,1); + declareBool("scene-use-gradient", "Use Gradient Background", true); + declareColour ("scene-day-background-colour", "Day Background Colour", QColor (110, 120, 128, 255)); + declareColour ("scene-day-gradient-colour", "Day Gradient Colour", QColor (47, 51, 51, 255)). + setTooltip("Sets the gradient color to use in conjunction with the day background color. Ignored if " + "the gradient option is disabled."); + declareColour ("scene-bright-background-colour", "Scene Bright Background Colour", QColor (79, 87, 92, 255)); + declareColour ("scene-bright-gradient-colour", "Scene Bright Gradient Colour", QColor (47, 51, 51, 255)). + setTooltip("Sets the gradient color to use in conjunction with the bright background color. Ignored if " + "the gradient option is disabled."); + declareColour ("scene-night-background-colour", "Scene Night Background Colour", QColor (64, 77, 79, 255)); + declareColour ("scene-night-gradient-colour", "Scene Night Gradient Colour", QColor (47, 51, 51, 255)). + setTooltip("Sets the gradient color to use in conjunction with the night background color. Ignored if " + "the gradient option is disabled."); declareCategory ("Tooltips"); declareBool ("scene", "Show Tooltips in 3D scenes", true); diff --git a/apps/opencs/model/world/actoradapter.cpp b/apps/opencs/model/world/actoradapter.cpp index 5ed80a1e4..8558aa9bc 100644 --- a/apps/opencs/model/world/actoradapter.cpp +++ b/apps/opencs/model/world/actoradapter.cpp @@ -593,56 +593,33 @@ namespace CSMWorld } else if (type == UniversalId::Type_Clothing) { - int priority = 0; - // TODO: reserve bodyparts for robes and skirts auto& clothing = dynamic_cast&>(record).get(); + std::vector parts; if (clothing.mData.mType == ESM::Clothing::Robe) { - auto reservedList = std::vector(); - - ESM::PartReference pr; - pr.mMale = ""; - pr.mFemale = ""; - - ESM::PartReferenceType parts[] = { + parts = { ESM::PRT_Groin, ESM::PRT_Skirt, ESM::PRT_RLeg, ESM::PRT_LLeg, ESM::PRT_RUpperarm, ESM::PRT_LUpperarm, ESM::PRT_RKnee, ESM::PRT_LKnee, - ESM::PRT_RForearm, ESM::PRT_LForearm + ESM::PRT_RForearm, ESM::PRT_LForearm, ESM::PRT_Cuirass }; - size_t parts_size = sizeof(parts)/sizeof(parts[0]); - for(size_t p = 0;p < parts_size;++p) - { - pr.mPart = parts[p]; - reservedList.push_back(pr); - } - - priority = parts_size; - addParts(reservedList, priority); } else if (clothing.mData.mType == ESM::Clothing::Skirt) { - auto reservedList = std::vector(); - - ESM::PartReference pr; - pr.mMale = ""; - pr.mFemale = ""; - - ESM::PartReferenceType parts[] = { - ESM::PRT_Groin, ESM::PRT_RLeg, ESM::PRT_LLeg - }; - size_t parts_size = sizeof(parts)/sizeof(parts[0]); - for(size_t p = 0;p < parts_size;++p) - { - pr.mPart = parts[p]; - reservedList.push_back(pr); - } - - priority = parts_size; - addParts(reservedList, priority); + parts = {ESM::PRT_Groin, ESM::PRT_RLeg, ESM::PRT_LLeg}; } + std::vector reservedList; + for (const auto& p : parts) + { + ESM::PartReference pr; + pr.mPart = p; + reservedList.emplace_back(pr); + } + + int priority = parts.size(); addParts(clothing.mParts.mParts, priority); + addParts(reservedList, priority); // Changing parts could affect what is picked for rendering data->addOtherDependency(itemId); diff --git a/apps/opencs/model/world/refcollection.cpp b/apps/opencs/model/world/refcollection.cpp index d8f6b391b..126a0ea78 100644 --- a/apps/opencs/model/world/refcollection.cpp +++ b/apps/opencs/model/world/refcollection.cpp @@ -64,10 +64,12 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool // ignore content file number std::map::iterator iter = cache.begin(); - ref.mRefNum.mIndex = ref.mRefNum.mIndex & 0x00ffffff; + unsigned int thisIndex = ref.mRefNum.mIndex & 0x00ffffff; + if (ref.mRefNum.mContentFile != -1 && !base) ref.mRefNum.mContentFile = ref.mRefNum.mIndex >> 24; + for (; iter != cache.end(); ++iter) { - if (ref.mRefNum.mIndex == iter->first.mIndex) + if (thisIndex == iter->first.mIndex) break; } diff --git a/apps/opencs/view/render/cellarrow.cpp b/apps/opencs/view/render/cellarrow.cpp index b6fee1545..ac260fe83 100644 --- a/apps/opencs/view/render/cellarrow.cpp +++ b/apps/opencs/view/render/cellarrow.cpp @@ -151,9 +151,9 @@ void CSVRender::CellArrow::buildShape() osg::Vec4Array *colours = new osg::Vec4Array; for (int i=0; i<6; ++i) - colours->push_back (osg::Vec4f (1.0f, 0.0f, 0.0f, 1.0f)); + colours->push_back (osg::Vec4f (0.11, 0.6f, 0.95f, 1.0f)); for (int i=0; i<6; ++i) - colours->push_back (osg::Vec4f (0.8f, (i==2 || i==5) ? 0.6f : 0.4f, 0.0f, 1.0f)); + colours->push_back (osg::Vec4f (0.08f, 0.44f, 0.7f, 1.0f)); geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); diff --git a/apps/opencs/view/render/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp index 3abc01d2e..4d73cde15 100644 --- a/apps/opencs/view/render/scenewidget.cpp +++ b/apps/opencs/view/render/scenewidget.cpp @@ -69,7 +69,6 @@ RenderWidget::RenderWidget(QWidget *parent, Qt::WindowFlags f) setLayout(layout); mView->getCamera()->setGraphicsContext(window); - mView->getCamera()->setClearColor( osg::Vec4(0.2, 0.2, 0.6, 1.0) ); mView->getCamera()->setViewport( new osg::Viewport(0, 0, traits->width, traits->height) ); SceneUtil::LightManager* lightMgr = new SceneUtil::LightManager; @@ -212,6 +211,25 @@ SceneWidget::SceneWidget(std::shared_ptr resourceSyste mOrbitCamControl->setConstRoll( CSMPrefs::get()["3D Scene Input"]["navi-orbit-const-roll"].isTrue() ); + // set up gradient view or configured clear color + QColor bgColour = CSMPrefs::get()["Rendering"]["scene-day-background-colour"].toColor(); + + if (CSMPrefs::get()["Rendering"]["scene-use-gradient"].isTrue()) { + QColor gradientColour = CSMPrefs::get()["Rendering"]["scene-day-gradient-colour"].toColor(); + mGradientCamera = createGradientCamera(bgColour, gradientColour); + + mView->getCamera()->setClearMask(0); + mView->getCamera()->addChild(mGradientCamera.get()); + } + else { + mView->getCamera()->setClearColor(osg::Vec4( + bgColour.redF(), + bgColour.greenF(), + bgColour.blueF(), + 1.0f + )); + } + // we handle lighting manually mView->setLightingMode(osgViewer::View::NO_LIGHT); @@ -249,6 +267,79 @@ SceneWidget::~SceneWidget() mResourceSystem->releaseGLObjects(mView->getCamera()->getGraphicsContext()->getState()); } + +osg::ref_ptr SceneWidget::createGradientRectangle(QColor bgColour, QColor gradientColour) +{ + osg::ref_ptr geometry = new osg::Geometry; + + osg::ref_ptr vertices = new osg::Vec3Array; + + vertices->push_back(osg::Vec3(0.0f, 0.0f, -1.0f)); + vertices->push_back(osg::Vec3(1.0f, 0.0f, -1.0f)); + vertices->push_back(osg::Vec3(0.0f, 1.0f, -1.0f)); + vertices->push_back(osg::Vec3(1.0f, 1.0f, -1.0f)); + + geometry->setVertexArray(vertices); + + osg::ref_ptr primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0); + + // triangle 1 + primitives->push_back (0); + primitives->push_back (1); + primitives->push_back (2); + + // triangle 2 + primitives->push_back (2); + primitives->push_back (1); + primitives->push_back (3); + + geometry->addPrimitiveSet(primitives); + + osg::ref_ptr colours = new osg::Vec4ubArray; + colours->push_back(osg::Vec4ub(gradientColour.red(), gradientColour.green(), gradientColour.blue(), 1.0f)); + colours->push_back(osg::Vec4ub(gradientColour.red(), gradientColour.green(), gradientColour.blue(), 1.0f)); + colours->push_back(osg::Vec4ub(bgColour.red(), bgColour.green(), bgColour.blue(), 1.0f)); + colours->push_back(osg::Vec4ub(bgColour.red(), bgColour.green(), bgColour.blue(), 1.0f)); + + geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX); + + geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + geometry->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + + return geometry; +} + + +osg::ref_ptr SceneWidget::createGradientCamera(QColor bgColour, QColor gradientColour) +{ + osg::ref_ptr camera = new osg::Camera(); + camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); + camera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1.0f, 0, 1.0f)); + camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); + camera->setViewMatrix(osg::Matrix::identity()); + + camera->setClearMask(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + camera->setAllowEventFocus(false); + + // draw subgraph before main camera view. + camera->setRenderOrder(osg::Camera::PRE_RENDER); + + camera->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + + osg::ref_ptr gradientQuad = createGradientRectangle(bgColour, gradientColour); + + camera->addChild(gradientQuad); + return camera; +} + + +void SceneWidget::updateGradientCamera(QColor bgColour, QColor gradientColour) +{ + osg::ref_ptr gradientRect = createGradientRectangle(bgColour, gradientColour); + // Replaces previous rectangle + mGradientCamera->setChild(0, gradientRect.get()); +} + void SceneWidget::setLighting(Lighting *lighting) { if (mLighting) @@ -276,12 +367,59 @@ void SceneWidget::setAmbient(const osg::Vec4f& ambient) void SceneWidget::selectLightingMode (const std::string& mode) { - if (mode=="day") - setLighting (&mLightingDay); - else if (mode=="night") - setLighting (&mLightingNight); - else if (mode=="bright") - setLighting (&mLightingBright); + QColor backgroundColour; + QColor gradientColour; + if (mode == "day") + { + backgroundColour = CSMPrefs::get()["Rendering"]["scene-day-background-colour"].toColor(); + gradientColour = CSMPrefs::get()["Rendering"]["scene-day-gradient-colour"].toColor(); + setLighting(&mLightingDay); + } + else if (mode == "night") + { + backgroundColour = CSMPrefs::get()["Rendering"]["scene-night-background-colour"].toColor(); + gradientColour = CSMPrefs::get()["Rendering"]["scene-night-gradient-colour"].toColor(); + setLighting(&mLightingNight); + } + else if (mode == "bright") + { + backgroundColour = CSMPrefs::get()["Rendering"]["scene-bright-background-colour"].toColor(); + gradientColour = CSMPrefs::get()["Rendering"]["scene-bright-gradient-colour"].toColor(); + setLighting(&mLightingBright); + } + if (CSMPrefs::get()["Rendering"]["scene-use-gradient"].isTrue()) { + if (mGradientCamera.get() != nullptr) { + // we can go ahead and update since this camera still exists + updateGradientCamera(backgroundColour, gradientColour); + + if (!mView->getCamera()->containsNode(mGradientCamera.get())) + { + // need to re-attach the gradient camera + mView->getCamera()->setClearMask(0); + mView->getCamera()->addChild(mGradientCamera.get()); + } + } + else { + // need to create the gradient camera + mGradientCamera = createGradientCamera(backgroundColour, gradientColour); + mView->getCamera()->setClearMask(0); + mView->getCamera()->addChild(mGradientCamera.get()); + } + } + else { + // Fall back to using the clear color for the camera + mView->getCamera()->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + mView->getCamera()->setClearColor(osg::Vec4( + backgroundColour.redF(), + backgroundColour.greenF(), + backgroundColour.blueF(), + 1.0f + )); + if (mGradientCamera.get() != nullptr && mView->getCamera()->containsNode(mGradientCamera.get())) { + // Remove the child to prevent the gradient from rendering + mView->getCamera()->removeChild(mGradientCamera.get()); + } + } } CSVWidget::SceneToolMode *SceneWidget::makeLightingSelector (CSVWidget::SceneToolbar *parent) diff --git a/apps/opencs/view/render/scenewidget.hpp b/apps/opencs/view/render/scenewidget.hpp index 6a94254b9..d7d9dba0c 100644 --- a/apps/opencs/view/render/scenewidget.hpp +++ b/apps/opencs/view/render/scenewidget.hpp @@ -100,10 +100,15 @@ namespace CSVRender void mouseMoveEvent (QMouseEvent *event) override; void wheelEvent (QWheelEvent *event) override; + osg::ref_ptr createGradientRectangle(QColor bgColour, QColor gradientColour); + osg::ref_ptr createGradientCamera(QColor bgColour, QColor gradientColour); + void updateGradientCamera(QColor bgColour, QColor gradientColour); + std::shared_ptr mResourceSystem; Lighting* mLighting; - + + osg::ref_ptr mGradientCamera; osg::Vec4f mDefaultAmbient; bool mHasDefaultAmbient; bool mIsExterior; diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 907972843..49356159c 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -66,14 +66,14 @@ add_openmw_dir (mwworld cells localscripts customdata inventorystore ptr actionopen actionread actionharvest actionequip timestamp actionalchemy cellstore actionapply actioneat store esmstore recordcmp fallback actionrepair actionsoulgem livecellref actiondoor - contentloader esmloader actiontrap cellreflist cellref physicssystem weather projectilemanager + contentloader esmloader actiontrap cellreflist cellref weather projectilemanager cellpreloader datetimemanager ) add_openmw_dir (mwphysics physicssystem trace collisiontype actor convert object heightfield closestnotmerayresultcallback - contacttestresultcallback deepestnotmecontacttestresultcallback stepper movementsolver - closestnotmeconvexresultcallback raycasting mtphysics + contacttestresultcallback deepestnotmecontacttestresultcallback stepper movementsolver projectile + closestnotmeconvexresultcallback raycasting mtphysics contacttestwrapper ) add_openmw_dir (mwclass diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index e2c0707f1..e9926f326 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -477,8 +477,6 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) , mNewGame (false) , mCfgMgr(configurationManager) { - MWClass::registerClasses(); - SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0"); // We use only gamepads Uint32 flags = SDL_INIT_VIDEO|SDL_INIT_NOPARACHUTE|SDL_INIT_GAMECONTROLLER|SDL_INIT_JOYSTICK|SDL_INIT_SENSOR; @@ -972,6 +970,8 @@ void OMW::Engine::go() std::string settingspath; settingspath = loadSettings (settings); + MWClass::registerClasses(); + // Create encoder mEncoder = new ToUTF8::Utf8Encoder(mEncoding); diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index f55f5ce60..a12906434 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -168,7 +168,7 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat if (variables.count ("help")) { - std::cout << desc << std::endl; + getRawStdout() << desc << std::endl; return false; } @@ -177,7 +177,7 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat cfgMgr.readConfiguration(variables, desc, true); Version::Version v = Version::getOpenmwVersion(variables["resources"].as().mPath.string()); - std::cout << v.describe() << std::endl; + getRawStdout() << v.describe() << std::endl; return false; } @@ -202,7 +202,7 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat Because there is no need to print the commit hash again, only print OpenMW's version */ - std::cout << "OpenMW version " << v.mVersion << std::endl; + Log(Debug::Info) << "OpenMW version " << v.mVersion; /* End of tes3mp change (minor) */ @@ -240,7 +240,7 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat // Font encoding settings std::string encoding(variables["encoding"].as().toStdString()); - std::cout << ToUTF8::encodingUsingMessage(encoding) << std::endl; + Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding); engine.setEncoding(ToUTF8::calculateEncoding(encoding)); // directory settings @@ -314,6 +314,42 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat return true; } +namespace +{ + class OSGLogHandler : public osg::NotifyHandler + { + void notify(osg::NotifySeverity severity, const char* msg) override + { + // Copy, because osg logging is not thread safe. + std::string msgCopy(msg); + if (msgCopy.empty()) + return; + + Debug::Level level; + switch (severity) + { + case osg::ALWAYS: + case osg::FATAL: + level = Debug::Error; + break; + case osg::WARN: + case osg::NOTICE: + level = Debug::Warning; + break; + case osg::INFO: + level = Debug::Info; + break; + case osg::DEBUG_INFO: + case osg::DEBUG_FP: + default: + level = Debug::Debug; + } + std::string_view s(msgCopy); + Log(level) << (s.back() == '\n' ? s.substr(0, s.size() - 1) : s); + } + }; +} + int runApplication(int argc, char *argv[]) { #ifdef __APPLE__ @@ -322,6 +358,7 @@ int runApplication(int argc, char *argv[]) setenv("OSG_GL_TEXTURE_STORAGE", "OFF", 0); #endif + osg::setNotifyHandler(new OSGLogHandler()); Files::ConfigurationManager cfgMgr; std::unique_ptr engine; engine.reset(new OMW::Engine(cfgMgr)); diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index 8b8727b5f..c6dfbbe9f 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -209,6 +209,7 @@ namespace MWBase virtual std::list getActorsSidingWith(const MWWorld::Ptr& actor) = 0; virtual std::list getActorsFollowing(const MWWorld::Ptr& actor) = 0; virtual std::list getActorsFollowingIndices(const MWWorld::Ptr& actor) = 0; + virtual std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor) = 0; ///Returns a list of actors who are fighting the given actor within the fAlarmDistance /** ie AiCombat is active and the target is the actor **/ diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index db9da6c11..a4f385595 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -383,6 +383,9 @@ namespace MWBase virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, float x, float y, float z, bool movePhysics=true) = 0; ///< @return an updated Ptr + virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, osg::Vec3f vec) = 0; + ///< @return an updated Ptr + virtual void scaleObject (const MWWorld::Ptr& ptr, float scale) = 0; virtual void rotateObject(const MWWorld::Ptr& ptr, float x, float y, float z, @@ -687,7 +690,7 @@ namespace MWBase /// Returns true if levitation spell effect is allowed. virtual bool isLevitationEnabled() const = 0; - virtual bool getGodModeState() = 0; + virtual bool getGodModeState() const = 0; virtual bool toggleGodMode() = 0; diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index deaec9c29..2be2b8199 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -14,6 +14,7 @@ #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -71,6 +72,11 @@ namespace MWClass return *this; } + Container::Container() + { + mHarvestEnabled = Settings::Manager::getBool("graphic herbalism", "Game"); + } + void Container::ensureCustomData (const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) @@ -84,8 +90,10 @@ namespace MWClass } } - bool canBeHarvested(const MWWorld::ConstPtr& ptr) + bool Container::canBeHarvested(const MWWorld::ConstPtr& ptr) const { + if (!mHarvestEnabled) + return false; const MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (animation == nullptr) return false; diff --git a/apps/openmw/mwclass/container.hpp b/apps/openmw/mwclass/container.hpp index 23a9f4cd5..74175a9ad 100644 --- a/apps/openmw/mwclass/container.hpp +++ b/apps/openmw/mwclass/container.hpp @@ -30,11 +30,16 @@ namespace MWClass class Container : public MWWorld::Class { + bool mHarvestEnabled; + void ensureCustomData (const MWWorld::Ptr& ptr) const; MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + bool canBeHarvested(const MWWorld::ConstPtr& ptr) const; + public: + Container(); void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index c6c91b39f..a075ded67 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -551,6 +551,7 @@ namespace MWGui setTitle(mPtr.getClass().getName(mPtr)); updateTopics(); + updateTopicsPane(); // force update for new services updateDisposition(); restock(); @@ -603,12 +604,14 @@ namespace MWGui mHistoryContents.clear(); } - void DialogueWindow::setKeywords(std::list keyWords) + bool DialogueWindow::setKeywords(std::list keyWords) { if (mKeywords == keyWords && isCompanion() == mIsCompanion) - return; + return false; mIsCompanion = isCompanion(); mKeywords = keyWords; + updateTopicsPane(); + return true; } void DialogueWindow::updateTopicsPane() @@ -683,6 +686,8 @@ namespace MWGui mTopicsList->adjustSize(); updateHistory(); + // The topics list has been regenerated so topic formatting needs to be updated + updateTopicFormat(); } void DialogueWindow::updateHistory(bool scrollbar) @@ -885,9 +890,9 @@ namespace MWGui void DialogueWindow::updateTopics() { - setKeywords(MWBase::Environment::get().getDialogueManager()->getAvailableTopics()); - updateTopicsPane(); - updateTopicFormat(); + // Topic formatting needs to be updated regardless of whether the topic list has changed + if (!setKeywords(MWBase::Environment::get().getDialogueManager()->getAvailableTopics())) + updateTopicFormat(); } bool DialogueWindow::isCompanion() diff --git a/apps/openmw/mwgui/dialogue.hpp b/apps/openmw/mwgui/dialogue.hpp index 6a31e8b7a..9da967cda 100644 --- a/apps/openmw/mwgui/dialogue.hpp +++ b/apps/openmw/mwgui/dialogue.hpp @@ -142,7 +142,8 @@ namespace MWGui void setPtr(const MWWorld::Ptr& actor) override; - void setKeywords(std::list keyWord); + /// @return true if stale keywords were updated successfully + bool setKeywords(std::list keyWord); void addResponse (const std::string& title, const std::string& text, bool needMargin = true); diff --git a/apps/openmw/mwgui/formatting.cpp b/apps/openmw/mwgui/formatting.cpp index 75bf8a342..156dc5b0d 100644 --- a/apps/openmw/mwgui/formatting.cpp +++ b/apps/openmw/mwgui/formatting.cpp @@ -30,14 +30,18 @@ namespace MWGui // vanilla game does not show any text after the last EOL tag. const std::string lowerText = Misc::StringUtils::lowerCase(mText); - int brIndex = lowerText.rfind("
"); - int pIndex = lowerText.rfind("

"); - if (brIndex == pIndex) - mText = ""; - else if (brIndex > pIndex) - mText = mText.substr(0, brIndex+4); - else - mText = mText.substr(0, pIndex+3); + size_t brIndex = lowerText.rfind("
"); + size_t pIndex = lowerText.rfind("

"); + mPlainTextEnd = 0; + if (brIndex != pIndex) + { + if (brIndex != std::string::npos && pIndex != std::string::npos) + mPlainTextEnd = std::max(brIndex, pIndex); + else if (brIndex != std::string::npos) + mPlainTextEnd = brIndex; + else + mPlainTextEnd = pIndex; + } registerTag("br", Event_BrTag); registerTag("p", Event_PTag); @@ -103,7 +107,8 @@ namespace MWGui { if (!mIgnoreLineEndings || ch != '\n') { - mBuffer.push_back(ch); + if (mIndex < mPlainTextEnd) + mBuffer.push_back(ch); mIgnoreLineEndings = false; mIgnoreNewlineTags = false; } diff --git a/apps/openmw/mwgui/formatting.hpp b/apps/openmw/mwgui/formatting.hpp index d56351414..12a3d46ca 100644 --- a/apps/openmw/mwgui/formatting.hpp +++ b/apps/openmw/mwgui/formatting.hpp @@ -73,6 +73,8 @@ namespace MWGui bool mClosingTag; std::map mTagTypes; std::string mBuffer; + + size_t mPlainTextEnd; }; class Paginator diff --git a/apps/openmw/mwgui/keyboardnavigation.cpp b/apps/openmw/mwgui/keyboardnavigation.cpp index 50ab74fb5..f7d54adf6 100644 --- a/apps/openmw/mwgui/keyboardnavigation.cpp +++ b/apps/openmw/mwgui/keyboardnavigation.cpp @@ -78,7 +78,7 @@ void KeyboardNavigation::saveFocus(int mode) { mKeyFocus[mode] = focus; } - else + else if(shouldAcceptKeyFocus(mCurrentFocus)) { mKeyFocus[mode] = mCurrentFocus; } @@ -104,6 +104,7 @@ void KeyboardNavigation::_unlinkWidget(MyGUI::Widget *widget) mCurrentFocus = nullptr; } +#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) void styleFocusedButton(MyGUI::Widget* w) { if (w) @@ -114,6 +115,7 @@ void styleFocusedButton(MyGUI::Widget* w) } } } +#endif bool isRootParent(MyGUI::Widget* widget, MyGUI::Widget* root) { @@ -145,7 +147,9 @@ void KeyboardNavigation::onFrame() if (focus == mCurrentFocus) { +#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) styleFocusedButton(mCurrentFocus); +#endif return; } @@ -156,19 +160,21 @@ void KeyboardNavigation::onFrame() focus = mCurrentFocus; } - // style highlighted button (won't be needed for MyGUI 3.2.3) if (focus != mCurrentFocus) { +#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) if (mCurrentFocus) { if (MyGUI::Button* b = mCurrentFocus->castType(false)) b->_setWidgetState("normal"); } - +#endif mCurrentFocus = focus; } +#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) styleFocusedButton(mCurrentFocus); +#endif } void KeyboardNavigation::setDefaultFocus(MyGUI::Widget *window, MyGUI::Widget *defaultFocus) diff --git a/apps/openmw/mwgui/loadingscreen.cpp b/apps/openmw/mwgui/loadingscreen.cpp index cd0384bb0..7ddc8c550 100644 --- a/apps/openmw/mwgui/loadingscreen.cpp +++ b/apps/openmw/mwgui/loadingscreen.cpp @@ -1,10 +1,12 @@ #include "loadingscreen.hpp" #include +#include #include #include +#include #include #include @@ -43,6 +45,8 @@ namespace MWGui , mNestedLoadingCount(0) , mProgress(0) , mShowWallpaper(true) + , mOldCallback(nullptr) + , mHasCallback(false) { mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); @@ -136,24 +140,54 @@ namespace MWGui { public: CopyFramebufferToTextureCallback(osg::Texture2D* texture) - : mTexture(texture) - , oneshot(true) + : mOneshot(true) + , mTexture(texture) { } void operator () (osg::RenderInfo& renderInfo) const override { - if (!oneshot) - return; - oneshot = false; + { + std::unique_lock lock(mMutex); + mOneshot = false; + } + mSignal.notify_all(); + int w = renderInfo.getCurrentCamera()->getViewport()->width(); int h = renderInfo.getCurrentCamera()->getViewport()->height(); mTexture->copyTexImage2D(*renderInfo.getState(), 0, 0, w, h); + + { + std::unique_lock lock(mMutex); + mOneshot = false; + } + mSignal.notify_all(); + } + + void wait() + { + std::unique_lock lock(mMutex); + while (mOneshot) + mSignal.wait(lock); + } + + void waitUntilInvoked() + { + std::unique_lock lock(mMutex); + while (mOneshot) + mSignal.wait(lock); + } + + void reset() + { + mOneshot = true; } private: + mutable bool mOneshot; + mutable std::mutex mMutex; + mutable std::condition_variable mSignal; osg::ref_ptr mTexture; - mutable bool oneshot; }; class DontComputeBoundCallback : public osg::Node::ComputeBoundingSphereCallback @@ -322,9 +356,20 @@ namespace MWGui mGuiTexture.reset(new osgMyGUI::OSGTexture(mTexture)); } - // Notice that the next time this is called, the current CopyFramebufferToTextureCallback will be deleted - // so there's no memory leak as at most one object of type CopyFramebufferToTextureCallback is allocated at a time. - mViewer->getCamera()->setInitialDrawCallback(new CopyFramebufferToTextureCallback(mTexture)); + if (!mCopyFramebufferToTextureCallback) + { + mCopyFramebufferToTextureCallback = new CopyFramebufferToTextureCallback(mTexture); + } + +#if OSG_VERSION_GREATER_OR_EQUAL(3, 5, 10) + mViewer->getCamera()->addInitialDrawCallback(mCopyFramebufferToTextureCallback); +#else + // TODO: Remove once we officially end support for OSG versions pre 3.5.10 + mOldCallback = mViewer->getCamera()->getInitialDrawCallback(); + mViewer->getCamera()->setInitialDrawCallback(mCopyFramebufferToTextureCallback); +#endif + mCopyFramebufferToTextureCallback->reset(); + mHasCallback = true; mBackgroundImage->setBackgroundImage(""); mBackgroundImage->setVisible(false); @@ -367,6 +412,21 @@ namespace MWGui mViewer->renderingTraversals(); mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); + if (mHasCallback) + { + mCopyFramebufferToTextureCallback->waitUntilInvoked(); + + // Note that we are removing the callback before the draw thread has returned from it. + // This is OK as we are retaining the ref_ptr. +#if OSG_VERSION_GREATER_OR_EQUAL(3, 5, 10) + mViewer->getCamera()->removeInitialDrawCallback(mCopyFramebufferToTextureCallback); +#else + // TODO: Remove once we officially end support for OSG versions pre 3.5.10 + mViewer->getCamera()->setInitialDrawCallback(mOldCallback); +#endif + mHasCallback = false; + } + mLastRenderTime = mTimer.time_m(); } diff --git a/apps/openmw/mwgui/loadingscreen.hpp b/apps/openmw/mwgui/loadingscreen.hpp index 2577827aa..5d86ed389 100644 --- a/apps/openmw/mwgui/loadingscreen.hpp +++ b/apps/openmw/mwgui/loadingscreen.hpp @@ -3,6 +3,7 @@ #include +#include #include #include @@ -28,6 +29,7 @@ namespace Resource namespace MWGui { class BackgroundImage; + class CopyFramebufferToTextureCallback; class LoadingScreen : public WindowBase, public Loading::Listener { @@ -84,6 +86,9 @@ namespace MWGui std::vector mSplashScreens; osg::ref_ptr mTexture; + osg::ref_ptr mCopyFramebufferToTextureCallback; + osg::ref_ptr mOldCallback; + bool mHasCallback; std::unique_ptr mGuiTexture; void changeWallpaper(); diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index 136547c4c..78b9171e5 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -48,9 +48,9 @@ namespace MWGui const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - for (unsigned int i = 0; i < effects.mList.size(); ++i) + for (const auto& effect : effects.mList) { - short effectId = effects.mList[i].mEffectID; + short effectId = effect.mEffectID; if (effectId != -1) { @@ -59,14 +59,14 @@ namespace MWGui std::string effectIDStr = ESM::MagicEffect::effectIdToString(effectId); std::string fullEffectName = wm->getGameSettingString(effectIDStr, ""); - if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill && effects.mList[i].mSkill != -1) + if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill && effect.mSkill != -1) { - fullEffectName += " " + wm->getGameSettingString(ESM::Skill::sSkillNameIds[effects.mList[i].mSkill], ""); + fullEffectName += " " + wm->getGameSettingString(ESM::Skill::sSkillNameIds[effect.mSkill], ""); } - if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute && effects.mList[i].mAttribute != -1) + if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute && effect.mAttribute != -1) { - fullEffectName += " " + wm->getGameSettingString(ESM::Attribute::sGmstAttributeIds[effects.mList[i].mAttribute], ""); + fullEffectName += " " + wm->getGameSettingString(ESM::Attribute::sGmstAttributeIds[effect.mAttribute], ""); } std::string convert = Misc::StringUtils::lowerCaseUtf8(fullEffectName); diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp index 2a3e2cd85..66a1ea1ef 100644 --- a/apps/openmw/mwgui/statswindow.cpp +++ b/apps/openmw/mwgui/statswindow.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -335,6 +336,17 @@ namespace MWGui { int max = MWBase::Environment::get().getWorld()->getStore().get().find("iLevelUpTotal")->mValue.getInteger(); getWidget(levelWidget, i==0 ? "Level_str" : "LevelText"); + + std::stringstream detail; + for (int i = 0; i < ESM::Attribute::Length; ++i) + { + if (auto increase = PCstats.getLevelUpAttributeIncrease(i)) + detail << (detail.str().empty() ? "" : "\n") << "#{" + << MyGUI::TextIterator::toTagsString(ESM::Attribute::sGmstAttributeIds[i]) + << "} x" << MyGUI::utility::toString(increase); + } + if (!detail.str().empty()) + levelWidget->setUserString("Caption_LevelDetailText", MyGUI::LanguageManager::getInstance().replaceTags(detail.str())); levelWidget->setUserString("RangePosition_LevelProgress", MyGUI::utility::toString(PCstats.getLevelProgress())); levelWidget->setUserString("Range_LevelProgress", MyGUI::utility::toString(max)); levelWidget->setUserString("Caption_LevelProgressText", MyGUI::utility::toString(PCstats.getLevelProgress()) + "/" diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 3de7947c2..c18755630 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -161,6 +161,29 @@ void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float magicka = fRestMagicMult * stats.getAttribute(ESM::Attribute::Intelligence).getModified(); } +template +void forEachFollowingPackage(MWMechanics::Actors::PtrActorMap& actors, const MWWorld::Ptr& actor, const MWWorld::Ptr& player, T&& func) +{ + for(auto& iter : actors) + { + const MWWorld::Ptr &iteratedActor = iter.first; + if (iteratedActor == player || iteratedActor == actor) + continue; + + const MWMechanics::CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor); + if (stats.isDead()) + continue; + + // An actor counts as following if AiFollow is the current AiPackage, + // or there are only Combat and Wander packages before the AiFollow package + for (const auto& package : stats.getAiSequence()) + { + if(!func(iter, package)) + break; + } + } +} + } namespace MWMechanics @@ -1482,6 +1505,13 @@ namespace MWMechanics if (heldIter != inventoryStore.end() && heldIter->getTypeName() != typeid(ESM::Light).name()) inventoryStore.unequipItem(*heldIter, ptr); } + else if (heldIter == inventoryStore.end() || heldIter->getTypeName() == typeid(ESM::Light).name()) + { + // For hostile NPCs, see if they have anything better to equip first + auto shield = inventoryStore.getPreferredShield(ptr); + if(shield != inventoryStore.end()) + inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, shield, ptr); + } heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); @@ -2788,26 +2818,14 @@ namespace MWMechanics std::list Actors::getActorsFollowing(const MWWorld::Ptr& actor) { std::list list; - for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) + forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr& package) { - const MWWorld::Ptr &iteratedActor = iter->first; - if (iteratedActor == getPlayer() || iteratedActor == actor) - continue; - - const CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor); - if (stats.isDead()) - continue; - - // An actor counts as following if AiFollow is the current AiPackage, - // or there are only Combat and Wander packages before the AiFollow package - for (const auto& package : stats.getAiSequence()) - { - if (package->followTargetThroughDoors() && package->getTarget() == actor) - list.push_back(iteratedActor); - else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) - break; - } - } + if (package->followTargetThroughDoors() && package->getTarget() == actor) + list.push_back(iter.first); + else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) + return false; + return true; + }); return list; } @@ -2851,32 +2869,38 @@ namespace MWMechanics std::list Actors::getActorsFollowingIndices(const MWWorld::Ptr &actor) { std::list list; - for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) + forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr& package) { - const MWWorld::Ptr &iteratedActor = iter->first; - if (iteratedActor == getPlayer() || iteratedActor == actor) - continue; - - const CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor); - if (stats.isDead()) - continue; - - // An actor counts as following if AiFollow is the current AiPackage, - // or there are only Combat and Wander packages before the AiFollow package - for (const auto& package : stats.getAiSequence()) + if (package->followTargetThroughDoors() && package->getTarget() == actor) { - if (package->followTargetThroughDoors() && package->getTarget() == actor) - { - list.push_back(static_cast(package.get())->getFollowIndex()); - break; - } - else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) - break; + list.push_back(static_cast(package.get())->getFollowIndex()); + return false; } - } + else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) + return false; + return true; + }); return list; } + std::map Actors::getActorsFollowingByIndex(const MWWorld::Ptr &actor) + { + std::map map; + forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr& package) + { + if (package->followTargetThroughDoors() && package->getTarget() == actor) + { + int index = static_cast(package.get())->getFollowIndex(); + map[index] = iter.first; + return false; + } + else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) + return false; + return true; + }); + return map; + } + std::list Actors::getActorsFighting(const MWWorld::Ptr& actor) { std::list list; std::vector neighbors; diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 8543537fe..59890aa16 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -191,6 +191,7 @@ namespace MWMechanics /// Get the list of AiFollow::mFollowIndex for all actors following this target std::list getActorsFollowingIndices(const MWWorld::Ptr& actor); + std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor); ///Returns the list of actors which are fighting the given actor /**ie AiCombat is active and the target is the actor **/ diff --git a/apps/openmw/mwmechanics/aicast.cpp b/apps/openmw/mwmechanics/aicast.cpp index 9ad7b4c56..af3aac340 100644 --- a/apps/openmw/mwmechanics/aicast.cpp +++ b/apps/openmw/mwmechanics/aicast.cpp @@ -46,27 +46,29 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac { return false; } - - osg::Vec3f targetPos = target.getRefData().getPosition().asVec3(); - if (target.getClass().isActor()) - { - osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(target); - targetPos.z() += halfExtents.z() * 2 * 0.75f; - } - - osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); - osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); - actorPos.z() += halfExtents.z() * 2 * 0.75f; - - osg::Vec3f dir = targetPos - actorPos; - - bool turned = smoothTurn(actor, getZAngleToDir(dir), 2, osg::DegreesToRadians(3.f)); - turned &= smoothTurn(actor, getXAngleToDir(dir), 0, osg::DegreesToRadians(3.f)); - - if (!turned) - return false; } + osg::Vec3f targetPos = target.getRefData().getPosition().asVec3(); + // If the target of an on-target spell is an actor that is not the caster + // the target position must be adjusted so that it's not casted at the actor's feet. + if (target != actor && target.getClass().isActor()) + { + osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(target); + targetPos.z() += halfExtents.z() * 2 * 0.75f; + } + + osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); + osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); + actorPos.z() += halfExtents.z() * 2 * 0.75f; + + osg::Vec3f dir = targetPos - actorPos; + + bool turned = smoothTurn(actor, getZAngleToDir(dir), 2, osg::DegreesToRadians(3.f)); + turned &= smoothTurn(actor, getXAngleToDir(dir), 0, osg::DegreesToRadians(3.f)); + + if (!turned) + return false; + // Check if the actor is already casting another spell bool isCasting = MWBase::Environment::get().getMechanicsManager()->isCastingSpell(actor); if (isCasting && !mCasting) diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index 8a1fc54f7..59d5db0a7 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -124,24 +124,22 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte if (!mActive) return false; - // The distances below are approximations based on observations of the original engine. - // If only one actor is following the target, it uses 186. - // If there are multiple actors following the same target, they form a group with each group member at 313 + (130 * i) distance to the target. - - short followDistance = 186; - std::list followers = MWBase::Environment::get().getMechanicsManager()->getActorsFollowingIndices(target); - if (followers.size() >= 2) + // In the original engine the first follower stays closer to the player than any subsequent followers. + // Followers beyond the first usually attempt to stand inside each other. + osg::Vec3f::value_type floatingDistance = 0; + auto followers = MWBase::Environment::get().getMechanicsManager()->getActorsFollowingByIndex(target); + if (followers.size() >= 2 && followers.cbegin()->first != mFollowIndex) { - followDistance = 313; - short i = 0; - followers.sort(); - for (int followIndex : followers) + for(auto& follower : followers) { - if (followIndex == mFollowIndex) - followDistance += 130 * i; - ++i; + auto halfExtent = MWBase::Environment::get().getWorld()->getHalfExtents(follower.second).y(); + if(halfExtent > floatingDistance) + floatingDistance = halfExtent; } } + floatingDistance += MWBase::Environment::get().getWorld()->getHalfExtents(target).y(); + floatingDistance += MWBase::Environment::get().getWorld()->getHalfExtents(actor).y() * 2; + short followDistance = static_cast(floatingDistance); if (!mAlwaysFollow) //Update if you only follow for a bit { diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 4bffd28ba..8880820dd 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -158,7 +158,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& zTurn(actor, getZAngleToPoint(position, dest)); smoothTurn(actor, getXAngleToPoint(position, dest), 0); world->removeActorPath(actor); - return true; + return isDestReached; } world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest); diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 57d32898c..575a03434 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -403,7 +403,7 @@ const AiPackage& MWMechanics::AiSequence::getActivePackage() void AiSequence::fill(const ESM::AIPackageList &list) { // If there is more than one package in the list, enable repeating - if (!list.mList.empty() && list.mList.begin() != (list.mList.end()-1)) + if (list.mList.size() >= 2) mRepeat = true; for (const auto& esmPackage : list.mList) @@ -459,8 +459,15 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) int count = 0; for (auto& container : sequence.mPackages) { - if (isActualAiPackage(static_cast(container.mType))) - count++; + switch (container.mType) + { + case ESM::AiSequence::Ai_Wander: + case ESM::AiSequence::Ai_Travel: + case ESM::AiSequence::Ai_Escort: + case ESM::AiSequence::Ai_Follow: + case ESM::AiSequence::Ai_Activate: + ++count; + } } if (count > 1) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 4514549ed..e6ca75455 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -2156,7 +2156,7 @@ void CharacterController::update(float duration, bool animationOnly) movementSettings.mSpeedFactor *= 2.f; static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); - if (smoothMovement && !isFirstPersonPlayer) + if (smoothMovement) { static const float playerTurningCoef = 1.0 / std::max(0.01f, Settings::Manager::getFloat("smooth movement player turning delay", "Game")); float angle = mPtr.getRefData().getPosition().rot[2]; @@ -2166,7 +2166,9 @@ void CharacterController::update(float duration, bool animationOnly) float deltaLen = delta.length(); float maxDelta; - if (std::abs(speedDelta) < deltaLen / 2) + if (isFirstPersonPlayer) + maxDelta = 1; + else if (std::abs(speedDelta) < deltaLen / 2) // Turning is smooth for player and less smooth for NPCs (otherwise NPC can miss a path point). maxDelta = duration * (isPlayer ? playerTurningCoef : 6.f); else if (isPlayer && speedDelta < -deltaLen / 2) @@ -2204,7 +2206,10 @@ void CharacterController::update(float duration, bool animationOnly) bool canMove = cls.getMaxSpeed(mPtr) > 0; static const bool turnToMovementDirection = Settings::Manager::getBool("turn to movement direction", "Game"); if (!turnToMovementDirection || isFirstPersonPlayer) + { movementSettings.mIsStrafing = std::abs(vec.x()) > std::abs(vec.y()) * 2; + stats.setSideMovementAngle(0); + } else if (canMove) { float targetMovementAngle = vec.y() >= 0 ? std::atan2(-vec.x(), vec.y()) : std::atan2(vec.x(), -vec.y()); @@ -2456,18 +2461,19 @@ void CharacterController::update(float duration, bool animationOnly) sndMgr->playSound3D(mPtr, sound, 1.f, 1.f, MWSound::Type::Foot, MWSound::PlayMode::NoPlayerLocal); } - if (turnToMovementDirection) + if (turnToMovementDirection && !isFirstPersonPlayer && + (movestate == CharState_SwimRunForward || movestate == CharState_SwimWalkForward || + movestate == CharState_SwimRunBack || movestate == CharState_SwimWalkBack)) { - float targetSwimmingPitch; - if (inwater && vec.y() != 0 && !isFirstPersonPlayer && !movementSettings.mIsStrafing) - targetSwimmingPitch = -mPtr.getRefData().getPosition().rot[0]; - else - targetSwimmingPitch = 0; - float maxSwimPitchDelta = 3.0f * duration; float swimmingPitch = mAnimation->getBodyPitchRadians(); + float targetSwimmingPitch = -mPtr.getRefData().getPosition().rot[0]; + float maxSwimPitchDelta = 3.0f * duration; swimmingPitch += osg::clampBetween(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta); mAnimation->setBodyPitchRadians(swimmingPitch); } + else + mAnimation->setBodyPitchRadians(0); + static const bool swimUpwardCorrection = Settings::Manager::getBool("swim upward correction", "Game"); if (inwater && isPlayer && !isFirstPersonPlayer && swimUpwardCorrection) { @@ -2615,7 +2621,7 @@ void CharacterController::update(float duration, bool animationOnly) moved.y() *= scale; // Ensure we're moving in generally the right direction... - if(speed > 0.f) + if (speed > 0.f && moved != osg::Vec3f()) { float l = moved.length(); if (std::abs(movement.x() - moved.x()) > std::abs(moved.x()) / 2 || @@ -2631,8 +2637,14 @@ void CharacterController::update(float duration, bool animationOnly) } } - if (mFloatToSurface && cls.isActor() && cls.getCreatureStats(mPtr).isDead() && cls.canSwim(mPtr)) - moved.z() = 1.0; + if (mFloatToSurface && cls.isActor() && cls.canSwim(mPtr)) + { + if (cls.getCreatureStats(mPtr).isDead() + || (!godmode && cls.getCreatureStats(mPtr).isParalyzed())) + { + moved.z() = 1.0; + } + } // Update movement if(!animationOnly && mMovementAnimationControlled && mPtr.getClass().isActor()) diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index f42c3cf44..fe4a55216 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1794,6 +1794,11 @@ namespace MWMechanics return mActors.getActorsFollowingIndices(actor); } + std::map MechanicsManager::getActorsFollowingByIndex(const MWWorld::Ptr& actor) + { + return mActors.getActorsFollowingByIndex(actor); + } + std::list MechanicsManager::getActorsFighting(const MWWorld::Ptr& actor) { return mActors.getActorsFighting(actor); } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index c1a59fccd..1087fd6fc 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -160,6 +160,7 @@ namespace MWMechanics std::list getActorsSidingWith(const MWWorld::Ptr& actor) override; std::list getActorsFollowing(const MWWorld::Ptr& actor) override; std::list getActorsFollowingIndices(const MWWorld::Ptr& actor) override; + std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor) override; std::list getActorsFighting(const MWWorld::Ptr& actor) override; std::list getEnemiesNearby(const MWWorld::Ptr& actor) override; diff --git a/apps/openmw/mwmechanics/npcstats.cpp b/apps/openmw/mwmechanics/npcstats.cpp index b3a234a1b..b96bea419 100644 --- a/apps/openmw/mwmechanics/npcstats.cpp +++ b/apps/openmw/mwmechanics/npcstats.cpp @@ -383,6 +383,11 @@ void MWMechanics::NpcStats::updateHealth() setHealth(floor(0.5f * (strength + endurance))); } +int MWMechanics::NpcStats::getLevelUpAttributeIncrease(int attribute) const +{ + return mSkillIncreases[attribute]; +} + int MWMechanics::NpcStats::getLevelupAttributeMultiplier(int attribute) const { int num = mSkillIncreases[attribute]; diff --git a/apps/openmw/mwmechanics/npcstats.hpp b/apps/openmw/mwmechanics/npcstats.hpp index b693cf271..86fc4ef0d 100644 --- a/apps/openmw/mwmechanics/npcstats.hpp +++ b/apps/openmw/mwmechanics/npcstats.hpp @@ -131,6 +131,8 @@ namespace MWMechanics End of tes3mp addition */ + int getLevelUpAttributeIncrease(int attribute) const; + int getLevelupAttributeMultiplier(int attribute) const; int getSkillIncreasesForSpecialization(int spec) const; diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index 78b8abafb..b84068cf9 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -13,7 +13,6 @@ End of tes3mp addition */ -#include #include #include @@ -27,13 +26,15 @@ #include "collisiontype.hpp" #include "mtphysics.hpp" +#include + namespace MWPhysics { Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler) : mStandingOnPtr(nullptr), mCanWaterWalk(false), mWalkingOnWater(false) - , mCollisionObject(nullptr), mMeshTranslation(shape->mCollisionBoxTranslate), mHalfExtents(shape->mCollisionBoxHalfExtents) + , mCollisionObject(nullptr), mMeshTranslation(shape->mCollisionBox.center), mHalfExtents(shape->mCollisionBox.extents) , mForce(0.f, 0.f, 0.f), mOnGround(true), mOnSlope(false) , mInternalCollisionMode(true) , mExternalCollisionMode(true) @@ -65,17 +66,8 @@ Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, Physic Log(Debug::Error) << "Error: Failed to calculate bounding box for actor \"" << ptr.getCellRef().getRefId() << "\"."; } - // Use capsule shape only if base is square (nonuniform scaling apparently doesn't work on it) - if (std::abs(mHalfExtents.x()-mHalfExtents.y())= mHalfExtents.x()) - { - mShape.reset(new btCapsuleShapeZ(mHalfExtents.x(), 2*mHalfExtents.z() - 2*mHalfExtents.x())); - mRotationallyInvariant = true; - } - else - { - mShape.reset(new btBoxShape(Misc::Convert::toBullet(mHalfExtents))); - mRotationallyInvariant = false; - } + mShape.reset(new btBoxShape(Misc::Convert::toBullet(mHalfExtents))); + mRotationallyInvariant = (mMeshTranslation.x() == 0.0 && mMeshTranslation.y() == 0.0) && std::fabs(mHalfExtents.x() - mHalfExtents.y()) < 2.2; mConvexShape = static_cast(mShape.get()); @@ -83,11 +75,14 @@ Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, Physic mCollisionObject->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT); mCollisionObject->setActivationState(DISABLE_DEACTIVATION); mCollisionObject->setCollisionShape(mShape.get()); - mCollisionObject->setUserPointer(static_cast(this)); + mCollisionObject->setUserPointer(this); - updateRotation(); updateScale(); - resetPosition(); + + if(!mRotationallyInvariant) + updateRotation(); + + updatePosition(); addCollisionMask(getCollisionMask()); /* @@ -110,6 +105,8 @@ Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, Physic /* End of tes3mp addition */ + + updateCollisionObjectPosition(); } Actor::~Actor() @@ -152,26 +149,34 @@ int Actor::getCollisionMask() const return collisionMask; } -void Actor::updatePositionUnsafe() -{ - mWorldPosition = mPtr.getRefData().getPosition().asVec3(); -} - void Actor::updatePosition() { std::scoped_lock lock(mPositionMutex); - updatePositionUnsafe(); + updateWorldPosition(); + mPreviousPosition = mWorldPosition; + mPosition = mWorldPosition; + mSimulationPosition = mWorldPosition; + mStandingOnPtr = nullptr; + mSkipSimulation = true; +} + +void Actor::updateWorldPosition() +{ + if (mWorldPosition != mPtr.getRefData().getPosition().asVec3()) + mWorldPositionChanged = true; + mWorldPosition = mPtr.getRefData().getPosition().asVec3(); } osg::Vec3f Actor::getWorldPosition() const { - std::scoped_lock lock(mPositionMutex); return mWorldPosition; } void Actor::setSimulationPosition(const osg::Vec3f& position) { - mSimulationPosition = position; + if (!mSkipSimulation) + mSimulationPosition = position; + mSkipSimulation = false; } osg::Vec3f Actor::getSimulationPosition() const @@ -179,20 +184,21 @@ osg::Vec3f Actor::getSimulationPosition() const return mSimulationPosition; } -void Actor::updateCollisionObjectPositionUnsafe() +osg::Vec3f Actor::getScaledMeshTranslation() const { + return mRotation * osg::componentMultiply(mMeshTranslation, mScale); +} + +void Actor::updateCollisionObjectPosition() +{ + std::scoped_lock lock(mPositionMutex); mShape->setLocalScaling(Misc::Convert::toBullet(mScale)); osg::Vec3f scaledTranslation = mRotation * osg::componentMultiply(mMeshTranslation, mScale); osg::Vec3f newPosition = scaledTranslation + mPosition; mLocalTransform.setOrigin(Misc::Convert::toBullet(newPosition)); mLocalTransform.setRotation(Misc::Convert::toBullet(mRotation)); mCollisionObject->setWorldTransform(mLocalTransform); -} - -void Actor::updateCollisionObjectPosition() -{ - std::scoped_lock lock(mPositionMutex); - updateCollisionObjectPositionUnsafe(); + mWorldPositionChanged = false; } osg::Vec3f Actor::getCollisionObjectPosition() const @@ -201,28 +207,20 @@ osg::Vec3f Actor::getCollisionObjectPosition() const return Misc::Convert::toOsg(mLocalTransform.getOrigin()); } -void Actor::setPosition(const osg::Vec3f& position) +bool Actor::setPosition(const osg::Vec3f& position) { std::scoped_lock lock(mPositionMutex); - mPreviousPosition = mPosition; - mPosition = position; + bool hasChanged = mPosition != position || mPositionOffset.length() != 0 || mWorldPositionChanged; + mPreviousPosition = mPosition + mPositionOffset; + mPosition = position + mPositionOffset; + mPositionOffset = osg::Vec3f(); + return hasChanged; } void Actor::adjustPosition(const osg::Vec3f& offset) { std::scoped_lock lock(mPositionMutex); - mPosition += offset; - mPreviousPosition += offset; -} - -void Actor::resetPosition() -{ - std::scoped_lock lock(mPositionMutex); - updatePositionUnsafe(); - mPreviousPosition = mWorldPosition; - mPosition = mWorldPosition; - mSimulationPosition = mWorldPosition; - updateCollisionObjectPositionUnsafe(); + mPositionOffset += offset; } osg::Vec3f Actor::getPosition() const @@ -238,8 +236,6 @@ osg::Vec3f Actor::getPreviousPosition() const void Actor::updateRotation () { std::scoped_lock lock(mPositionMutex); - if (mRotation == mPtr.getRefData().getBaseNode()->getAttitude()) - return; mRotation = mPtr.getRefData().getBaseNode()->getAttitude(); } @@ -270,7 +266,6 @@ osg::Vec3f Actor::getHalfExtents() const osg::Vec3f Actor::getOriginalHalfExtents() const { - std::scoped_lock lock(mPositionMutex); return mHalfExtents; } @@ -307,7 +302,6 @@ void Actor::setWalkingOnWater(bool walkingOnWater) void Actor::setCanWaterWalk(bool waterWalk) { - std::scoped_lock lock(mPositionMutex); if (waterWalk != mCanWaterWalk) { mCanWaterWalk = waterWalk; diff --git a/apps/openmw/mwphysics/actor.hpp b/apps/openmw/mwphysics/actor.hpp index 07a9bebd2..9d129f2ba 100644 --- a/apps/openmw/mwphysics/actor.hpp +++ b/apps/openmw/mwphysics/actor.hpp @@ -60,7 +60,7 @@ namespace MWPhysics * Set mWorldPosition to the position in the Ptr's RefData. This is used by the physics simulation to account for * when an object is "instantly" moved/teleported as opposed to being moved by the physics simulation. */ - void updatePosition(); + void updateWorldPosition(); osg::Vec3f getWorldPosition() const; /** @@ -82,6 +82,9 @@ namespace MWPhysics */ osg::Vec3f getOriginalHalfExtents() const; + /// Returns the mesh translation, scaled and rotated as necessary + osg::Vec3f getScaledMeshTranslation() const; + /** * Returns the position of the collision body * @note The collision shape's origin is in its center, so the position returned can be described as center of the actor collision box in world space. @@ -90,9 +93,10 @@ namespace MWPhysics /** * Store the current position into mPreviousPosition, then move to this position. + * Returns true if the new position is different. */ - void setPosition(const osg::Vec3f& position); - void resetPosition(); + bool setPosition(const osg::Vec3f& position); + void updatePosition(); void adjustPosition(const osg::Vec3f& offset); osg::Vec3f getPosition() const; @@ -154,8 +158,6 @@ namespace MWPhysics void updateCollisionMask(); void addCollisionMask(int collisionMask); int getCollisionMask() const; - void updateCollisionObjectPositionUnsafe(); - void updatePositionUnsafe(); bool mCanWaterWalk; std::atomic mWalkingOnWater; @@ -177,6 +179,9 @@ namespace MWPhysics osg::Vec3f mSimulationPosition; osg::Vec3f mPosition; osg::Vec3f mPreviousPosition; + osg::Vec3f mPositionOffset; + bool mWorldPositionChanged; + bool mSkipSimulation; btTransform mLocalTransform; mutable std::mutex mPositionMutex; diff --git a/apps/openmw/mwphysics/closestnotmeconvexresultcallback.cpp b/apps/openmw/mwphysics/closestnotmeconvexresultcallback.cpp index ddfdb8a42..27e7a390c 100644 --- a/apps/openmw/mwphysics/closestnotmeconvexresultcallback.cpp +++ b/apps/openmw/mwphysics/closestnotmeconvexresultcallback.cpp @@ -1,20 +1,92 @@ +#include + #include "closestnotmeconvexresultcallback.hpp" +#include "collisiontype.hpp" +#include "contacttestwrapper.h" #include +#include + +#include "collisiontype.hpp" +#include "projectile.hpp" namespace MWPhysics { - ClosestNotMeConvexResultCallback::ClosestNotMeConvexResultCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot) + class ActorOverlapTester : public btCollisionWorld::ContactResultCallback + { + public: + bool overlapping = false; + + btScalar addSingleResult(btManifoldPoint& cp, + const btCollisionObjectWrapper* colObj0Wrap, + int partId0, + int index0, + const btCollisionObjectWrapper* colObj1Wrap, + int partId1, + int index1) override + { + if(cp.getDistance() <= 0.0f) + overlapping = true; + return btScalar(1); + } + }; + + ClosestNotMeConvexResultCallback::ClosestNotMeConvexResultCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot, const btCollisionWorld * world) : btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)), - mMe(me), mMotion(motion), mMinCollisionDot(minCollisionDot) + mMe(me), mMotion(motion), mMinCollisionDot(minCollisionDot), mWorld(world) { } - btScalar ClosestNotMeConvexResultCallback::addSingleResult(btCollisionWorld::LocalConvexResult& convexResult,bool normalInWorldSpace) + btScalar ClosestNotMeConvexResultCallback::addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace) { if (convexResult.m_hitCollisionObject == mMe) return btScalar(1); + // override data for actor-actor collisions + // vanilla Morrowind seems to make overlapping actors collide as though they are both cylinders with a diameter of the distance between them + // For some reason this doesn't work as well as it should when using capsules, but it still helps a lot. + if(convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor) + { + ActorOverlapTester isOverlapping; + // FIXME: This is absolutely terrible and bullet should feel terrible for not making contactPairTest const-correct. + ContactTestWrapper::contactPairTest(const_cast(mWorld), const_cast(mMe), const_cast(convexResult.m_hitCollisionObject), isOverlapping); + + if(isOverlapping.overlapping) + { + auto originA = Misc::Convert::toOsg(mMe->getWorldTransform().getOrigin()); + auto originB = Misc::Convert::toOsg(convexResult.m_hitCollisionObject->getWorldTransform().getOrigin()); + osg::Vec3f motion = Misc::Convert::toOsg(mMotion); + osg::Vec3f normal = (originA-originB); + normal.z() = 0; + normal.normalize(); + // only collide if horizontally moving towards the hit actor (note: the motion vector appears to be inverted) + // FIXME: This kinda screws with standing on actors that walk up slopes for some reason. Makes you fall through them. + // It happens in vanilla Morrowind too, but much less often. + // I tried hunting down why but couldn't figure it out. Possibly a stair stepping or ground ejection bug. + if(normal * motion > 0.0f) + { + convexResult.m_hitFraction = 0.0f; + convexResult.m_hitNormalLocal = Misc::Convert::toBullet(normal); + return ClosestConvexResultCallback::addSingleResult(convexResult, true); + } + else + { + return btScalar(1); + } + } + } + if (convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Projectile) + { + auto* projectileHolder = static_cast(convexResult.m_hitCollisionObject->getUserPointer()); + if (!projectileHolder->isActive()) + return btScalar(1); + auto* targetHolder = static_cast(mMe->getUserPointer()); + const MWWorld::Ptr target = targetHolder->getPtr(); + if (projectileHolder->isValidTarget(target)) + projectileHolder->hit(target, convexResult.m_hitPointLocal, convexResult.m_hitNormalLocal); + return btScalar(1); + } + btVector3 hitNormalWorld; if (normalInWorldSpace) hitNormalWorld = convexResult.m_hitNormalLocal; diff --git a/apps/openmw/mwphysics/closestnotmeconvexresultcallback.hpp b/apps/openmw/mwphysics/closestnotmeconvexresultcallback.hpp index 97aaa64a1..538721ad8 100644 --- a/apps/openmw/mwphysics/closestnotmeconvexresultcallback.hpp +++ b/apps/openmw/mwphysics/closestnotmeconvexresultcallback.hpp @@ -10,7 +10,7 @@ namespace MWPhysics class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback { public: - ClosestNotMeConvexResultCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot); + ClosestNotMeConvexResultCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot, const btCollisionWorld * world); btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult,bool normalInWorldSpace) override; @@ -18,6 +18,7 @@ namespace MWPhysics const btCollisionObject *mMe; const btVector3 mMotion; const btScalar mMinCollisionDot; + const btCollisionWorld * mWorld; }; } diff --git a/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp b/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp index 86763a793..c3104f860 100644 --- a/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp +++ b/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp @@ -1,18 +1,22 @@ #include "closestnotmerayresultcallback.hpp" #include +#include #include #include "../mwworld/class.hpp" +#include "actor.hpp" +#include "collisiontype.hpp" +#include "projectile.hpp" #include "ptrholder.hpp" namespace MWPhysics { - ClosestNotMeRayResultCallback::ClosestNotMeRayResultCallback(const btCollisionObject* me, const std::vector& targets, const btVector3& from, const btVector3& to) + ClosestNotMeRayResultCallback::ClosestNotMeRayResultCallback(const btCollisionObject* me, std::vector targets, const btVector3& from, const btVector3& to, Projectile* proj) : btCollisionWorld::ClosestRayResultCallback(from, to) - , mMe(me), mTargets(targets) + , mMe(me), mTargets(std::move(targets)), mProjectile(proj) { } @@ -20,15 +24,41 @@ namespace MWPhysics { if (rayResult.m_collisionObject == mMe) return 1.f; + + if (mProjectile && rayResult.m_collisionObject == mProjectile->getCollisionObject()) + return 1.f; + if (!mTargets.empty()) { if ((std::find(mTargets.begin(), mTargets.end(), rayResult.m_collisionObject) == mTargets.end())) { - PtrHolder* holder = static_cast(rayResult.m_collisionObject->getUserPointer()); + auto* holder = static_cast(rayResult.m_collisionObject->getUserPointer()); if (holder && !holder->getPtr().isEmpty() && holder->getPtr().getClass().isActor()) return 1.f; } } - return btCollisionWorld::ClosestRayResultCallback::addSingleResult(rayResult, normalInWorldSpace); + + btCollisionWorld::ClosestRayResultCallback::addSingleResult(rayResult, normalInWorldSpace); + if (mProjectile) + { + switch (rayResult.m_collisionObject->getBroadphaseHandle()->m_collisionFilterGroup) + { + case CollisionType_Actor: + { + auto* target = static_cast(rayResult.m_collisionObject->getUserPointer()); + mProjectile->hit(target->getPtr(), m_hitPointWorld, m_hitNormalWorld); + break; + } + case CollisionType_Projectile: + { + auto* target = static_cast(rayResult.m_collisionObject->getUserPointer()); + target->hit(mProjectile->getPtr(), m_hitPointWorld, m_hitNormalWorld); + mProjectile->hit(target->getPtr(), m_hitPointWorld, m_hitNormalWorld); + break; + } + } + } + + return rayResult.m_hitFraction; } } diff --git a/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp b/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp index 23d52998c..b86427165 100644 --- a/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp +++ b/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp @@ -9,15 +9,18 @@ class btCollisionObject; namespace MWPhysics { + class Projectile; + class ClosestNotMeRayResultCallback : public btCollisionWorld::ClosestRayResultCallback { public: - ClosestNotMeRayResultCallback(const btCollisionObject* me, const std::vector& targets, const btVector3& from, const btVector3& to); + ClosestNotMeRayResultCallback(const btCollisionObject* me, std::vector targets, const btVector3& from, const btVector3& to, Projectile* proj=nullptr); btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) override; private: const btCollisionObject* mMe; const std::vector mTargets; + Projectile* mProjectile; }; } diff --git a/apps/openmw/mwphysics/constants.hpp b/apps/openmw/mwphysics/constants.hpp index 46367ab34..c6f2f3b53 100644 --- a/apps/openmw/mwphysics/constants.hpp +++ b/apps/openmw/mwphysics/constants.hpp @@ -5,12 +5,22 @@ namespace MWPhysics { static const float sStepSizeUp = 34.0f; static const float sStepSizeDown = 62.0f; - static const float sMinStep = 10.f; + + static const float sMinStep = 10.0f; // hack to skip over tiny unwalkable slopes + static const float sMinStep2 = 20.0f; // hack to skip over shorter but longer/wider/further unwalkable slopes + // whether to do the above stairstepping logic hacks to work around bad morrowind assets - disabling causes problems but improves performance + static const bool sDoExtraStairHacks = true; + static const float sGroundOffset = 1.0f; static const float sMaxSlope = 49.0f; // Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. static const int sMaxIterations = 8; + // Allows for more precise movement solving without getting stuck or snagging too easily. + static const float sCollisionMargin = 0.1; + // Allow for a small amount of penetration to prevent numerical precision issues from causing the "unstuck"ing code to run unnecessarily + // Currently set to 0 because having the "unstuck"ing code run whenever possible prevents some glitchy snagging issues + static const float sAllowedPenetration = 0.0; } #endif diff --git a/apps/openmw/mwphysics/contacttestwrapper.cpp b/apps/openmw/mwphysics/contacttestwrapper.cpp new file mode 100644 index 000000000..c11a7e292 --- /dev/null +++ b/apps/openmw/mwphysics/contacttestwrapper.cpp @@ -0,0 +1,21 @@ +#include + +#include "contacttestwrapper.h" + +namespace MWPhysics +{ + // Concurrent calls to contactPairTest (and by extension contactTest) are forbidden. + static std::mutex contactMutex; + void ContactTestWrapper::contactTest(btCollisionWorld* collisionWorld, btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) + { + std::unique_lock lock(contactMutex); + collisionWorld->contactTest(colObj, resultCallback); + } + + void ContactTestWrapper::contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback) + { + std::unique_lock lock(contactMutex); + collisionWorld->contactPairTest(colObjA, colObjB, resultCallback); + } + +} diff --git a/apps/openmw/mwphysics/contacttestwrapper.h b/apps/openmw/mwphysics/contacttestwrapper.h new file mode 100644 index 000000000..b3b6edc59 --- /dev/null +++ b/apps/openmw/mwphysics/contacttestwrapper.h @@ -0,0 +1,14 @@ +#ifndef OPENMW_MWPHYSICS_CONTACTTESTWRAPPER_H +#define OPENMW_MWPHYSICS_CONTACTTESTWRAPPER_H + +#include + +namespace MWPhysics +{ + struct ContactTestWrapper + { + static void contactTest(btCollisionWorld* collisionWorld, btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback); + static void contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback); + }; +} +#endif diff --git a/apps/openmw/mwphysics/movementsolver.cpp b/apps/openmw/mwphysics/movementsolver.cpp index 3c78104dc..9d2957409 100644 --- a/apps/openmw/mwphysics/movementsolver.cpp +++ b/apps/openmw/mwphysics/movementsolver.cpp @@ -17,10 +17,13 @@ #include "actor.hpp" #include "collisiontype.hpp" #include "constants.hpp" +#include "contacttestwrapper.h" #include "physicssystem.hpp" #include "stepper.hpp" #include "trace.h" +#include + namespace MWPhysics { static bool isActor(const btCollisionObject *obj) @@ -29,12 +32,50 @@ namespace MWPhysics return obj->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor; } - template - static bool isWalkableSlope(const Vec3 &normal) + class ContactCollectionCallback : public btCollisionWorld::ContactResultCallback { - static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope)); - return (normal.z() > sMaxSlopeCos); - } + public: + ContactCollectionCallback(const btCollisionObject * me, osg::Vec3f velocity) : mMe(me) + { + m_collisionFilterGroup = me->getBroadphaseHandle()->m_collisionFilterGroup; + m_collisionFilterMask = me->getBroadphaseHandle()->m_collisionFilterMask; + mVelocity = Misc::Convert::toBullet(velocity); + } + btScalar addSingleResult(btManifoldPoint & contact, const btCollisionObjectWrapper * colObj0Wrap, int partId0, int index0, const btCollisionObjectWrapper * colObj1Wrap, int partId1, int index1) override + { + if (isActor(colObj0Wrap->getCollisionObject()) && isActor(colObj1Wrap->getCollisionObject())) + return 0.0; + // ignore overlap if we're moving in the same direction as it would push us out (don't change this to >=, that would break detection when not moving) + if (contact.m_normalWorldOnB.dot(mVelocity) > 0.0) + return 0.0; + auto delta = contact.m_normalWorldOnB * -contact.m_distance1; + mContactSum += delta; + mMaxX = std::max(std::abs(delta.x()), mMaxX); + mMaxY = std::max(std::abs(delta.y()), mMaxY); + mMaxZ = std::max(std::abs(delta.z()), mMaxZ); + if (contact.m_distance1 < mDistance) + { + mDistance = contact.m_distance1; + mNormal = contact.m_normalWorldOnB; + mDelta = delta; + return mDistance; + } + else + { + return 0.0; + } + } + btScalar mMaxX = 0.0; + btScalar mMaxY = 0.0; + btScalar mMaxZ = 0.0; + btVector3 mContactSum{0.0, 0.0, 0.0}; + btVector3 mNormal{0.0, 0.0, 0.0}; // points towards "me" + btVector3 mDelta{0.0, 0.0, 0.0}; // points towards "me" + btScalar mDistance = 0.0; // negative or zero + protected: + btVector3 mVelocity; + const btCollisionObject * mMe; + }; osg::Vec3f MovementSolver::traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight) { @@ -78,12 +119,14 @@ namespace MWPhysics WorldFrameData& worldData) { auto* physicActor = actor.mActorRaw; - auto ptr = actor.mPtr; const ESM::Position& refpos = actor.mRefpos; // Early-out for totally static creatures // (Not sure if gravity should still apply?) - if (!ptr.getClass().isMobile(ptr)) - return; + { + const auto ptr = physicActor->getPtr(); + if (!ptr.getClass().isMobile(ptr)) + return; + } // Reset per-frame data physicActor->setWalkingOnWater(false); @@ -97,13 +140,13 @@ namespace MWPhysics } const btCollisionObject *colobj = physicActor->getCollisionObject(); - osg::Vec3f halfExtents = physicActor->getHalfExtents(); - // NOTE: here we don't account for the collision box translation (i.e. physicActor->getPosition() - refpos.pos). - // That means the collision shape used for moving this actor is in a different spot than the collision shape - // other actors are using to collide against this actor. - // While this is strictly speaking wrong, it's needed for MW compatibility. - actor.mPosition.z() += halfExtents.z(); + // Adjust for collision mesh offset relative to actor's "location" + // (doTrace doesn't take local/interior collision shape translation into account, so we have to do it on our own) + // for compatibility with vanilla assets, we have to derive this from the vertical half extent instead of from internal hull translation + // if not for this hack, the "correct" collision hull position would be physicActor->getScaledMeshTranslation() + osg::Vec3f halfExtents = physicActor->getHalfExtents(); + actor.mPosition.z() += halfExtents.z(); // vanilla-accurate static const float fSwimHeightScale = MWBase::Environment::get().getWorld()->getStore().get().find("fSwimHeightScale")->mValue.getFloat(); float swimlevel = actor.mWaterlevel + halfExtents.z() - (physicActor->getRenderingHalfExtents().z() * 2 * fSwimHeightScale); @@ -128,8 +171,9 @@ namespace MWPhysics velocity = velocity + inertia; } - // dead actors underwater will float to the surface, if the CharacterController tells us to do so - if (actor.mMovement.z() > 0 && actor.mIsDead && actor.mPosition.z() < swimlevel) + // Dead and paralyzed actors underwater will float to the surface, + // if the CharacterController tells us to do so + if (actor.mMovement.z() > 0 && actor.mFloatToSurface && actor.mPosition.z() < swimlevel) velocity = osg::Vec3f(0,0,1) * 25; if (actor.mWantJump) @@ -153,6 +197,13 @@ namespace MWPhysics * The initial velocity was set earlier (see above). */ float remainingTime = time; + bool seenGround = physicActor->getOnGround() && !physicActor->getOnSlope() && !actor.mFlying; + + int numTimesSlid = 0; + osg::Vec3f lastSlideNormal(0,0,1); + osg::Vec3f lastSlideNormalFallback(0,0,1); + bool forceGroundTest = false; + for (int iterations = 0; iterations < sMaxIterations && remainingTime > 0.01f; ++iterations) { osg::Vec3f nextpos = newPosition + velocity * remainingTime; @@ -161,7 +212,7 @@ namespace MWPhysics if(!actor.mFlying && nextpos.z() > swimlevel && newPosition.z() < swimlevel) { const osg::Vec3f down(0,0,-1); - velocity = slide(velocity, down); + velocity = reject(velocity, down); // NOTE: remainingTime is unchanged before the loop continues continue; // velocity updated, calculate nextpos again } @@ -190,91 +241,158 @@ namespace MWPhysics break; } - // We are touching something. - if (tracer.mFraction < 1E-9f) - { - // Try to separate by backing off slighly to unstuck the solver - osg::Vec3f backOff = (newPosition - tracer.mHitPoint) * 1E-2f; - newPosition += backOff; - } + if (isWalkableSlope(tracer.mPlaneNormal) && !actor.mFlying && newPosition.z() >= swimlevel) + seenGround = true; // We hit something. Check if we can step up. float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + halfExtents.z(); osg::Vec3f oldPosition = newPosition; - bool result = false; + bool usedStepLogic = false; if (hitHeight < sStepSizeUp && !isActor(tracer.mHitObject)) { // Try to step up onto it. - // NOTE: stepMove does not allow stepping over, modifies newPosition if successful - result = stepper.step(newPosition, velocity*remainingTime, remainingTime); + // NOTE: this modifies newPosition and velocity on its own if successful + usedStepLogic = stepper.step(newPosition, velocity, remainingTime, seenGround, iterations == 0); } - if (result) + if (usedStepLogic) { // don't let pure water creatures move out of water after stepMove + const auto ptr = physicActor->getPtr(); if (ptr.getClass().isPureWaterCreature(ptr) && newPosition.z() + halfExtents.z() > actor.mWaterlevel) newPosition = oldPosition; + else if(!actor.mFlying && actor.mPosition.z() >= swimlevel) + forceGroundTest = true; } else { - // Can't move this way, try to find another spot along the plane - osg::Vec3f newVelocity = slide(velocity, tracer.mPlaneNormal); + // Can't step up, so slide against what we ran into + remainingTime *= (1.0f-tracer.mFraction); - // Do not allow sliding upward if there is gravity. - // Stepping will have taken care of that. - if(!(newPosition.z() < swimlevel || actor.mFlying)) - newVelocity.z() = std::min(newVelocity.z(), 0.0f); + auto planeNormal = tracer.mPlaneNormal; - if ((newVelocity-velocity).length2() < 0.01) + // If we touched the ground this frame, and whatever we ran into is a wall of some sort, + // pretend that its collision normal is pointing horizontally + // (fixes snagging on slightly downward-facing walls, and crawling up the bases of very steep walls because of the collision margin) + if (seenGround && !isWalkableSlope(planeNormal) && planeNormal.z() != 0) + { + planeNormal.z() = 0; + planeNormal.normalize(); + } + + // Move up to what we ran into (with a bit of a collision margin) + if ((newPosition-tracer.mEndPos).length2() > sCollisionMargin*sCollisionMargin) + { + auto direction = velocity; + direction.normalize(); + newPosition = tracer.mEndPos; + newPosition -= direction*sCollisionMargin; + } + + osg::Vec3f newVelocity = (velocity * planeNormal <= 0.0) ? reject(velocity, planeNormal) : velocity; + bool usedSeamLogic = false; + + // check for the current and previous collision planes forming an acute angle; slide along the seam if they do + if(numTimesSlid > 0) + { + auto dotA = lastSlideNormal * planeNormal; + auto dotB = lastSlideNormalFallback * planeNormal; + if(numTimesSlid <= 1) // ignore fallback normal if this is only the first or second slide + dotB = 1.0; + if(dotA <= 0.0 || dotB <= 0.0) + { + osg::Vec3f bestNormal = lastSlideNormal; + // use previous-to-previous collision plane if it's acute with current plane but actual previous plane isn't + if(dotB < dotA) + { + bestNormal = lastSlideNormalFallback; + lastSlideNormal = lastSlideNormalFallback; + } + + auto constraintVector = bestNormal ^ planeNormal; // cross product + if(constraintVector.length2() > 0) // only if it's not zero length + { + constraintVector.normalize(); + newVelocity = project(velocity, constraintVector); + + // version of surface rejection for acute crevices/seams + auto averageNormal = bestNormal + planeNormal; + averageNormal.normalize(); + tracer.doTrace(colobj, newPosition, newPosition + averageNormal*(sCollisionMargin*2.0), collisionWorld); + newPosition = (newPosition + tracer.mEndPos)/2.0; + + usedSeamLogic = true; + } + } + } + // otherwise just keep the normal vector rejection + + // if this isn't the first iteration, or if the first iteration is also the last iteration, + // move away from the collision plane slightly, if possible + // this reduces getting stuck in some concave geometry, like the gaps above the railings in some ald'ruhn buildings + // this is different from the normal collision margin, because the normal collision margin is along the movement path, + // but this is along the collision normal + if(!usedSeamLogic && (iterations > 0 || remainingTime < 0.01f)) + { + tracer.doTrace(colobj, newPosition, newPosition + planeNormal*(sCollisionMargin*2.0), collisionWorld); + newPosition = (newPosition + tracer.mEndPos)/2.0; + } + + // Do not allow sliding up steep slopes if there is gravity. + if (newPosition.z() >= swimlevel && !actor.mFlying && !isWalkableSlope(planeNormal)) + newVelocity.z() = std::min(newVelocity.z(), velocity.z()); + + if (newVelocity * origVelocity <= 0.0f) break; - if ((newVelocity * origVelocity) <= 0.f) - break; // ^ dot product + numTimesSlid += 1; + lastSlideNormalFallback = lastSlideNormal; + lastSlideNormal = planeNormal; velocity = newVelocity; } } bool isOnGround = false; bool isOnSlope = false; - if (!(inertia.z() > 0.f) && !(newPosition.z() < swimlevel)) + if (forceGroundTest || (inertia.z() <= 0.f && newPosition.z() >= swimlevel)) { osg::Vec3f from = newPosition; - osg::Vec3f to = newPosition - (physicActor->getOnGround() ? osg::Vec3f(0,0,sStepSizeDown + 2*sGroundOffset) : osg::Vec3f(0,0,2*sGroundOffset)); + auto dropDistance = 2*sGroundOffset + (physicActor->getOnGround() ? sStepSizeDown : 0); + osg::Vec3f to = newPosition - osg::Vec3f(0,0,dropDistance); tracer.doTrace(colobj, from, to, collisionWorld); - if(tracer.mFraction < 1.0f && !isActor(tracer.mHitObject)) + if(tracer.mFraction < 1.0f) { - const btCollisionObject* standingOn = tracer.mHitObject; - PtrHolder* ptrHolder = static_cast(standingOn->getUserPointer()); - if (ptrHolder) - actor.mStandingOn = ptrHolder->getPtr(); - - if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water) - physicActor->setWalkingOnWater(true); - if (!actor.mFlying) - newPosition.z() = tracer.mEndPos.z() + sGroundOffset; - - isOnGround = true; - - isOnSlope = !isWalkableSlope(tracer.mPlaneNormal); - } - else - { - // standing on actors is not allowed (see above). - // in addition to that, apply a sliding effect away from the center of the actor, - // so that we do not stay suspended in air indefinitely. - if (tracer.mFraction < 1.0f && isActor(tracer.mHitObject)) + if (!isActor(tracer.mHitObject)) { - if (osg::Vec3f(velocity.x(), velocity.y(), 0).length2() < 100.f*100.f) + isOnGround = true; + isOnSlope = !isWalkableSlope(tracer.mPlaneNormal); + + const btCollisionObject* standingOn = tracer.mHitObject; + PtrHolder* ptrHolder = static_cast(standingOn->getUserPointer()); + if (ptrHolder) + actor.mStandingOn = ptrHolder->getPtr(); + + if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water) + physicActor->setWalkingOnWater(true); + if (!actor.mFlying && !isOnSlope) { - btVector3 aabbMin, aabbMax; - tracer.mHitObject->getCollisionShape()->getAabb(tracer.mHitObject->getWorldTransform(), aabbMin, aabbMax); - btVector3 center = (aabbMin + aabbMax) / 2.f; - inertia = osg::Vec3f(actor.mPosition.x() - center.x(), actor.mPosition.y() - center.y(), 0); - inertia.normalize(); - inertia *= 100; + if (tracer.mFraction*dropDistance > sGroundOffset) + newPosition.z() = tracer.mEndPos.z() + sGroundOffset; + else + { + newPosition.z() = tracer.mEndPos.z(); + tracer.doTrace(colobj, newPosition, newPosition + osg::Vec3f(0, 0, 2*sGroundOffset), collisionWorld); + newPosition = (newPosition+tracer.mEndPos)/2.0; + } } } + else + { + // Vanilla allows actors to float on top of other actors. Do not push them off. + if (!actor.mFlying && isWalkableSlope(tracer.mPlaneNormal) && tracer.mEndPos.z()+sGroundOffset <= newPosition.z()) + newPosition.z() = tracer.mEndPos.z() + sGroundOffset; - isOnGround = false; + isOnGround = false; + } } } @@ -294,7 +412,98 @@ namespace MWPhysics physicActor->setOnGround(isOnGround); physicActor->setOnSlope(isOnSlope); - newPosition.z() -= halfExtents.z(); // remove what was added at the beginning actor.mPosition = newPosition; + // remove what was added earlier in compensating for doTrace not taking interior transformation into account + actor.mPosition.z() -= halfExtents.z(); // vanilla-accurate + } + + btVector3 addMarginToDelta(btVector3 delta) + { + if(delta.length2() == 0.0) + return delta; + return delta + delta.normalized() * sCollisionMargin; + } + + void MovementSolver::unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld) + { + const auto& ptr = actor.mActorRaw->getPtr(); + if (!ptr.getClass().isMobile(ptr)) + return; + + auto* physicActor = actor.mActorRaw; + if(!physicActor->getCollisionMode()) // noclipping/tcl + return; + + auto* collisionObject = physicActor->getCollisionObject(); + auto tempPosition = actor.mPosition; + + // use vanilla-accurate collision hull position hack (do same hitbox offset hack as movement solver) + // if vanilla compatibility didn't matter, the "correct" collision hull position would be physicActor->getScaledMeshTranslation() + const auto verticalHalfExtent = osg::Vec3f(0.0, 0.0, physicActor->getHalfExtents().z()); + + // use a 3d approximation of the movement vector to better judge player intent + const ESM::Position& refpos = ptr.getRefData().getPosition(); + auto velocity = (osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement; + // try to pop outside of the world before doing anything else if we're inside of it + if (!physicActor->getOnGround() || physicActor->getOnSlope()) + velocity += physicActor->getInertialForce(); + + // because of the internal collision box offset hack, and the fact that we're moving the collision box manually, + // we need to replicate part of the collision box's transform process from scratch + osg::Vec3f refPosition = tempPosition + verticalHalfExtent; + osg::Vec3f goodPosition = refPosition; + const btTransform oldTransform = collisionObject->getWorldTransform(); + btTransform newTransform = oldTransform; + + auto gatherContacts = [&](btVector3 newOffset) -> ContactCollectionCallback + { + goodPosition = refPosition + Misc::Convert::toOsg(addMarginToDelta(newOffset)); + newTransform.setOrigin(Misc::Convert::toBullet(goodPosition)); + collisionObject->setWorldTransform(newTransform); + + ContactCollectionCallback callback{collisionObject, velocity}; + ContactTestWrapper::contactTest(const_cast(collisionWorld), collisionObject, callback); + return callback; + }; + + // check whether we're inside the world with our collision box with manually-derived offset + auto contactCallback = gatherContacts({0.0, 0.0, 0.0}); + if(contactCallback.mDistance < -sAllowedPenetration) + { + // we are; try moving it out of the world + auto positionDelta = contactCallback.mContactSum; + // limit rejection delta to the largest known individual rejections + if(std::abs(positionDelta.x()) > contactCallback.mMaxX) + positionDelta *= contactCallback.mMaxX / std::abs(positionDelta.x()); + if(std::abs(positionDelta.y()) > contactCallback.mMaxY) + positionDelta *= contactCallback.mMaxY / std::abs(positionDelta.y()); + if(std::abs(positionDelta.z()) > contactCallback.mMaxZ) + positionDelta *= contactCallback.mMaxZ / std::abs(positionDelta.z()); + + auto contactCallback2 = gatherContacts(positionDelta); + // successfully moved further out from contact (does not have to be in open space, just less inside of things) + if(contactCallback2.mDistance > contactCallback.mDistance) + tempPosition = goodPosition - verticalHalfExtent; + // try again but only upwards (fixes some bad coc floors) + else + { + // upwards-only offset + auto contactCallback3 = gatherContacts({0.0, 0.0, std::abs(positionDelta.z())}); + // success + if(contactCallback3.mDistance > contactCallback.mDistance) + tempPosition = goodPosition - verticalHalfExtent; + else + // try again but fixed distance up + { + auto contactCallback4 = gatherContacts({0.0, 0.0, 10.0}); + // success + if(contactCallback4.mDistance > contactCallback.mDistance) + tempPosition = goodPosition - verticalHalfExtent; + } + } + } + + collisionObject->setWorldTransform(oldTransform); + actor.mPosition = tempPosition; } } diff --git a/apps/openmw/mwphysics/movementsolver.hpp b/apps/openmw/mwphysics/movementsolver.hpp index 75fba1cf0..76141ec0e 100644 --- a/apps/openmw/mwphysics/movementsolver.hpp +++ b/apps/openmw/mwphysics/movementsolver.hpp @@ -5,6 +5,9 @@ #include +#include "constants.hpp" +#include "../mwworld/ptr.hpp" + class btCollisionWorld; namespace MWWorld @@ -14,29 +17,35 @@ namespace MWWorld namespace MWPhysics { + /// Vector projection + static inline osg::Vec3f project(const osg::Vec3f& u, const osg::Vec3f &v) + { + return v * (u * v); + } + + /// Vector rejection + static inline osg::Vec3f reject(const osg::Vec3f& direction, const osg::Vec3f &planeNormal) + { + return direction - project(direction, planeNormal); + } + + template + static bool isWalkableSlope(const Vec3 &normal) + { + static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope)); + return (normal.z() > sMaxSlopeCos); + } + class Actor; struct ActorFrameData; struct WorldFrameData; class MovementSolver { - private: - ///Project a vector u on another vector v - static inline osg::Vec3f project(const osg::Vec3f& u, const osg::Vec3f &v) - { - return v * (u * v); - // ^ dot product - } - - ///Helper for computing the character sliding - static inline osg::Vec3f slide(const osg::Vec3f& direction, const osg::Vec3f &planeNormal) - { - return direction - project(direction, planeNormal); - } - public: static osg::Vec3f traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight); static void move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, WorldFrameData& worldData); + static void unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld); }; } diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index 2d4009e7d..0d1e5962a 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -13,10 +13,12 @@ #include "../mwworld/player.hpp" #include "actor.hpp" +#include "contacttestwrapper.h" #include "movementsolver.hpp" #include "mtphysics.hpp" #include "object.hpp" #include "physicssystem.hpp" +#include "projectile.hpp" namespace { @@ -86,12 +88,13 @@ namespace void updateMechanics(MWPhysics::ActorFrameData& actorData) { + auto ptr = actorData.mActorRaw->getPtr(); if (actorData.mDidJump) - handleJump(actorData.mPtr); + handleJump(ptr); - MWMechanics::CreatureStats& stats = actorData.mPtr.getClass().getCreatureStats(actorData.mPtr); + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); if (actorData.mNeedLand) - stats.land(actorData.mPtr == MWMechanics::getPlayer() && (actorData.mFlying || actorData.mSwimming)); + stats.land(ptr == MWMechanics::getPlayer() && (actorData.mFlying || actorData.mSwimming)); else if (actorData.mFallHeight < 0) stats.addToFallHeight(-actorData.mFallHeight); } @@ -99,15 +102,6 @@ namespace osg::Vec3f interpolateMovements(MWPhysics::ActorFrameData& actorData, float timeAccum, float physicsDt) { const float interpolationFactor = timeAccum / physicsDt; - - // account for force change of actor's position in the main thread - const auto correction = actorData.mActorRaw->getWorldPosition() - actorData.mOrigin; - if (correction.length() != 0) - { - actorData.mActorRaw->adjustPosition(correction); - actorData.mPosition = actorData.mActorRaw->getPosition(); - } - return actorData.mPosition * interpolationFactor + actorData.mActorRaw->getPreviousPosition() * (1.f - interpolationFactor); } @@ -174,7 +168,14 @@ namespace MWPhysics mPreStepBarrier = std::make_unique(mNumThreads, [&]() { + if (mDeferAabbUpdate) updateAabbs(); + for (auto& data : mActorsFrameData) + if (data.mActor.lock()) + { + std::unique_lock lock(mCollisionWorldMutex); + MovementSolver::unstuck(data, mCollisionWorld.get()); + } }); mPostStepBarrier = std::make_unique(mNumThreads, [&]() @@ -212,45 +213,43 @@ namespace MWPhysics thread.join(); } - const PtrPositionList& PhysicsTaskScheduler::moveActors(int numSteps, float timeAccum, std::vector&& actorsData, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) + const std::vector& PhysicsTaskScheduler::moveActors(int numSteps, float timeAccum, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { // This function run in the main thread. // While the mSimulationMutex is held, background physics threads can't run. std::unique_lock lock(mSimulationMutex); - for (auto& data : actorsData) - data.updatePosition(); + mMovedActors.clear(); // start by finishing previous background computation if (mNumThreads != 0) { for (auto& data : mActorsFrameData) { - // Ignore actors that were deleted while the background thread was running - if (!data.mActor.lock()) - continue; + const auto actorActive = [&data](const auto& newFrameData) -> bool + { + const auto actor = data.mActor.lock(); + return actor && actor->getPtr() == newFrameData.mActorRaw->getPtr(); + }; + // Only return actors that are still part of the scene + if (std::any_of(actorsData.begin(), actorsData.end(), actorActive)) + { + updateMechanics(data); - updateMechanics(data); - if (mAdvanceSimulation) - data.mActorRaw->setStandingOnPtr(data.mStandingOn); - - if (mMovementResults.find(data.mPtr) != mMovementResults.end()) - data.mActorRaw->setSimulationPosition(mMovementResults[data.mPtr]); + // these variables are accessed directly from the main thread, update them here to prevent accessing "too new" values + if (mAdvanceSimulation) + data.mActorRaw->setStandingOnPtr(data.mStandingOn); + data.mActorRaw->setSimulationPosition(interpolateMovements(data, mTimeAccum, mPhysicsDt)); + mMovedActors.emplace_back(data.mActorRaw->getPtr()); + } } - - if (mFrameNumber == frameNumber - 1) - { - stats.setAttribute(mFrameNumber, "physicsworker_time_begin", mTimer->delta_s(mFrameStart, mTimeBegin)); - stats.setAttribute(mFrameNumber, "physicsworker_time_taken", mTimer->delta_s(mTimeBegin, mTimeEnd)); - stats.setAttribute(mFrameNumber, "physicsworker_time_end", mTimer->delta_s(mFrameStart, mTimeEnd)); - } - mFrameStart = frameStart; - mTimeBegin = mTimer->tick(); - mFrameNumber = frameNumber; + updateStats(frameStart, frameNumber, stats); } // init + for (auto& data : actorsData) + data.updatePosition(); mRemainingSteps = numSteps; mTimeAccum = timeAccum; mActorsFrameData = std::move(actorsData); @@ -263,52 +262,30 @@ namespace MWPhysics if (mAdvanceSimulation) mWorldFrameData = std::make_unique(); - // we are asked to skip the simulation (load a savegame for instance) - // just return the actors' reference position without applying the movements - if (skipSimulation) - { - mMovementResults.clear(); - for (const auto& m : mActorsFrameData) - { - m.mActorRaw->setStandingOnPtr(nullptr); - m.mActorRaw->resetPosition(); - mMovementResults[m.mPtr] = m.mActorRaw->getWorldPosition(); - } - return mMovementResults; - } - if (mNumThreads == 0) { - mMovementResults.clear(); syncComputation(); - - for (auto& data : mActorsFrameData) - { - if (mAdvanceSimulation) - data.mActorRaw->setStandingOnPtr(data.mStandingOn); - if (mMovementResults.find(data.mPtr) != mMovementResults.end()) - data.mActorRaw->setSimulationPosition(mMovementResults[data.mPtr]); - } - return mMovementResults; + return mMovedActors; } - // Remove actors that were deleted while the background thread was running - for (auto& data : mActorsFrameData) - { - if (!data.mActor.lock()) - mMovementResults.erase(data.mPtr); - } - std::swap(mMovementResults, mPreviousMovementResults); - - // mMovementResults is shared between all workers instance - // pre-allocate all nodes so that we don't need synchronization - mMovementResults.clear(); - for (const auto& m : mActorsFrameData) - mMovementResults[m.mPtr] = m.mPosition; - lock.unlock(); mHasJob.notify_all(); - return mPreviousMovementResults; + return mMovedActors; + } + + const std::vector& PhysicsTaskScheduler::resetSimulation(const ActorMap& actors) + { + std::unique_lock lock(mSimulationMutex); + mMovedActors.clear(); + mActorsFrameData.clear(); + for (const auto& [_, actor] : actors) + { + actor->updatePosition(); + actor->setSimulationPosition(actor->getWorldPosition()); // updatePosition skip next simulation, now we need to "consume" it + actor->updateCollisionObjectPosition(); + mMovedActors.emplace_back(actor->getPtr()); + } + return mMovedActors; } void PhysicsTaskScheduler::rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, btCollisionWorld::RayResultCallback& resultCallback) const @@ -326,7 +303,7 @@ namespace MWPhysics void PhysicsTaskScheduler::contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) { std::shared_lock lock(mCollisionWorldMutex); - mCollisionWorld->contactTest(colObj, resultCallback); + ContactTestWrapper::contactTest(mCollisionWorld.get(), colObj, resultCallback); } std::optional PhysicsTaskScheduler::getHitPoint(const btTransform& from, btCollisionObject* target) @@ -376,18 +353,18 @@ namespace MWPhysics mCollisionWorld->removeCollisionObject(collisionObject); } - void PhysicsTaskScheduler::updateSingleAabb(std::weak_ptr ptr) + void PhysicsTaskScheduler::updateSingleAabb(std::weak_ptr ptr, bool immediate) { - if (mDeferAabbUpdate) - { - std::unique_lock lock(mUpdateAabbMutex); - mUpdateAabb.insert(std::move(ptr)); - } - else + if (!mDeferAabbUpdate || immediate) { std::unique_lock lock(mCollisionWorldMutex); updatePtrAabb(ptr); } + else + { + std::unique_lock lock(mUpdateAabbMutex); + mUpdateAabb.insert(std::move(ptr)); + } } bool PhysicsTaskScheduler::getLineOfSight(const std::weak_ptr& actor1, const std::weak_ptr& actor2) @@ -453,6 +430,11 @@ namespace MWPhysics object->commitPositionChange(); mCollisionWorld->updateSingleAabb(object->getCollisionObject()); } + else if (const auto projectile = std::dynamic_pointer_cast(p)) + { + projectile->commitPositionChange(); + mCollisionWorld->updateSingleAabb(projectile->getCollisionObject()); + } }; } @@ -464,8 +446,7 @@ namespace MWPhysics if (!mNewFrame) mHasJob.wait(lock, [&]() { return mQuit || mNewFrame; }); - if (mDeferAabbUpdate) - mPreStepBarrier->wait(); + mPreStepBarrier->wait(); int job = 0; while (mRemainingSteps && (job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) @@ -485,7 +466,6 @@ namespace MWPhysics { auto& actorData = mActorsFrameData[job]; handleFall(actorData, mAdvanceSimulation); - mMovementResults[actorData.mPtr] = interpolateMovements(actorData, mTimeAccum, mPhysicsDt); } } @@ -503,9 +483,7 @@ namespace MWPhysics { if(const auto actor = actorData.mActor.lock()) { - bool positionChanged = actorData.mPosition != actorData.mActorRaw->getPosition(); - actorData.mActorRaw->setPosition(actorData.mPosition); - if (positionChanged) + if (actor->setPosition(actorData.mPosition)) { actor->updateCollisionObjectPosition(); mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); @@ -534,7 +512,10 @@ namespace MWPhysics while (mRemainingSteps--) { for (auto& actorData : mActorsFrameData) + { + MovementSolver::unstuck(actorData, mCollisionWorld.get()); MovementSolver::move(actorData, mPhysicsDt, mCollisionWorld.get(), *mWorldFrameData); + } updateActorsPositions(); } @@ -542,8 +523,24 @@ namespace MWPhysics for (auto& actorData : mActorsFrameData) { handleFall(actorData, mAdvanceSimulation); - mMovementResults[actorData.mPtr] = interpolateMovements(actorData, mTimeAccum, mPhysicsDt); + actorData.mActorRaw->setSimulationPosition(interpolateMovements(actorData, mTimeAccum, mPhysicsDt)); updateMechanics(actorData); + mMovedActors.emplace_back(actorData.mActorRaw->getPtr()); + if (mAdvanceSimulation) + actorData.mActorRaw->setStandingOnPtr(actorData.mStandingOn); } } + + void PhysicsTaskScheduler::updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) + { + if (mFrameNumber == frameNumber - 1) + { + stats.setAttribute(mFrameNumber, "physicsworker_time_begin", mTimer->delta_s(mFrameStart, mTimeBegin)); + stats.setAttribute(mFrameNumber, "physicsworker_time_taken", mTimer->delta_s(mTimeBegin, mTimeEnd)); + stats.setAttribute(mFrameNumber, "physicsworker_time_end", mTimer->delta_s(mFrameStart, mTimeEnd)); + } + mFrameStart = frameStart; + mTimeBegin = mTimer->tick(); + mFrameNumber = frameNumber; + } } diff --git a/apps/openmw/mwphysics/mtphysics.hpp b/apps/openmw/mwphysics/mtphysics.hpp index 3ffc851e5..f10b39f0c 100644 --- a/apps/openmw/mwphysics/mtphysics.hpp +++ b/apps/openmw/mwphysics/mtphysics.hpp @@ -32,7 +32,9 @@ namespace MWPhysics /// @param timeAccum accumulated time from previous run to interpolate movements /// @param actorsData per actor data needed to compute new positions /// @return new position of each actor - const PtrPositionList& moveActors(int numSteps, float timeAccum, std::vector&& actorsData, bool skip, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + const std::vector& moveActors(int numSteps, float timeAccum, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + + const std::vector& resetSimulation(const ActorMap& actors); // Thread safe wrappers void rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, btCollisionWorld::RayResultCallback& resultCallback) const; @@ -44,7 +46,7 @@ namespace MWPhysics void setCollisionFilterMask(btCollisionObject* collisionObject, int collisionFilterMask); void addCollisionObject(btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask); void removeCollisionObject(btCollisionObject* collisionObject); - void updateSingleAabb(std::weak_ptr ptr); + void updateSingleAabb(std::weak_ptr ptr, bool immediate=false); bool getLineOfSight(const std::weak_ptr& actor1, const std::weak_ptr& actor2); private: @@ -55,11 +57,11 @@ namespace MWPhysics void refreshLOSCache(); void updateAabbs(); void updatePtrAabb(const std::weak_ptr& ptr); + void updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); std::unique_ptr mWorldFrameData; std::vector mActorsFrameData; - PtrPositionList mMovementResults; - PtrPositionList mPreviousMovementResults; + std::vector mMovedActors; /* Start of tes3mp change (major) diff --git a/apps/openmw/mwphysics/object.cpp b/apps/openmw/mwphysics/object.cpp index c822bbcbe..910f7bf15 100644 --- a/apps/openmw/mwphysics/object.cpp +++ b/apps/openmw/mwphysics/object.cpp @@ -24,7 +24,7 @@ namespace MWPhysics mCollisionObject.reset(new btCollisionObject); mCollisionObject->setCollisionShape(shapeInstance->getCollisionShape()); - mCollisionObject->setUserPointer(static_cast(this)); + mCollisionObject->setUserPointer(this); setScale(ptr.getCellRef().getScale()); setRotation(Misc::Convert::toBullet(ptr.getRefData().getBaseNode()->getAttitude())); diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 2bf94060f..f347682b2 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -46,6 +46,8 @@ #include "collisiontype.hpp" #include "actor.hpp" + +#include "projectile.hpp" #include "trace.h" #include "object.hpp" #include "heightfield.hpp" @@ -64,6 +66,7 @@ namespace MWPhysics , mResourceSystem(resourceSystem) , mDebugDrawEnabled(false) , mTimeAccum(0.0f) + , mProjectileId(0) , mWaterHeight(0) , mWaterEnabled(false) , mParentNode(parentNode) @@ -112,7 +115,7 @@ namespace MWPhysics mObjects.clear(); mActors.clear(); - + mProjectiles.clear(); } void PhysicsSystem::setUnrefQueue(SceneUtil::UnrefQueue *unrefQueue) @@ -272,7 +275,7 @@ namespace MWPhysics return 0.f; } - RayCastingResult PhysicsSystem::castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore, std::vector targets, int mask, int group) const + RayCastingResult PhysicsSystem::castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore, std::vector targets, int mask, int group, int projId) const { if (from == to) { @@ -309,7 +312,7 @@ namespace MWPhysics } } - ClosestNotMeRayResultCallback resultCallback(me, targetCollisionObjects, btFrom, btTo); + ClosestNotMeRayResultCallback resultCallback(me, targetCollisionObjects, btFrom, btTo, getProjectile(projId)); resultCallback.m_collisionFilterGroup = group; resultCallback.m_collisionFilterMask = mask; @@ -458,7 +461,6 @@ namespace MWPhysics ActorMap::iterator found = mActors.find(ptr); if (found == mActors.end()) return ptr.getRefData().getPosition().asVec3(); - found->second->resetPosition(); return MovementSolver::traceDown(ptr, position, found->second.get(), mCollisionWorld.get(), maxHeight); } @@ -526,6 +528,13 @@ namespace MWPhysics } } + void PhysicsSystem::removeProjectile(const int projectileId) + { + ProjectileMap::iterator foundProjectile = mProjectiles.find(projectileId); + if (foundProjectile != mProjectiles.end()) + mProjectiles.erase(foundProjectile); + } + void PhysicsSystem::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated) { ObjectMap::iterator found = mObjects.find(old); @@ -551,6 +560,13 @@ namespace MWPhysics if (actor->getStandingOnPtr() == old) actor->setStandingOnPtr(updated); } + + for (auto& [_, projectile] : mProjectiles) + { + if (projectile->getCaster() == old) + projectile->setCaster(updated); + } + } Actor *PhysicsSystem::getActor(const MWWorld::Ptr &ptr) @@ -577,6 +593,14 @@ namespace MWPhysics return nullptr; } + Projectile* PhysicsSystem::getProjectile(int projectileId) const + { + ProjectileMap::const_iterator found = mProjectiles.find(projectileId); + if (found != mProjectiles.end()) + return found->second.get(); + return nullptr; + } + void PhysicsSystem::updateScale(const MWWorld::Ptr &ptr) { ObjectMap::iterator found = mObjects.find(ptr); @@ -596,6 +620,17 @@ namespace MWPhysics } } + void PhysicsSystem::updateProjectile(const int projectileId, const osg::Vec3f &position) + { + ProjectileMap::iterator foundProjectile = mProjectiles.find(projectileId); + if (foundProjectile != mProjectiles.end()) + { + foundProjectile->second->setPosition(position); + mTaskScheduler->updateSingleAabb(foundProjectile->second); + return; + } + } + void PhysicsSystem::updateRotation(const MWWorld::Ptr &ptr) { ObjectMap::iterator found = mObjects.find(ptr); @@ -630,7 +665,7 @@ namespace MWPhysics if (foundActor != mActors.end()) { foundActor->second->updatePosition(); - mTaskScheduler->updateSingleAabb(foundActor->second); + mTaskScheduler->updateSingleAabb(foundActor->second, true); return; } } @@ -640,7 +675,7 @@ namespace MWPhysics osg::ref_ptr shape = mShapeManager->getShape(mesh); // Try to get shape from basic model as fallback for creatures - if (!ptr.getClass().isNpc() && shape && shape->mCollisionBoxHalfExtents.length2() == 0) + if (!ptr.getClass().isNpc() && shape && shape->mCollisionBox.extents.length2() == 0) { const std::string fallbackModel = ptr.getClass().getModel(ptr); if (fallbackModel != mesh) @@ -656,6 +691,15 @@ namespace MWPhysics mActors.emplace(ptr, std::move(actor)); } + int PhysicsSystem::addProjectile (const MWWorld::Ptr& caster, const osg::Vec3f& position) + { + mProjectileId++; + auto projectile = std::make_shared(mProjectileId, caster, position, mTaskScheduler.get(), this); + mProjectiles.emplace(mProjectileId, std::move(projectile)); + + return mProjectileId; + } + bool PhysicsSystem::toggleCollisionMode() { ActorMap::iterator found = mActors.find(MWMechanics::getPlayer()); @@ -690,7 +734,7 @@ namespace MWPhysics mMovementQueue.clear(); } - const PtrPositionList& PhysicsSystem::applyQueuedMovement(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) + const std::vector& PhysicsSystem::applyQueuedMovement(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { mTimeAccum += dt; @@ -700,7 +744,10 @@ namespace MWPhysics mTimeAccum -= numSteps * mPhysicsDt; - return mTaskScheduler->moveActors(numSteps, mTimeAccum, prepareFrameData(numSteps), skipSimulation, frameStart, frameNumber, stats); + if (skipSimulation) + return mTaskScheduler->resetSimulation(mActors); + + return mTaskScheduler->moveActors(numSteps, mTimeAccum, prepareFrameData(numSteps), frameStart, frameNumber, stats); } std::vector PhysicsSystem::prepareFrameData(int numSteps) @@ -746,7 +793,7 @@ namespace MWPhysics if (numSteps == 0) standingOn = physicActor->getStandingOnPtr(); - actorsFrameData.emplace_back(std::move(physicActor), character, standingOn, moveToWaterSurface, movement, slowFall, waterlevel); + actorsFrameData.emplace_back(std::move(physicActor), standingOn, moveToWaterSurface, movement, slowFall, waterlevel); } mMovementQueue.clear(); return actorsFrameData; @@ -888,33 +935,34 @@ namespace MWPhysics mDebugDrawer->addCollision(position, normal); } - ActorFrameData::ActorFrameData(const std::shared_ptr& actor, const MWWorld::Ptr character, const MWWorld::Ptr standingOn, + ActorFrameData::ActorFrameData(const std::shared_ptr& actor, const MWWorld::Ptr standingOn, bool moveToWaterSurface, osg::Vec3f movement, float slowFall, float waterlevel) : mActor(actor), mActorRaw(actor.get()), mStandingOn(standingOn), mDidJump(false), mNeedLand(false), mMoveToWaterSurface(moveToWaterSurface), mWaterlevel(waterlevel), mSlowFall(slowFall), mOldHeight(0), mFallHeight(0), mMovement(movement), mPosition(), mRefpos() { const MWBase::World *world = MWBase::Environment::get().getWorld(); - mPtr = actor->getPtr(); - mFlying = world->isFlying(character); - mSwimming = world->isSwimming(character); - mWantJump = mPtr.getClass().getMovementSettings(mPtr).mPosition[2] != 0; - mIsDead = mPtr.getClass().getCreatureStats(mPtr).isDead(); + const auto ptr = actor->getPtr(); + mFlying = world->isFlying(ptr); + mSwimming = world->isSwimming(ptr); + mWantJump = ptr.getClass().getMovementSettings(ptr).mPosition[2] != 0; + auto& stats = ptr.getClass().getCreatureStats(ptr); + const bool godmode = ptr == world->getPlayerConstPtr() && world->getGodModeState(); + mFloatToSurface = stats.isDead() || (!godmode && stats.isParalyzed()); mWasOnGround = actor->getOnGround(); } void ActorFrameData::updatePosition() { - mActorRaw->updatePosition(); - mOrigin = mActorRaw->getSimulationPosition(); + mActorRaw->updateWorldPosition(); mPosition = mActorRaw->getPosition(); if (mMoveToWaterSurface) { mPosition.z() = mWaterlevel; - mActorRaw->setPosition(mPosition); + MWBase::Environment::get().getWorld()->moveObject(mActorRaw->getPtr(), mPosition.x(), mPosition.y(), mPosition.z()); } mOldHeight = mPosition.z(); - mRefpos = mPtr.getRefData().getPosition(); + mRefpos = mActorRaw->getPtr().getRefData().getPosition(); } WorldFrameData::WorldFrameData() diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 8e369a254..ead229ec4 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -50,12 +50,13 @@ class btVector3; namespace MWPhysics { - using PtrPositionList = std::map; - class HeightField; class Object; class Actor; class PhysicsTaskScheduler; + class Projectile; + + using ActorMap = std::map>; struct ContactPoint { @@ -77,18 +78,17 @@ namespace MWPhysics struct ActorFrameData { - ActorFrameData(const std::shared_ptr& actor, const MWWorld::Ptr character, const MWWorld::Ptr standingOn, bool moveToWaterSurface, osg::Vec3f movement, float slowFall, float waterlevel); + ActorFrameData(const std::shared_ptr& actor, const MWWorld::Ptr standingOn, bool moveToWaterSurface, osg::Vec3f movement, float slowFall, float waterlevel); void updatePosition(); std::weak_ptr mActor; Actor* mActorRaw; - MWWorld::Ptr mPtr; MWWorld::Ptr mStandingOn; bool mFlying; bool mSwimming; bool mWasOnGround; bool mWantJump; bool mDidJump; - bool mIsDead; + bool mFloatToSurface; bool mNeedLand; bool mMoveToWaterSurface; float mWaterlevel; @@ -96,7 +96,6 @@ namespace MWPhysics float mOldHeight; float mFallHeight; osg::Vec3f mMovement; - osg::Vec3f mOrigin; osg::Vec3f mPosition; ESM::Position mRefpos; }; @@ -125,6 +124,10 @@ namespace MWPhysics void addObject (const MWWorld::Ptr& ptr, const std::string& mesh, int collisionType = CollisionType_World); void addActor (const MWWorld::Ptr& ptr, const std::string& mesh); + int addProjectile(const MWWorld::Ptr& caster, const osg::Vec3f& position); + void updateProjectile(const int projectileId, const osg::Vec3f &position); + void removeProjectile(const int projectileId); + void updatePtr (const MWWorld::Ptr& old, const MWWorld::Ptr& updated); Actor* getActor(const MWWorld::Ptr& ptr); @@ -132,6 +135,8 @@ namespace MWPhysics const Object* getObject(const MWWorld::ConstPtr& ptr) const; + Projectile* getProjectile(int projectileId) const; + // Object or Actor void remove (const MWWorld::Ptr& ptr); @@ -139,7 +144,6 @@ namespace MWPhysics void updateRotation (const MWWorld::Ptr& ptr); void updatePosition (const MWWorld::Ptr& ptr); - void addHeightField (const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject); void removeHeightField (int x, int y); @@ -170,7 +174,7 @@ namespace MWPhysics /// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all other actors. RayCastingResult castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore = MWWorld::ConstPtr(), std::vector targets = std::vector(), - int mask = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, int group=0xff) const override; + int mask = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, int group=0xff, int projId=-1) const override; RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius) const override; @@ -202,7 +206,7 @@ namespace MWPhysics void queueObjectMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &velocity); /// Apply all queued movements, then clear the list. - const PtrPositionList& applyQueuedMovement(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + const std::vector& applyQueuedMovement(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); /// Clear the queued movements list without applying. void clearQueuedMovement(); @@ -275,9 +279,11 @@ namespace MWPhysics std::set mAnimatedObjects; // stores pointers to elements in mObjects - using ActorMap = std::map>; ActorMap mActors; + using ProjectileMap = std::map>; + ProjectileMap mProjectiles; + using HeightFieldMap = std::map, HeightField *>; HeightFieldMap mHeightFields; @@ -288,6 +294,8 @@ namespace MWPhysics float mTimeAccum; + unsigned int mProjectileId; + float mWaterHeight; bool mWaterEnabled; diff --git a/apps/openmw/mwphysics/projectile.cpp b/apps/openmw/mwphysics/projectile.cpp new file mode 100644 index 000000000..a8aaeb72a --- /dev/null +++ b/apps/openmw/mwphysics/projectile.cpp @@ -0,0 +1,131 @@ +#include + +#include +#include + +#include + +#include +#include +#include +#include + +#include "../mwworld/class.hpp" + +#include "collisiontype.hpp" +#include "mtphysics.hpp" +#include "projectile.hpp" + +namespace MWPhysics +{ +Projectile::Projectile(int projectileId, const MWWorld::Ptr& caster, const osg::Vec3f& position, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem) + : mActive(true) + , mCaster(caster) + , mPhysics(physicssystem) + , mTaskScheduler(scheduler) + , mProjectileId(projectileId) +{ + mShape.reset(new btSphereShape(1.f)); + mConvexShape = static_cast(mShape.get()); + + mCollisionObject.reset(new btCollisionObject); + mCollisionObject->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT); + mCollisionObject->setActivationState(DISABLE_DEACTIVATION); + mCollisionObject->setCollisionShape(mShape.get()); + mCollisionObject->setUserPointer(this); + + setPosition(position); + + const int collisionMask = CollisionType_World | CollisionType_HeightMap | + CollisionType_Actor | CollisionType_Door | CollisionType_Water | CollisionType_Projectile; + mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Projectile, collisionMask); + + commitPositionChange(); +} + +Projectile::~Projectile() +{ + if (mCollisionObject) + { + if (!mActive) + mPhysics->reportCollision(mHitPosition, mHitNormal); + mTaskScheduler->removeCollisionObject(mCollisionObject.get()); + } +} + +void Projectile::commitPositionChange() +{ + std::scoped_lock lock(mMutex); + if (mTransformUpdatePending) + { + mCollisionObject->setWorldTransform(mLocalTransform); + mTransformUpdatePending = false; + } +} + +void Projectile::setPosition(const osg::Vec3f &position) +{ + std::scoped_lock lock(mMutex); + mLocalTransform.setOrigin(Misc::Convert::toBullet(position)); + mTransformUpdatePending = true; +} + +void Projectile::hit(MWWorld::Ptr target, btVector3 pos, btVector3 normal) +{ + if (!mActive.load(std::memory_order_acquire)) + return; + std::scoped_lock lock(mMutex); + mHitTarget = target; + mHitPosition = pos; + mHitNormal = normal; + mActive.store(false, std::memory_order_release); +} + +void Projectile::activate() +{ + assert(!mActive); + mActive.store(true, std::memory_order_release); +} + +MWWorld::Ptr Projectile::getCaster() const +{ + std::scoped_lock lock(mMutex); + return mCaster; +} + +void Projectile::setCaster(MWWorld::Ptr caster) +{ + std::scoped_lock lock(mMutex); + mCaster = caster; +} + +void Projectile::setValidTargets(const std::vector& targets) +{ + std::scoped_lock lock(mMutex); + mValidTargets = targets; +} + +bool Projectile::isValidTarget(const MWWorld::Ptr& target) const +{ + std::scoped_lock lock(mMutex); + if (mCaster == target) + return false; + + if (!mValidTargets.empty()) + { + bool validTarget = false; + for (const auto& targetActor : mValidTargets) + { + if (targetActor == target) + { + validTarget = true; + break; + } + } + + return validTarget; + } + return true; +} + +} diff --git a/apps/openmw/mwphysics/projectile.hpp b/apps/openmw/mwphysics/projectile.hpp new file mode 100644 index 000000000..5ae963b9a --- /dev/null +++ b/apps/openmw/mwphysics/projectile.hpp @@ -0,0 +1,112 @@ +#ifndef OPENMW_MWPHYSICS_PROJECTILE_H +#define OPENMW_MWPHYSICS_PROJECTILE_H + +#include +#include +#include + +#include "components/misc/convert.hpp" + +#include "ptrholder.hpp" + +class btCollisionObject; +class btCollisionShape; +class btConvexShape; +class btVector3; + +namespace osg +{ + class Vec3f; +} + +namespace Resource +{ + class BulletShape; +} + +namespace MWPhysics +{ + class PhysicsTaskScheduler; + class PhysicsSystem; + + class Projectile final : public PtrHolder + { + public: + Projectile(const int projectileId, const MWWorld::Ptr& caster, const osg::Vec3f& position, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem); + ~Projectile() override; + + btConvexShape* getConvexShape() const { return mConvexShape; } + + void commitPositionChange(); + + void setPosition(const osg::Vec3f& position); + + btCollisionObject* getCollisionObject() const + { + return mCollisionObject.get(); + } + + int getProjectileId() const + { + return mProjectileId; + } + + bool isActive() const + { + return mActive.load(std::memory_order_acquire); + } + + MWWorld::Ptr getTarget() const + { + assert(!mActive); + return mHitTarget; + } + + MWWorld::Ptr getCaster() const; + void setCaster(MWWorld::Ptr caster); + + osg::Vec3f getHitPos() const + { + assert(!mActive); + return Misc::Convert::toOsg(mHitPosition); + } + + void hit(MWWorld::Ptr target, btVector3 pos, btVector3 normal); + void activate(); + + void setValidTargets(const std::vector& targets); + bool isValidTarget(const MWWorld::Ptr& target) const; + + private: + + std::unique_ptr mShape; + btConvexShape* mConvexShape; + + std::unique_ptr mCollisionObject; + btTransform mLocalTransform; + bool mTransformUpdatePending; + std::atomic mActive; + MWWorld::Ptr mCaster; + MWWorld::Ptr mHitTarget; + btVector3 mHitPosition; + btVector3 mHitNormal; + + std::vector mValidTargets; + + mutable std::mutex mMutex; + + osg::Vec3f mPosition; + + PhysicsSystem *mPhysics; + PhysicsTaskScheduler *mTaskScheduler; + + Projectile(const Projectile&); + Projectile& operator=(const Projectile&); + + int mProjectileId; + }; + +} + + +#endif diff --git a/apps/openmw/mwphysics/ptrholder.hpp b/apps/openmw/mwphysics/ptrholder.hpp index f8188b43e..152a5d64f 100644 --- a/apps/openmw/mwphysics/ptrholder.hpp +++ b/apps/openmw/mwphysics/ptrholder.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_MWPHYSICS_PTRHOLDER_H #define OPENMW_MWPHYSICS_PTRHOLDER_H +#include + #include "../mwworld/ptr.hpp" namespace MWPhysics @@ -12,21 +14,27 @@ namespace MWPhysics void updatePtr(const MWWorld::Ptr& updated) { + std::scoped_lock lock(mMutex); mPtr = updated; } MWWorld::Ptr getPtr() { + std::scoped_lock lock(mMutex); return mPtr; } MWWorld::ConstPtr getPtr() const { + std::scoped_lock lock(mMutex); return mPtr; } protected: MWWorld::Ptr mPtr; + + private: + mutable std::mutex mMutex; }; } diff --git a/apps/openmw/mwphysics/raycasting.hpp b/apps/openmw/mwphysics/raycasting.hpp index 7afbe9321..b176e8330 100644 --- a/apps/openmw/mwphysics/raycasting.hpp +++ b/apps/openmw/mwphysics/raycasting.hpp @@ -16,7 +16,7 @@ namespace MWPhysics osg::Vec3f mHitNormal; MWWorld::Ptr mHitObject; }; - + class RayCastingInterface { public: @@ -29,7 +29,7 @@ namespace MWPhysics /// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all other actors. virtual RayCastingResult castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore = MWWorld::ConstPtr(), std::vector targets = std::vector(), - int mask = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, int group=0xff) const = 0; + int mask = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, int group=0xff, int projId=-1) const = 0; virtual RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius) const = 0; diff --git a/apps/openmw/mwphysics/stepper.cpp b/apps/openmw/mwphysics/stepper.cpp index 0ab383dd1..2a28381be 100644 --- a/apps/openmw/mwphysics/stepper.cpp +++ b/apps/openmw/mwphysics/stepper.cpp @@ -7,6 +7,7 @@ #include "collisiontype.hpp" #include "constants.hpp" +#include "movementsolver.hpp" namespace MWPhysics { @@ -24,125 +25,155 @@ namespace MWPhysics Stepper::Stepper(const btCollisionWorld *colWorld, const btCollisionObject *colObj) : mColWorld(colWorld) , mColObj(colObj) - , mHaveMoved(true) { } - bool Stepper::step(osg::Vec3f &position, const osg::Vec3f &toMove, float &remainingTime) + bool Stepper::step(osg::Vec3f &position, osg::Vec3f &velocity, float &remainingTime, const bool & onGround, bool firstIteration) { - /* - * Slide up an incline or set of stairs. Should be called only after a - * collision detection otherwise unnecessary tracing will be performed. - * - * NOTE: with a small change this method can be used to step over an obstacle - * of height sStepSize. - * - * If successful return 'true' and update 'position' to the new possible - * location and adjust 'remainingTime'. - * - * If not successful return 'false'. May fail for these reasons: - * - can't move directly up from current position - * - having moved up by between epsilon() and sStepSize, can't move forward - * - having moved forward by between epsilon() and toMove, - * = moved down between 0 and just under sStepSize but slope was too steep, or - * = moved the full sStepSize down (FIXME: this could be a bug) - * - * Starting position. Obstacle or stairs with height upto sStepSize in front. - * - * +--+ +--+ |XX - * | | -------> toMove | | +--+XX - * | | | | |XXXXX - * | | +--+ | | +--+XXXXX - * | | |XX| | | |XXXXXXXX - * +--+ +--+ +--+ +-------- - * ============================================== - */ + if(velocity.x() == 0.0 && velocity.y() == 0.0) + return false; - /* - * Try moving up sStepSize using stepper. - * FIXME: does not work in case there is no front obstacle but there is one above - * - * +--+ +--+ - * | | | | - * | | | | |XX - * | | | | +--+XX - * | | | | |XXXXX - * +--+ +--+ +--+ +--+XXXXX - * |XX| |XXXXXXXX - * +--+ +-------- - * ============================================== - */ - if (mHaveMoved) + // Stairstepping algorithms work by moving up to avoid the step, moving forwards, then moving back down onto the ground. + // This algorithm has a couple of minor problems, but they don't cause problems for sane geometry, and just prevent stepping on insane geometry. + + mUpStepper.doTrace(mColObj, position, position+osg::Vec3f(0.0f,0.0f,sStepSizeUp), mColWorld); + + float upDistance = 0; + if(!mUpStepper.mHitObject) + upDistance = sStepSizeUp; + else if(mUpStepper.mFraction*sStepSizeUp > sCollisionMargin) + upDistance = mUpStepper.mFraction*sStepSizeUp - sCollisionMargin; + else { - mHaveMoved = false; - - mUpStepper.doTrace(mColObj, position, position+osg::Vec3f(0.0f,0.0f,sStepSizeUp), mColWorld); - if (mUpStepper.mFraction < std::numeric_limits::epsilon()) - return false; // didn't even move the smallest representable amount - // (TODO: shouldn't this be larger? Why bother with such a small amount?) + return false; } - /* - * Try moving from the elevated position using tracer. - * - * +--+ +--+ - * | | |YY| FIXME: collision with object YY - * | | +--+ - * | | - * <------------------->| | - * +--+ +--+ - * |XX| the moved amount is toMove*tracer.mFraction - * +--+ - * ============================================== - */ - osg::Vec3f tracerPos = mUpStepper.mEndPos; - mTracer.doTrace(mColObj, tracerPos, tracerPos + toMove, mColWorld); - if (mTracer.mFraction < std::numeric_limits::epsilon()) - return false; // didn't even move the smallest representable amount + auto toMove = velocity * remainingTime; - /* - * Try moving back down sStepSizeDown using stepper. - * NOTE: if there is an obstacle below (e.g. stairs), we'll be "stepping up". - * Below diagram is the case where we "stepped over" an obstacle in front. - * - * +--+ - * |YY| - * +--+ +--+ - * | | - * | | - * +--+ | | - * |XX| | | - * +--+ +--+ - * ============================================== - */ - mDownStepper.doTrace(mColObj, mTracer.mEndPos, mTracer.mEndPos-osg::Vec3f(0.0f,0.0f,sStepSizeDown), mColWorld); - if (!canStepDown(mDownStepper)) + osg::Vec3f tracerPos = position + osg::Vec3f(0.0f, 0.0f, upDistance); + + osg::Vec3f tracerDest; + auto normalMove = toMove; + auto moveDistance = normalMove.normalize(); + // attempt 1: normal movement + // attempt 2: fixed distance movement, only happens on the first movement solver iteration/bounce each frame to avoid a glitch + // attempt 3: further, less tall fixed distance movement, same as above + // If you're making a full conversion you should purge the logic for attempts 2 and 3. Attempts 2 and 3 just try to work around problems with vanilla Morrowind assets. + int attempt = 0; + float downStepSize; + while(attempt < 3) { - // Try again with increased step length - if (mTracer.mFraction < 1.0f || toMove.length2() > sMinStep*sMinStep) - return false; + attempt++; - osg::Vec3f direction = toMove; - direction.normalize(); - mTracer.doTrace(mColObj, tracerPos, tracerPos + direction*sMinStep, mColWorld); - if (mTracer.mFraction < 0.001f) + if(attempt == 1) + tracerDest = tracerPos + toMove; + else if (!firstIteration || !sDoExtraStairHacks) // first attempt failed and not on first movement solver iteration, can't retry -- or we have extra hacks disabled + { return false; + } + else if(attempt == 2) + { + moveDistance = sMinStep; + tracerDest = tracerPos + normalMove*sMinStep; + } + else if(attempt == 3) + { + if(upDistance > sStepSizeUp) + { + upDistance = sStepSizeUp; + tracerPos = position + osg::Vec3f(0.0f, 0.0f, upDistance); + } + moveDistance = sMinStep2; + tracerDest = tracerPos + normalMove*sMinStep2; + } + + mTracer.doTrace(mColObj, tracerPos, tracerDest, mColWorld); + if(mTracer.mHitObject) + { + // map against what we hit, minus the safety margin + moveDistance *= mTracer.mFraction; + if(moveDistance <= sCollisionMargin) // didn't move enough to accomplish anything + { + return false; + } + + moveDistance -= sCollisionMargin; + tracerDest = tracerPos + normalMove*moveDistance; + + // safely eject from what we hit by the safety margin + auto tempDest = tracerDest + mTracer.mPlaneNormal*sCollisionMargin*2; + + ActorTracer tempTracer; + tempTracer.doTrace(mColObj, tracerDest, tempDest, mColWorld); + + if(tempTracer.mFraction > 0.5f) // distance to any object is greater than sCollisionMargin (we checked sCollisionMargin*2 distance) + { + auto effectiveFraction = tempTracer.mFraction*2.0f - 1.0f; + tracerDest += mTracer.mPlaneNormal*sCollisionMargin*effectiveFraction; + } + } + + if(attempt > 2) // do not allow stepping down below original height for attempt 3 + downStepSize = upDistance; + else + downStepSize = moveDistance + upDistance + sStepSizeDown; + mDownStepper.doTrace(mColObj, tracerDest, tracerDest + osg::Vec3f(0.0f, 0.0f, -downStepSize), mColWorld); + + // can't step down onto air, non-walkable-slopes, or actors + // NOTE: using a capsule causes isWalkableSlope (used in canStepDown) to fail on certain geometry that were intended to be valid at the bottoms of stairs + // (like the bottoms of the staircases in aldruhn's guild of mages) + // The old code worked around this by trying to do mTracer again with a fixed distance of sMinStep (10.0) but it caused all sorts of other problems. + // Switched back to cylinders to avoid that and similer problems. + if(canStepDown(mDownStepper)) + { + break; + } + else + { + // do not try attempt 3 if we just tried attempt 2 and the horizontal distance was rather large + // (forces actor to get snug against the defective ledge for attempt 3 to be tried) + if(attempt == 2 && moveDistance > upDistance-(mDownStepper.mFraction*downStepSize)) + { + return false; + } + // do next attempt if first iteration of movement solver and not out of attempts + if(firstIteration && attempt < 3) + { + continue; + } - mDownStepper.doTrace(mColObj, mTracer.mEndPos, mTracer.mEndPos-osg::Vec3f(0.0f,0.0f,sStepSizeDown), mColWorld); - if (!canStepDown(mDownStepper)) return false; + } } - if (mDownStepper.mFraction < 1.0f) + // note: can't downstep onto actors so no need to pick safety margin + float downDistance = 0; + if(mDownStepper.mFraction*downStepSize > sCollisionMargin) + downDistance = mDownStepper.mFraction*downStepSize - sCollisionMargin; + + if(downDistance-sCollisionMargin-sGroundOffset > upDistance && !onGround) + return false; + + auto newpos = tracerDest + osg::Vec3f(0.0f, 0.0f, -downDistance); + + if((position-newpos).length2() < sCollisionMargin*sCollisionMargin) + return false; + + if(mTracer.mHitObject) { - // only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall. - // TODO: stepper.mPlaneNormal does not appear to be reliable - needs more testing - // NOTE: caller's variables 'position' & 'remainingTime' are modified here - position = mDownStepper.mEndPos; - remainingTime *= (1.0f-mTracer.mFraction); // remaining time is proportional to remaining distance - mHaveMoved = true; - return true; + auto planeNormal = mTracer.mPlaneNormal; + if (onGround && !isWalkableSlope(planeNormal) && planeNormal.z() != 0) + { + planeNormal.z() = 0; + planeNormal.normalize(); + } + velocity = reject(velocity, planeNormal); } - return false; + velocity = reject(velocity, mDownStepper.mPlaneNormal); + + position = newpos; + + remainingTime *= (1.0f-mTracer.mFraction); // remaining time is proportional to remaining distance + return true; } } diff --git a/apps/openmw/mwphysics/stepper.hpp b/apps/openmw/mwphysics/stepper.hpp index 27e6294b0..512493c52 100644 --- a/apps/openmw/mwphysics/stepper.hpp +++ b/apps/openmw/mwphysics/stepper.hpp @@ -20,12 +20,11 @@ namespace MWPhysics const btCollisionObject *mColObj; ActorTracer mTracer, mUpStepper, mDownStepper; - bool mHaveMoved; public: Stepper(const btCollisionWorld *colWorld, const btCollisionObject *colObj); - bool step(osg::Vec3f &position, const osg::Vec3f &toMove, float &remainingTime); + bool step(osg::Vec3f &position, osg::Vec3f &velocity, float &remainingTime, const bool & onGround, bool firstIteration); }; } diff --git a/apps/openmw/mwphysics/trace.cpp b/apps/openmw/mwphysics/trace.cpp index 58082f4db..f50b6100a 100644 --- a/apps/openmw/mwphysics/trace.cpp +++ b/apps/openmw/mwphysics/trace.cpp @@ -24,7 +24,7 @@ void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& star to.setOrigin(btend); const btVector3 motion = btstart-btend; - ClosestNotMeConvexResultCallback newTraceCallback(actor, motion, btScalar(0.0)); + ClosestNotMeConvexResultCallback newTraceCallback(actor, motion, btScalar(0.0), world); // Inherit the actor's collision group and mask newTraceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; newTraceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; @@ -62,7 +62,7 @@ void ActorTracer::findGround(const Actor* actor, const osg::Vec3f& start, const btTransform to(trans.getBasis(), btend); const btVector3 motion = btstart-btend; - ClosestNotMeConvexResultCallback newTraceCallback(actor->getCollisionObject(), motion, btScalar(0.0)); + ClosestNotMeConvexResultCallback newTraceCallback(actor->getCollisionObject(), motion, btScalar(0.0), world); // Inherit the actor's collision group and mask newTraceCallback.m_collisionFilterGroup = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterGroup; newTraceCallback.m_collisionFilterMask = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterMask; diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 8c9f5f493..f8ff3780d 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -782,8 +782,6 @@ namespace MWRender NodeMap::const_iterator found = nodeMap.find("bip01"); if (found == nodeMap.end()) found = nodeMap.find("root bone"); - if (found == nodeMap.end()) - found = nodeMap.find("root"); if (found != nodeMap.end()) mAccumRoot = found->second; diff --git a/apps/openmw/mwrender/bulletdebugdraw.cpp b/apps/openmw/mwrender/bulletdebugdraw.cpp index 00529ef80..207b0ee50 100644 --- a/apps/openmw/mwrender/bulletdebugdraw.cpp +++ b/apps/openmw/mwrender/bulletdebugdraw.cpp @@ -7,6 +7,9 @@ #include #include +#include +#include +#include #include "bulletdebugdraw.hpp" #include "vismask.hpp" @@ -41,6 +44,14 @@ void DebugDrawer::createGeometry() mGeometry->addPrimitiveSet(mDrawArrays); mParentNode->addChild(mGeometry); + + auto* stateSet = new osg::StateSet; + stateSet->setAttributeAndModes(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE), osg::StateAttribute::ON); + mShapesRoot = new osg::Group; + mShapesRoot->setStateSet(stateSet); + mShapesRoot->setDataVariance(osg::Object::DYNAMIC); + mShapesRoot->setNodeMask(Mask_Debug); + mParentNode->addChild(mShapesRoot); } } @@ -49,6 +60,7 @@ void DebugDrawer::destroyGeometry() if (mGeometry) { mParentNode->removeChild(mGeometry); + mParentNode->removeChild(mShapesRoot); mGeometry = nullptr; mVertices = nullptr; mDrawArrays = nullptr; @@ -66,6 +78,7 @@ void DebugDrawer::step() { mVertices->clear(); mColors->clear(); + mShapesRoot->removeChildren(0, mShapesRoot->getNumChildren()); mWorld->debugDrawWorld(); showCollisions(); mDrawArrays->setCount(mVertices->size()); @@ -106,12 +119,11 @@ void DebugDrawer::showCollisions() mCollisionViews.end()); } -void DebugDrawer::drawContactPoint(const btVector3 &PointOnB, const btVector3 &normalOnB, btScalar distance, int lifeTime, const btVector3 &color) +void DebugDrawer::drawSphere(btScalar radius, const btTransform& transform, const btVector3& color) { - mVertices->push_back(Misc::Convert::toOsg(PointOnB)); - mVertices->push_back(Misc::Convert::toOsg(PointOnB) + (Misc::Convert::toOsg(normalOnB) * distance * 20)); - mColors->push_back({1,1,1,1}); - mColors->push_back({1,1,1,1}); + auto* geom = new osg::ShapeDrawable(new osg::Sphere(Misc::Convert::toOsg(transform.getOrigin()), radius)); + geom->setColor(osg::Vec4(1, 1, 1, 1)); + mShapesRoot->addChild(geom); } void DebugDrawer::reportErrorWarning(const char *warningString) diff --git a/apps/openmw/mwrender/bulletdebugdraw.hpp b/apps/openmw/mwrender/bulletdebugdraw.hpp index ec421bd74..b24640427 100644 --- a/apps/openmw/mwrender/bulletdebugdraw.hpp +++ b/apps/openmw/mwrender/bulletdebugdraw.hpp @@ -32,6 +32,7 @@ private: CollisionView(btVector3 orig, btVector3 normal) : mOrig(orig), mEnd(orig + normal * 20), mCreated(std::chrono::steady_clock::now()) {}; }; std::vector mCollisionViews; + osg::ref_ptr mShapesRoot; protected: osg::ref_ptr mParentNode; @@ -59,7 +60,8 @@ public: void showCollisions(); - void drawContactPoint(const btVector3& PointOnB,const btVector3& normalOnB,btScalar distance,int lifeTime,const btVector3& color) override; + void drawContactPoint(const btVector3& PointOnB,const btVector3& normalOnB,btScalar distance,int lifeTime,const btVector3& color) override {}; + void drawSphere(btScalar radius, const btTransform& transform, const btVector3& color) override; void reportErrorWarning(const char* warningString) override; diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index b964eacff..12e6ef3ce 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -356,9 +356,6 @@ namespace MWRender else mViewModeToggleQueued = false; - if (mTrackingPtr.getClass().isActor()) - mTrackingPtr.getClass().getCreatureStats(mTrackingPtr).setSideMovementAngle(0); - mFirstPersonView = !mFirstPersonView; updateStandingPreviewMode(); instantTransition(); diff --git a/apps/openmw/mwrender/landmanager.hpp b/apps/openmw/mwrender/landmanager.hpp index ea73f11c2..1694bd243 100644 --- a/apps/openmw/mwrender/landmanager.hpp +++ b/apps/openmw/mwrender/landmanager.hpp @@ -1,5 +1,5 @@ -#ifndef OPENMW_COMPONENTS_ESMTERRAIN_LANDMANAGER_H -#define OPENMW_COMPONENTS_ESMTERRAIN_LANDMANAGER_H +#ifndef OPENMW_MWRENDER_LANDMANAGER_H +#define OPENMW_MWRENDER_LANDMANAGER_H #include diff --git a/apps/openmw/mwrender/objectpaging.hpp b/apps/openmw/mwrender/objectpaging.hpp index ff32dadd4..65f53d530 100644 --- a/apps/openmw/mwrender/objectpaging.hpp +++ b/apps/openmw/mwrender/objectpaging.hpp @@ -1,5 +1,5 @@ -#ifndef OPENMW_COMPONENTS_ESMPAGING_CHUNKMANAGER_H -#define OPENMW_COMPONENTS_ESMPAGING_CHUNKMANAGER_H +#ifndef OPENMW_MWRENDER_OBJECTPAGING_H +#define OPENMW_MWRENDER_OBJECTPAGING_H #include #include diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 63d3743a9..217f0b73c 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -305,7 +305,7 @@ namespace MWRender mTerrain->setWorkQueue(mWorkQueue.get()); // water goes after terrain for correct waterculling order - mWater.reset(new Water(mRootNode, sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), resourcePath)); + mWater.reset(new Water(sceneRoot->getParent(0), sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), resourcePath)); mCamera.reset(new Camera(mViewer->getCamera())); if (Settings::Manager::getBool("view over shoulder", "Camera")) diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index b569f1bfa..b9018e0a2 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -235,13 +235,14 @@ public: Refraction() { unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); - setRenderOrder(osg::Camera::PRE_RENDER); + setRenderOrder(osg::Camera::PRE_RENDER, 1); setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); setReferenceFrame(osg::Camera::RELATIVE_RF); setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); setName("RefractionCamera"); setCullCallback(new InheritViewPointCallback); + setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); setCullMask(Mask_Effect|Mask_Scene|Mask_Object|Mask_Static|Mask_Terrain|Mask_Actor|Mask_ParticleSystem|Mask_Sky|Mask_Sun|Mask_Player|Mask_Lighting); setNodeMask(Mask_RenderToTexture); @@ -284,7 +285,8 @@ public: attach(osg::Camera::DEPTH_BUFFER, mRefractionDepthTexture); - SceneUtil::ShadowManager::disableShadowsForStateSet(getOrCreateStateSet()); + if (Settings::Manager::getFloat("refraction scale", "Water") != 1) // TODO: to be removed with issue #5709 + SceneUtil::ShadowManager::disableShadowsForStateSet(getOrCreateStateSet()); } void setScene(osg::Node* scene) diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index 1dd8c253b..251844641 100644 --- a/apps/openmw/mwscript/transformationextensions.cpp +++ b/apps/openmw/mwscript/transformationextensions.cpp @@ -49,11 +49,7 @@ namespace MWScript std::vector actors; MWBase::Environment::get().getWorld()->getActorsStandingOn (ptr, actors); for (auto& actor : actors) - { - osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); - actorPos += diff; - MWBase::Environment::get().getWorld()->moveObject(actor, actorPos.x(), actorPos.y(), actorPos.z()); - } + MWBase::Environment::get().getWorld()->moveObjectBy(actor, diff); } template @@ -903,14 +899,12 @@ namespace MWScript return; osg::Vec3f diff = ptr.getRefData().getBaseNode()->getAttitude() * posChange; - osg::Vec3f worldPos(ptr.getRefData().getPosition().asVec3()); - worldPos += diff; // We should move actors, standing on moving object, too. // This approach can be used to create elevators. moveStandingActors(ptr, diff); dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObject(ptr, worldPos.x(), worldPos.y(), worldPos.z())); + MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff)); } }; @@ -931,15 +925,14 @@ namespace MWScript Interpreter::Type_Float movement = (runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); runtime.pop(); - const float *objPos = ptr.getRefData().getPosition().pos; osg::Vec3f diff; if (axis == "x") - diff.x() += movement; + diff.x() = movement; else if (axis == "y") - diff.y() += movement; + diff.y() = movement; else if (axis == "z") - diff.z() += movement; + diff.z() = movement; else return; @@ -947,7 +940,7 @@ namespace MWScript // This approach can be used to create elevators. moveStandingActors(ptr, diff); dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObject(ptr, objPos[0]+diff.x(), objPos[1]+diff.y(), objPos[2]+diff.z())); + MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff)); } }; diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index 3f22fa1b7..434f5c2d2 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -39,7 +39,7 @@ namespace void addScripts(MWWorld::ContainerStore& store, MWWorld::CellStore* cell) { auto& scripts = MWBase::Environment::get().getWorld()->getLocalScripts(); - for(const MWWorld::Ptr& ptr : store) + for(const auto&& ptr : store) { const std::string& script = ptr.getClass().getScript(ptr); if(!script.empty()) @@ -56,13 +56,10 @@ namespace { float sum = 0; - for (typename MWWorld::CellRefList::List::const_iterator iter ( - cellRefList.mList.begin()); - iter!=cellRefList.mList.end(); - ++iter) + for (const auto& iter : cellRefList.mList) { - if (iter->mData.getCount()>0) - sum += iter->mData.getCount()*iter->mBase->mData.mWeight; + if (iter.mData.getCount()>0) + sum += iter.mData.getCount()*iter.mBase->mData.mWeight; } return sum; @@ -75,12 +72,11 @@ namespace store->resolve(); std::string id2 = Misc::StringUtils::lowerCase (id); - for (typename MWWorld::CellRefList::List::iterator iter (list.mList.begin()); - iter!=list.mList.end(); ++iter) + for (auto& iter : list.mList) { - if (Misc::StringUtils::ciEqual(iter->mBase->mId, id2) && iter->mData.getCount()) + if (Misc::StringUtils::ciEqual(iter.mBase->mId, id2) && iter.mData.getCount()) { - MWWorld::Ptr ptr (&*iter, nullptr); + MWWorld::Ptr ptr (&iter, nullptr); ptr.setContainerStore (store); return ptr; } @@ -94,7 +90,7 @@ MWWorld::ResolutionListener::~ResolutionListener() { if(!mStore.mModified && mStore.mResolved && !mStore.mPtr.isEmpty()) { - for(const MWWorld::Ptr& ptr : mStore) + for(const auto&& ptr : mStore) ptr.getRefData().setCount(0); mStore.fillNonRandom(mStore.mPtr.get()->mBase->mInventory, "", mStore.mSeed); addScripts(mStore, mStore.mPtr.mCell); @@ -140,15 +136,14 @@ template void MWWorld::ContainerStore::storeStates (const CellRefList& collection, ESM::InventoryState& inventory, int& index, bool equipable) const { - for (typename CellRefList::List::const_iterator iter (collection.mList.begin()); - iter!=collection.mList.end(); ++iter) + for (const auto& iter : collection.mList) { - if (iter->mData.getCount() == 0) + if (iter.mData.getCount() == 0) continue; ESM::ObjectState state; - storeState (*iter, state); + storeState (iter, state); if (equipable) - storeEquipmentState(*iter, index, inventory); + storeEquipmentState(iter, index, inventory); inventory.mItems.push_back (state); ++index; } @@ -201,7 +196,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::end() int MWWorld::ContainerStore::count(const std::string &id) const { int total=0; - for (const auto& iter : *this) + for (const auto&& iter : *this) if (Misc::StringUtils::ciEqual(iter.getCellRef().getRefId(), id)) total += iter.getRefData().getCount(); return total; @@ -494,14 +489,14 @@ void MWWorld::ContainerStore::rechargeItems(float duration) updateRechargingItems(); mRechargingItemsUpToDate = true; } - for (TRechargingItems::iterator it = mRechargingItems.begin(); it != mRechargingItems.end(); ++it) + for (auto& it : mRechargingItems) { - if (!MWMechanics::rechargeItem(*it->first, it->second, duration)) + if (!MWMechanics::rechargeItem(*it.first, it.second, duration)) continue; // attempt to restack when fully recharged - if (it->first->getCellRef().getEnchantmentCharge() == it->second) - it->first = restack(*it->first); + if (it.first->getCellRef().getEnchantmentCharge() == it.second) + it.first = restack(*it.first); } } @@ -545,9 +540,9 @@ int MWWorld::ContainerStore::remove(const std::string& itemId, int count, const bool MWWorld::ContainerStore::hasVisibleItems() const { - for (auto iter(begin()); iter != end(); ++iter) + for (const auto&& iter : *this) { - if (iter->getClass().showsInInventory(*iter)) + if (iter.getClass().showsInInventory(iter)) return true; } @@ -692,8 +687,8 @@ void MWWorld::ContainerStore::addInitialItemImp(const MWWorld::Ptr& ptr, const s void MWWorld::ContainerStore::clear() { - for (ContainerStoreIterator iter (begin()); iter!=end(); ++iter) - iter->getRefData().setCount (0); + for (auto&& iter : *this) + iter.getRefData().setCount (0); flagAsModified(); mModified = true; @@ -728,7 +723,7 @@ void MWWorld::ContainerStore::resolve() { if(!mResolved && !mPtr.isEmpty()) { - for(const MWWorld::Ptr& ptr : *this) + for(const auto&& ptr : *this) ptr.getRefData().setCount(0); Misc::Rng::Seed seed{mSeed}; fill(mPtr.get()->mBase->mInventory, "", seed); @@ -749,7 +744,7 @@ MWWorld::ResolutionHandle MWWorld::ContainerStore::resolveTemporarily() } if(!mResolved && !mPtr.isEmpty()) { - for(const MWWorld::Ptr& ptr : *this) + for(const auto&& ptr : *this) ptr.getRefData().setCount(0); Misc::Rng::Seed seed{mSeed}; fill(mPtr.get()->mBase->mInventory, "", seed); @@ -832,10 +827,10 @@ MWWorld::Ptr MWWorld::ContainerStore::findReplacement(const std::string& id) { MWWorld::Ptr item; int itemHealth = 1; - for (MWWorld::ContainerStoreIterator iter = begin(); iter != end(); ++iter) + for (auto&& iter : *this) { - int iterHealth = iter->getClass().hasItemHealth(*iter) ? iter->getClass().getItemHealth(*iter) : 1; - if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), id)) + int iterHealth = iter.getClass().hasItemHealth(iter) ? iter.getClass().getItemHealth(iter) : 1; + if (Misc::StringUtils::ciEqual(iter.getCellRef().getRefId(), id)) { // Prefer the stack with the lowest remaining uses // Try to get item with zero durability only if there are no other items found @@ -843,7 +838,7 @@ MWWorld::Ptr MWWorld::ContainerStore::findReplacement(const std::string& id) (iterHealth > 0 && iterHealth < itemHealth) || (itemHealth <= 0 && iterHealth > 0)) { - item = *iter; + item = iter; itemHealth = iterHealth; } } @@ -972,11 +967,8 @@ void MWWorld::ContainerStore::readState (const ESM::InventoryState& inventory) mResolved = true; int index = 0; - for (std::vector::const_iterator - iter (inventory.mItems.begin()); iter!=inventory.mItems.end(); ++iter) + for (const ESM::ObjectState& state : inventory.mItems) { - const ESM::ObjectState& state = *iter; - int type = MWBase::Environment::get().getWorld()->getStore().find(state.mRef.mRefID); int thisIndex = index++; diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index fd7b81a18..54f5f12b3 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -601,6 +601,14 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) } } +MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getPreferredShield(const MWWorld::Ptr& actor) +{ + TSlots slots; + initSlots (slots); + autoEquipArmor(actor, slots); + return slots[Slot_CarriedLeft]; +} + const MWMechanics::MagicEffects& MWWorld::InventoryStore::getMagicEffects() const { return mMagicEffects; diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index e70c21480..df69a5709 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -153,6 +153,8 @@ namespace MWWorld ContainerStoreIterator getSlot (int slot); ConstContainerStoreIterator getSlot(int slot) const; + ContainerStoreIterator getPreferredShield(const MWWorld::Ptr& actor); + void unequipAll(const MWWorld::Ptr& actor); ///< Unequip all currently equipped items. diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index 39cdc3e72..08c9e4662 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -43,6 +44,7 @@ #include "../mwsound/sound.hpp" #include "../mwphysics/physicssystem.hpp" +#include "../mwphysics/projectile.hpp" namespace { @@ -284,7 +286,7 @@ namespace MWWorld else state.mActorId = -1; - std::string texture = ""; + std::string texture; state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId); @@ -302,6 +304,7 @@ namespace MWWorld MWWorld::Ptr ptr = ref.getPtr(); osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects); + createModel(state, ptr.getClass().getModel(ptr), pos, orient, true, true, lightDiffuseColor, texture); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); @@ -312,7 +315,9 @@ namespace MWWorld if (sound) state.mSounds.push_back(sound); } - + + state.mProjectileId = mPhysics->addProjectile(caster, pos); + state.mToDelete = false; mMagicBolts.push_back(state); } @@ -325,7 +330,6 @@ namespace MWWorld state.mIdArrow = projectile.getCellRef().getRefId(); state.mCasterHandle = actor; state.mAttackStrength = attackStrength; - int type = projectile.get()->mBase->mData.mType; state.mThrown = MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown; @@ -336,6 +340,8 @@ namespace MWWorld if (!ptr.getClass().getEnchantment(ptr).empty()) SceneUtil::addEnchantedGlow(state.mNode, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr)); + state.mProjectileId = mPhysics->addProjectile(actor, pos); + state.mToDelete = false; mProjectiles.push_back(state); } @@ -360,72 +366,68 @@ namespace MWWorld return (state.mNode->getPosition() - playerPos).length2() >= farawayThreshold*farawayThreshold; }; - for (std::vector::iterator it = mProjectiles.begin(); it != mProjectiles.end();) + for (auto& projectileState : mProjectiles) { - if (isCleanable(*it)) - { - cleanupProjectile(*it); - it = mProjectiles.erase(it); - } - else - ++it; + if (isCleanable(projectileState)) + cleanupProjectile(projectileState); } - for (std::vector::iterator it = mMagicBolts.begin(); it != mMagicBolts.end();) + for (auto& magicBoltState : mMagicBolts) { - if (isCleanable(*it)) - { - cleanupMagicBolt(*it); - it = mMagicBolts.erase(it); - } - else - ++it; + if (isCleanable(magicBoltState)) + cleanupMagicBolt(magicBoltState); } } } void ProjectileManager::moveMagicBolts(float duration) { - for (std::vector::iterator it = mMagicBolts.begin(); it != mMagicBolts.end();) + for (auto& magicBoltState : mMagicBolts) { + if (magicBoltState.mToDelete) + continue; + + auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId); + if (!projectile->isActive()) + continue; // If the actor caster is gone, the magic bolt needs to be removed from the scene during the next frame. - MWWorld::Ptr caster = it->getCaster(); + MWWorld::Ptr caster = magicBoltState.getCaster(); if (!caster.isEmpty() && caster.getClass().isActor()) { if (caster.getRefData().getCount() <= 0 || caster.getClass().getCreatureStats(caster).isDead()) { - cleanupMagicBolt(*it); - it = mMagicBolts.erase(it); + cleanupMagicBolt(magicBoltState); continue; } } - osg::Quat orient = it->mNode->getAttitude(); + osg::Quat orient = magicBoltState.mNode->getAttitude(); static float fTargetSpellMaxSpeed = MWBase::Environment::get().getWorld()->getStore().get() .find("fTargetSpellMaxSpeed")->mValue.getFloat(); - float speed = fTargetSpellMaxSpeed * it->mSpeed; + float speed = fTargetSpellMaxSpeed * magicBoltState.mSpeed; osg::Vec3f direction = orient * osg::Vec3f(0,1,0); direction.normalize(); - osg::Vec3f pos(it->mNode->getPosition()); + osg::Vec3f pos(magicBoltState.mNode->getPosition()); osg::Vec3f newPos = pos + direction * duration * speed; - for (size_t soundIter = 0; soundIter != it->mSounds.size(); soundIter++) - { - it->mSounds.at(soundIter)->setPosition(newPos); - } + for (const auto& sound : magicBoltState.mSounds) + sound->setPosition(newPos); - it->mNode->setPosition(newPos); + magicBoltState.mNode->setPosition(newPos); - update(*it, duration); + mPhysics->updateProjectile(magicBoltState.mProjectileId, newPos); + + update(magicBoltState, duration); // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. std::vector targetActors; if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); + projectile->setValidTargets(targetActors); // Check for impact // TODO: use a proper btRigidBody / btGhostObject? - MWPhysics::RayCastingResult result = mPhysics->castRay(pos, newPos, caster, targetActors, 0xff, MWPhysics::CollisionType_Projectile); + const auto result = mPhysics->castRay(pos, newPos, caster, targetActors, 0xff, MWPhysics::CollisionType_Projectile, magicBoltState.mProjectileId); bool hit = false; if (result.mHit) @@ -433,16 +435,16 @@ namespace MWWorld hit = true; if (result.mHitObject.isEmpty()) { - // terrain + // terrain or projectile } else { MWMechanics::CastSpell cast(caster, result.mHitObject); cast.mHitPosition = pos; - cast.mId = it->mSpellId; - cast.mSourceName = it->mSourceName; + cast.mId = magicBoltState.mSpellId; + cast.mSourceName = magicBoltState.mSourceName; cast.mStack = false; - cast.inflict(result.mHitObject, caster, it->mEffects, ESM::RT_Target, false, true); + cast.inflict(result.mHitObject, caster, magicBoltState.mEffects, ESM::RT_Target, false, true); mPhysics->reportCollision(Misc::Convert::toBullet(result.mHitPos), Misc::Convert::toBullet(result.mHitNormal)); } } @@ -453,99 +455,172 @@ namespace MWWorld if (hit) { - MWBase::Environment::get().getWorld()->explodeSpell(pos, it->mEffects, caster, result.mHitObject, - ESM::RT_Target, it->mSpellId, it->mSourceName); + MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, result.mHitObject, + ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName); - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - for (size_t soundIter = 0; soundIter != it->mSounds.size(); soundIter++) - sndMgr->stopSound(it->mSounds.at(soundIter)); - - mParent->removeChild(it->mNode); - - it = mMagicBolts.erase(it); - continue; + cleanupMagicBolt(magicBoltState); } - else - ++it; } } void ProjectileManager::moveProjectiles(float duration) { - for (std::vector::iterator it = mProjectiles.begin(); it != mProjectiles.end();) + for (auto& projectileState : mProjectiles) { + if (projectileState.mToDelete) + continue; + + auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId); + if (!projectile->isActive()) + continue; // gravity constant - must be way lower than the gravity affecting actors, since we're not // simulating aerodynamics at all - it->mVelocity -= osg::Vec3f(0, 0, Constants::GravityConst * Constants::UnitsPerMeter * 0.1f) * duration; + projectileState.mVelocity -= osg::Vec3f(0, 0, Constants::GravityConst * Constants::UnitsPerMeter * 0.1f) * duration; - osg::Vec3f pos(it->mNode->getPosition()); - osg::Vec3f newPos = pos + it->mVelocity * duration; + osg::Vec3f pos(projectileState.mNode->getPosition()); + osg::Vec3f newPos = pos + projectileState.mVelocity * duration; // rotation does not work well for throwing projectiles - their roll angle will depend on shooting direction. - if (!it->mThrown) + if (!projectileState.mThrown) { osg::Quat orient; - orient.makeRotate(osg::Vec3f(0,1,0), it->mVelocity); - it->mNode->setAttitude(orient); + orient.makeRotate(osg::Vec3f(0,1,0), projectileState.mVelocity); + projectileState.mNode->setAttitude(orient); } - it->mNode->setPosition(newPos); + projectileState.mNode->setPosition(newPos); - update(*it, duration); + mPhysics->updateProjectile(projectileState.mProjectileId, newPos); - MWWorld::Ptr caster = it->getCaster(); + update(projectileState, duration); + + MWWorld::Ptr caster = projectileState.getCaster(); // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. std::vector targetActors; if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); + projectile->setValidTargets(targetActors); // Check for impact // TODO: use a proper btRigidBody / btGhostObject? - MWPhysics::RayCastingResult result = mPhysics->castRay(pos, newPos, caster, targetActors, 0xff, MWPhysics::CollisionType_Projectile); + const auto result = mPhysics->castRay(pos, newPos, caster, targetActors, 0xff, MWPhysics::CollisionType_Projectile, projectileState.mProjectileId); bool underwater = MWBase::Environment::get().getWorld()->isUnderwater(MWMechanics::getPlayer().getCell(), newPos); if (result.mHit || underwater) { - MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), it->mIdArrow); - // Try to get a Ptr to the bow that was used. It might no longer exist. + MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), projectileState.mIdArrow); MWWorld::Ptr bow = projectileRef.getPtr(); - if (!caster.isEmpty() && it->mIdArrow != it->mBowId) + if (!caster.isEmpty() && projectileState.mIdArrow != projectileState.mBowId) { MWWorld::InventoryStore& inv = caster.getClass().getInventoryStore(caster); MWWorld::ContainerStoreIterator invIt = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), it->mBowId)) + if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), projectileState.mBowId)) bow = *invIt; } if (caster.isEmpty()) caster = result.mHitObject; - MWMechanics::projectileHit(caster, result.mHitObject, bow, projectileRef.getPtr(), result.mHit ? result.mHitPos : newPos, it->mAttackStrength); + MWMechanics::projectileHit(caster, result.mHitObject, bow, projectileRef.getPtr(), result.mHit ? result.mHitPos : newPos, projectileState.mAttackStrength); mPhysics->reportCollision(Misc::Convert::toBullet(result.mHitPos), Misc::Convert::toBullet(result.mHitNormal)); if (underwater) mRendering->emitWaterRipple(newPos); - mParent->removeChild(it->mNode); - it = mProjectiles.erase(it); + cleanupProjectile(projectileState); + } + } + } + + void ProjectileManager::processHits() + { + for (auto& projectileState : mProjectiles) + { + if (projectileState.mToDelete) + continue; + + auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId); + if (projectile->isActive()) + continue; + const auto target = projectile->getTarget(); + const auto pos = projectile->getHitPos(); + MWWorld::Ptr caster = projectileState.getCaster(); + assert(target != caster); + if (!projectile->isValidTarget(target)) + { + projectile->activate(); continue; } - ++it; + if (caster.isEmpty()) + caster = target; + + // Try to get a Ptr to the bow that was used. It might no longer exist. + MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), projectileState.mIdArrow); + MWWorld::Ptr bow = projectileRef.getPtr(); + if (!caster.isEmpty() && projectileState.mIdArrow != projectileState.mBowId) + { + MWWorld::InventoryStore& inv = caster.getClass().getInventoryStore(caster); + MWWorld::ContainerStoreIterator invIt = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), projectileState.mBowId)) + bow = *invIt; + } + + projectileState.mHitPosition = pos; + cleanupProjectile(projectileState); + MWMechanics::projectileHit(caster, target, bow, projectileRef.getPtr(), pos, projectileState.mAttackStrength); } + for (auto& magicBoltState : mMagicBolts) + { + if (magicBoltState.mToDelete) + continue; + + auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId); + if (projectile->isActive()) + continue; + const auto target = projectile->getTarget(); + const auto pos = projectile->getHitPos(); + MWWorld::Ptr caster = magicBoltState.getCaster(); + assert(target != caster); + if (!projectile->isValidTarget(target)) + { + projectile->activate(); + continue; + } + + magicBoltState.mHitPosition = pos; + cleanupMagicBolt(magicBoltState); + + MWMechanics::CastSpell cast(caster, target); + cast.mHitPosition = pos; + cast.mId = magicBoltState.mSpellId; + cast.mSourceName = magicBoltState.mSourceName; + cast.mStack = false; + cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, false, true); + + MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName); + } + mProjectiles.erase(std::remove_if(mProjectiles.begin(), mProjectiles.end(), [](const State& state) { return state.mToDelete; }), + mProjectiles.end()); + mMagicBolts.erase(std::remove_if(mMagicBolts.begin(), mMagicBolts.end(), [](const State& state) { return state.mToDelete; }), + mMagicBolts.end()); } void ProjectileManager::cleanupProjectile(ProjectileManager::ProjectileState& state) { mParent->removeChild(state.mNode); + mPhysics->removeProjectile(state.mProjectileId); + state.mToDelete = true; } void ProjectileManager::cleanupMagicBolt(ProjectileManager::MagicBoltState& state) { mParent->removeChild(state.mNode); + mPhysics->removeProjectile(state.mProjectileId); + state.mToDelete = true; for (size_t soundIter = 0; soundIter != state.mSounds.size(); soundIter++) { MWBase::Environment::get().getSoundManager()->stopSound(state.mSounds.at(soundIter)); @@ -554,15 +629,12 @@ namespace MWWorld void ProjectileManager::clear() { - for (std::vector::iterator it = mProjectiles.begin(); it != mProjectiles.end(); ++it) - { - cleanupProjectile(*it); - } + for (auto& mProjectile : mProjectiles) + cleanupProjectile(mProjectile); mProjectiles.clear(); - for (std::vector::iterator it = mMagicBolts.begin(); it != mMagicBolts.end(); ++it) - { - cleanupMagicBolt(*it); - } + + for (auto& mMagicBolt : mMagicBolts) + cleanupMagicBolt(mMagicBolt); mMagicBolts.clear(); } @@ -619,6 +691,7 @@ namespace MWWorld state.mVelocity = esm.mVelocity; state.mIdArrow = esm.mId; state.mAttackStrength = esm.mAttackStrength; + state.mToDelete = false; std::string model; try @@ -626,9 +699,10 @@ namespace MWWorld MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), esm.mId); MWWorld::Ptr ptr = ref.getPtr(); model = ptr.getClass().getModel(ptr); - int weaponType = ptr.get()->mBase->mData.mType; state.mThrown = MWMechanics::getWeaponType(weaponType)->mWeaponClass == ESM::WeaponType::Thrown; + + state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition)); } catch(...) { @@ -640,7 +714,7 @@ namespace MWWorld mProjectiles.push_back(state); return true; } - else if (type == ESM::REC_MPRJ) + if (type == ESM::REC_MPRJ) { ESM::MagicBoltState esm; esm.load(reader); @@ -649,7 +723,8 @@ namespace MWWorld state.mIdMagic.push_back(esm.mId); state.mSpellId = esm.mSpellId; state.mActorId = esm.mActorId; - std::string texture = ""; + state.mToDelete = false; + std::string texture; try { @@ -672,6 +747,7 @@ namespace MWWorld MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), state.mIdMagic.at(0)); MWWorld::Ptr ptr = ref.getPtr(); model = ptr.getClass().getModel(ptr); + state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition)); } catch(...) { diff --git a/apps/openmw/mwworld/projectilemanager.hpp b/apps/openmw/mwworld/projectilemanager.hpp index c7b1056b7..2cd570847 100644 --- a/apps/openmw/mwworld/projectilemanager.hpp +++ b/apps/openmw/mwworld/projectilemanager.hpp @@ -56,6 +56,8 @@ namespace MWWorld void update(float dt); + void processHits(); + /// Removes all current projectiles. Should be called when switching to a new worldspace. void clear(); @@ -76,6 +78,9 @@ namespace MWWorld std::shared_ptr mEffectAnimationTime; int mActorId; + int mProjectileId; + + osg::Vec3f mHitPosition; // TODO: this will break when the game is saved and reloaded, since there is currently // no way to write identifiers for non-actors to a savegame. @@ -88,6 +93,8 @@ namespace MWWorld // MW-id of an arrow projectile std::string mIdArrow; + + bool mToDelete; }; struct MagicBoltState : public State diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 1042e752c..99b3147f0 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1417,6 +1417,18 @@ namespace MWWorld return moveObjectImp(ptr, x, y, z, true, moveToActive); } + MWWorld::Ptr World::moveObjectBy(const Ptr& ptr, osg::Vec3f vec) + { + auto* actor = mPhysics->getActor(ptr); + if (actor) + { + actor->adjustPosition(vec); + return ptr; + } + osg::Vec3f newpos = ptr.getRefData().getPosition().asVec3() + vec; + return moveObject(ptr, newpos.x(), newpos.y(), newpos.z()); + } + void World::scaleObject (const Ptr& ptr, float scale) { if (mPhysics->getActor(ptr)) @@ -1498,22 +1510,15 @@ namespace MWWorld return; } - float terrainHeight = -std::numeric_limits::max(); - if (ptr.getCell()->isExterior()) - terrainHeight = getTerrainHeightAt(pos); - - if (pos.z() < terrainHeight) - pos.z() = terrainHeight; - - pos.z() += 20; // place slightly above. will snap down to ground with code below + const float terrainHeight = ptr.getCell()->isExterior() ? getTerrainHeightAt(pos) : -std::numeric_limits::max(); + pos.z() = std::max(pos.z(), terrainHeight) + 20; // place slightly above terrain. will snap down to ground with code below // We still should trace down dead persistent actors - they do not use the "swimdeath" animation. bool swims = ptr.getClass().isActor() && isSwimming(ptr) && !(ptr.getClass().isPersistent(ptr) && ptr.getClass().getCreatureStats(ptr).isDeathAnimationFinished()); if (force || !ptr.getClass().isActor() || (!isFlying(ptr) && !swims && isActorCollisionEnabled(ptr))) { osg::Vec3f traced = mPhysics->traceDown(ptr, pos, Constants::CellSizeInUnits); - if (traced.z() < pos.z()) - pos.z() = traced.z(); + pos.z() = std::min(pos.z(), traced.z()); } moveObject(ptr, ptr.getCell(), pos.x(), pos.y(), pos.z()); @@ -1615,17 +1620,11 @@ namespace MWWorld ipos.pos[1] = spawnPoint.y(); ipos.pos[2] = spawnPoint.z(); - if (!referenceObject.getClass().isActor()) - { - ipos.rot[0] = referenceObject.getRefData().getPosition().rot[0]; - ipos.rot[1] = referenceObject.getRefData().getPosition().rot[1]; - } - else + if (referenceObject.getClass().isActor()) { ipos.rot[0] = 0; ipos.rot[1] = 0; } - ipos.rot[2] = referenceObject.getRefData().getPosition().rot[2]; MWWorld::Ptr placed = copyObjectToCell(ptr, referenceCell, ipos, ptr.getRefData().getCount(), false); adjustPosition(placed, true); // snap to ground @@ -1697,19 +1696,30 @@ namespace MWWorld mProjectileManager->update(duration); - const auto results = mPhysics->applyQueuedMovement(duration, mDiscardMovements, frameStart, frameNumber, stats); + const auto& results = mPhysics->applyQueuedMovement(duration, mDiscardMovements, frameStart, frameNumber, stats); + mProjectileManager->processHits(); mDiscardMovements = false; - for(const auto& [actor, position]: results) + for(const auto& actor : results) { // Handle player last, in case a cell transition occurs if(actor != getPlayerPtr()) + { + auto* physactor = mPhysics->getActor(actor); + assert(physactor); + const auto position = physactor->getSimulationPosition(); moveObjectImp(actor, position.x(), position.y(), position.z(), false); + } } - const auto player = results.find(getPlayerPtr()); + const auto player = std::find(results.begin(), results.end(), getPlayerPtr()); if (player != results.end()) - moveObjectImp(player->first, player->second.x(), player->second.y(), player->second.z(), false); + { + auto* physactor = mPhysics->getActor(*player); + assert(physactor); + const auto position = physactor->getSimulationPosition(); + moveObjectImp(*player, position.x(), position.y(), position.z(), false); + } } void World::updateNavigator() @@ -2548,8 +2558,12 @@ namespace MWWorld if (stats.isDead()) return false; + const bool isPlayer = ptr == getPlayerConstPtr(); + if (!(isPlayer && mGodMode) && stats.isParalyzed()) + return false; + if (ptr.getClass().canFly(ptr)) - return !stats.isParalyzed(); + return true; if(stats.getMagicEffects().get(ESM::MagicEffect::Levitate).getMagnitude() > 0 && isLevitationEnabled()) @@ -3309,7 +3323,7 @@ namespace MWWorld mRendering->rebuildPtr(getPlayerPtr()); } - bool World::getGodModeState() + bool World::getGodModeState() const { return mGodMode; } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 6ed7a7fe0..60a9e9d79 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -466,6 +466,9 @@ namespace MWWorld MWWorld::Ptr moveObject (const Ptr& ptr, CellStore* newCell, float x, float y, float z, bool movePhysics=true) override; ///< @return an updated Ptr + MWWorld::Ptr moveObjectBy(const Ptr& ptr, osg::Vec3f vec) override; + ///< @return an updated Ptr + void scaleObject (const Ptr& ptr, float scale) override; /// World rotates object, uses radians @@ -776,7 +779,7 @@ namespace MWWorld /// Returns true if levitation spell effect is allowed. bool isLevitationEnabled() const override; - bool getGodModeState() override; + bool getGodModeState() const override; bool toggleGodMode() override; diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index cd2d2e80a..5658a5eef 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -1,5 +1,5 @@ -find_package(GTest REQUIRED) -find_package(GMock REQUIRED) +find_package(GTest 1.10 REQUIRED) +find_package(GMock 1.10 REQUIRED) if (GTEST_FOUND AND GMOCK_FOUND) include_directories(SYSTEM ${GTEST_INCLUDE_DIRS}) diff --git a/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp b/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp index a3606f827..621db51a8 100644 --- a/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp +++ b/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp @@ -15,7 +15,7 @@ namespace struct DetourNavigatorRecastMeshObjectTest : Test { btBoxShape mBoxShape {btVector3(1, 2, 3)}; - btCompoundShape mCompoundShape {btVector3(1, 2, 3)}; + btCompoundShape mCompoundShape {true}; btTransform mTransform {btQuaternion(btVector3(1, 2, 3), 1), btVector3(1, 2, 3)}; DetourNavigatorRecastMeshObjectTest() diff --git a/apps/openmw_test_suite/mwworld/test_store.cpp b/apps/openmw_test_suite/mwworld/test_store.cpp index 77aaccfdd..f3b2bb3dc 100644 --- a/apps/openmw_test_suite/mwworld/test_store.cpp +++ b/apps/openmw_test_suite/mwworld/test_store.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -58,10 +59,10 @@ struct ContentFileTest : public ::testing::Test boost::program_options::options_description desc("Allowed options"); desc.add_options() - ("data", boost::program_options::value()->default_value(Files::PathContainer(), "data")->multitoken()->composing()) - ("content", boost::program_options::value >()->default_value(std::vector(), "") - ->multitoken(), "content file(s): esm/esp, or omwgame/omwaddon") - ("data-local", boost::program_options::value()->default_value("")); + ("data", boost::program_options::value()->default_value(Files::EscapePathContainer(), "data")->multitoken()->composing()) + ("content", boost::program_options::value()->default_value(Files::EscapeStringVector(), "") + ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon") + ("data-local", boost::program_options::value()->default_value(Files::EscapePath(), "")); boost::program_options::notify(variables); @@ -69,12 +70,12 @@ struct ContentFileTest : public ::testing::Test Files::PathContainer dataDirs, dataLocal; if (!variables["data"].empty()) { - dataDirs = Files::PathContainer(variables["data"].as()); + dataDirs = Files::EscapePath::toPathContainer(variables["data"].as()); } - std::string local = variables["data-local"].as(); + Files::PathContainer::value_type local(variables["data-local"].as().mPath); if (!local.empty()) { - dataLocal.push_back(Files::PathContainer::value_type(local)); + dataLocal.push_back(local); } mConfigurationManager.processPaths (dataDirs); @@ -85,7 +86,7 @@ struct ContentFileTest : public ::testing::Test Files::Collections collections (dataDirs, true); - std::vector contentFiles = variables["content"].as >(); + std::vector contentFiles = variables["content"].as().toStdStringVector(); for (auto & contentFile : contentFiles) mContentFiles.push_back(collections.getPath(contentFile)); } diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp index 7da8f0fd5..a1f5d6091 100644 --- a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp +++ b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp @@ -162,8 +162,8 @@ namespace Resource { return compareObjects(lhs.mCollisionShape, rhs.mCollisionShape) && compareObjects(lhs.mAvoidCollisionShape, rhs.mAvoidCollisionShape) - && lhs.mCollisionBoxHalfExtents == rhs.mCollisionBoxHalfExtents - && lhs.mCollisionBoxTranslate == rhs.mCollisionBoxTranslate + && lhs.mCollisionBox.extents == rhs.mCollisionBox.extents + && lhs.mCollisionBox.center == rhs.mCollisionBox.center && lhs.mAnimatedShapes == rhs.mAnimatedShapes; } @@ -172,7 +172,8 @@ namespace Resource return stream << "Resource::BulletShape {" << value.mCollisionShape << ", " << value.mAvoidCollisionShape << ", " - << "osg::Vec3f {" << value.mCollisionBoxHalfExtents << "}" << ", " + << "osg::Vec3f {" << value.mCollisionBox.extents << "}" << ", " + << "osg::Vec3f {" << value.mCollisionBox.center << "}" << ", " << value.mAnimatedShapes << "}"; } @@ -258,14 +259,19 @@ namespace value.isBone = false; } - void init(Nif::NiTriShape& value) + void init(Nif::NiGeometry& value) { init(static_cast(value)); - value.recType = Nif::RC_NiTriShape; - value.data = Nif::NiTriShapeDataPtr(nullptr); + value.data = Nif::NiGeometryDataPtr(nullptr); value.skin = Nif::NiSkinInstancePtr(nullptr); } + void init(Nif::NiTriShape& value) + { + init(static_cast(value)); + value.recType = Nif::RC_NiTriShape; + } + void init(Nif::NiSkinInstance& value) { value.data = Nif::NiSkinDataPtr(nullptr); @@ -293,22 +299,22 @@ namespace struct NifFileMock : Nif::File { - MOCK_CONST_METHOD1(getRecord, Nif::Record* (std::size_t)); - MOCK_CONST_METHOD0(numRecords, std::size_t ()); - MOCK_CONST_METHOD1(getRoot, Nif::Record* (std::size_t)); - MOCK_CONST_METHOD0(numRoots, std::size_t ()); - MOCK_CONST_METHOD1(getString, std::string (uint32_t)); - MOCK_METHOD1(setUseSkinning, void (bool)); - MOCK_CONST_METHOD0(getUseSkinning, bool ()); - MOCK_CONST_METHOD0(getFilename, std::string ()); - MOCK_CONST_METHOD0(getVersion, unsigned int ()); - MOCK_CONST_METHOD0(getUserVersion, unsigned int ()); - MOCK_CONST_METHOD0(getBethVersion, unsigned int ()); + MOCK_METHOD(Nif::Record*, getRecord, (std::size_t), (const, override)); + MOCK_METHOD(std::size_t, numRecords, (), (const, override)); + MOCK_METHOD(Nif::Record*, getRoot, (std::size_t), (const, override)); + MOCK_METHOD(std::size_t, numRoots, (), (const, override)); + MOCK_METHOD(std::string, getString, (uint32_t), (const, override)); + MOCK_METHOD(void, setUseSkinning, (bool), (override)); + MOCK_METHOD(bool, getUseSkinning, (), (const, override)); + MOCK_METHOD(std::string, getFilename, (), (const, override)); + MOCK_METHOD(unsigned int, getVersion, (), (const, override)); + MOCK_METHOD(unsigned int, getUserVersion, (), (const, override)); + MOCK_METHOD(unsigned int, getBethVersion, (), (const, override)); }; struct RecordMock : Nif::Record { - MOCK_METHOD1(read, void (Nif::NIFStream *nif)); + MOCK_METHOD(void, read, (Nif::NIFStream *nif), (override)); }; struct TestBulletNifLoader : Test @@ -360,13 +366,15 @@ namespace init(mNiStringExtraData2); init(mController); + mNiTriShapeData.recType = Nif::RC_NiTriShapeData; mNiTriShapeData.vertices = {osg::Vec3f(0, 0, 0), osg::Vec3f(1, 0, 0), osg::Vec3f(1, 1, 0)}; mNiTriShapeData.triangles = {0, 1, 2}; - mNiTriShape.data = Nif::NiTriShapeDataPtr(&mNiTriShapeData); + mNiTriShape.data = Nif::NiGeometryDataPtr(&mNiTriShapeData); + mNiTriShapeData2.recType = Nif::RC_NiTriShapeData; mNiTriShapeData2.vertices = {osg::Vec3f(0, 0, 1), osg::Vec3f(1, 0, 1), osg::Vec3f(1, 1, 1)}; mNiTriShapeData2.triangles = {0, 1, 2}; - mNiTriShape2.data = Nif::NiTriShapeDataPtr(&mNiTriShapeData2); + mNiTriShape2.data = Nif::NiGeometryDataPtr(&mNiTriShapeData2); } }; @@ -433,8 +441,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBoxHalfExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBoxTranslate = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); @@ -458,8 +466,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBoxHalfExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBoxTranslate = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); @@ -488,8 +496,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBoxHalfExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBoxTranslate = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); @@ -523,8 +531,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBoxHalfExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBoxTranslate = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); @@ -558,8 +566,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBoxHalfExtents = osg::Vec3f(4, 5, 6); - expected.mCollisionBoxTranslate = osg::Vec3f(-4, -5, -6); + expected.mCollisionBox.extents = osg::Vec3f(4, 5, 6); + expected.mCollisionBox.center = osg::Vec3f(-4, -5, -6); std::unique_ptr box(new btBoxShape(btVector3(4, 5, 6))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-4, -5, -6)), box.release()); @@ -581,8 +589,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBoxHalfExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBoxTranslate = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); EXPECT_EQ(*result, expected); } @@ -615,8 +623,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBoxHalfExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBoxTranslate = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); EXPECT_EQ(*result, expected); } @@ -872,7 +880,7 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_empty_data_should_return_shape_with_null_collision_shape) { - mNiTriShape.data = Nif::NiTriShapeDataPtr(nullptr); + mNiTriShape.data = Nif::NiGeometryDataPtr(nullptr); mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -887,7 +895,8 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_empty_data_triangles_should_return_shape_with_null_collision_shape) { - mNiTriShape.data->triangles.clear(); + auto data = static_cast(mNiTriShape.data.getPtr()); + data->triangles.clear(); mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 160c6f721..d92f5b029 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -157,6 +157,8 @@ void Wizard::MainWizard::setupGameSettings() mGameSettings.readUserFile(stream); } + file.close(); + // Now the rest QStringList paths; paths.append(userPath + QLatin1String("openmw.cfg")); diff --git a/appveyor.yml b/appveyor.yml index e2c13ed94..ed6f727be 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -50,7 +50,7 @@ install: before_build: - cmd: git submodule update --init --recursive - - cmd: sh %APPVEYOR_BUILD_FOLDER%\CI\before_script.msvc.sh -c %configuration% -p %PLATFORM% -v %msvc% -V -i %APPVEYOR_BUILD_FOLDER%\install + - cmd: sh %APPVEYOR_BUILD_FOLDER%\CI\before_script.msvc.sh -c %configuration% -p %PLATFORM% -v %msvc% -V -i %APPVEYOR_BUILD_FOLDER%\install -D build_script: - cmd: if %PLATFORM%==Win32 set build=MSVC%msvc%_32 diff --git a/cmake/OSIdentity.cmake b/cmake/OSIdentity.cmake new file mode 100644 index 000000000..40b7b2089 --- /dev/null +++ b/cmake/OSIdentity.cmake @@ -0,0 +1,67 @@ +if (UNIX) + + if (APPLE) + + set(CMAKE_OS_NAME "OSX" CACHE STRING "Operating system name" FORCE) + + else (APPLE) + + ## Check for Debian GNU/Linux ________________ + + find_file(DEBIAN_FOUND debian_version debconf.conf + PATHS /etc + ) + if (DEBIAN_FOUND) + set(CMAKE_OS_NAME "Debian" CACHE STRING "Operating system name" FORCE) + endif (DEBIAN_FOUND) + + ## Check for Fedora _________________________ + + find_file(FEDORA_FOUND fedora-release + PATHS /etc + ) + if (FEDORA_FOUND) + set(CMAKE_OS_NAME "Fedora" CACHE STRING "Operating system name" FORCE) + endif (FEDORA_FOUND) + + ## Check for RedHat _________________________ + + find_file(REDHAT_FOUND redhat-release inittab.RH + PATHS /etc + ) + if (REDHAT_FOUND) + set(CMAKE_OS_NAME "RedHat" CACHE STRING "Operating system name" FORCE) + endif (REDHAT_FOUND) + + ## Extra check for Ubuntu ____________________ + + if (DEBIAN_FOUND) + + ## At its core Ubuntu is a Debian system, with + ## a slightly altered configuration; hence from + ## a first superficial inspection a system will + ## be considered as Debian, which signifies an + ## extra check is required. + + find_file(UBUNTU_EXTRA legal issue + PATHS /etc + ) + + if (UBUNTU_EXTRA) + ## Scan contents of file + file(STRINGS ${UBUNTU_EXTRA} UBUNTU_FOUND + REGEX Ubuntu + ) + ## Check result of string search + if (UBUNTU_FOUND) + set(CMAKE_OS_NAME "Ubuntu" CACHE STRING "Operating system name" FORCE) + set(DEBIAN_FOUND FALSE) + endif (UBUNTU_FOUND) + + endif (UBUNTU_EXTRA) + + endif (DEBIAN_FOUND) + + endif (APPLE) + +endif (UNIX) diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index e9db49037..1171338c7 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -224,13 +224,23 @@ add_component_dir (fallback fallback validate ) -if(NOT WIN32 AND NOT ANDROID) - if (BUILD_OPENMW OR BUILD_OPENCS) +# Start of tes3mp change (major) +# +# Don't require the crashcatcher when building the server +if (BUILD_OPENMW OR BUILD_OPENCS) + if(WIN32) + add_component_dir (crashcatcher + windows_crashcatcher + windows_crashmonitor + windows_crashshm + ) + elseif(NOT ANDROID) add_component_dir (crashcatcher crashcatcher ) endif() endif() +# End of tes3mp change (major) if (BUILD_OPENMW OR BUILD_OPENCS) add_component_dir(detournavigator @@ -309,19 +319,28 @@ target_link_libraries(components ${OSGGA_LIBRARIES} ${OSGSHADOW_LIBRARIES} ${OSGANIMATION_LIBRARIES} - ${BULLET_LIBRARIES} ${SDL2_LIBRARIES} ${OPENGL_gl_LIBRARY} ${MyGUI_LIBRARIES} LZ4::LZ4 ) +# Start of tes3mp change (major) +# +# Don't require RecastNavigation when building the server if (BUILD_OPENMW OR BUILD_OPENCS) target_link_libraries(components RecastNavigation::DebugUtils RecastNavigation::Detour RecastNavigation::Recast) endif () +# End of tes3mp change (major) + +if (BULLET_USE_DOUBLES AND (UBUNTU_FOUND OR DEBIAN_FOUND)) + target_link_libraries(components BulletCollision-float64 LinearMath-float64) +else() + target_link_libraries(components ${BULLET_LIBRARIES}) +endif() if (WIN32) target_link_libraries(components diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index 85c090a3f..d7fe7da94 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -14,8 +14,6 @@ Config::GameSettings::GameSettings(Files::ConfigurationManager &cfg) { } -Config::GameSettings::~GameSettings() = default; - void Config::GameSettings::validatePaths() { QStringList paths = mSettings.values(QString("data")); diff --git a/components/config/gamesettings.hpp b/components/config/gamesettings.hpp index ccb1d5fd2..263a151c9 100644 --- a/components/config/gamesettings.hpp +++ b/components/config/gamesettings.hpp @@ -21,7 +21,6 @@ namespace Config { public: GameSettings(Files::ConfigurationManager &cfg); - ~GameSettings(); inline QString value(const QString &key, const QString &defaultValue = QString()) { @@ -54,11 +53,11 @@ namespace Config mUserSettings.remove(key); } - inline QStringList getDataDirs() { return mDataDirs; } + inline QStringList getDataDirs() const { return mDataDirs; } inline void removeDataDir(const QString &dir) { if(!dir.isEmpty()) mDataDirs.removeAll(dir); } inline void addDataDir(const QString &dir) { if(!dir.isEmpty()) mDataDirs.append(dir); } - inline QString getDataLocal() {return mDataLocal; } + inline QString getDataLocal() const {return mDataLocal; } bool hasMaster(); diff --git a/components/config/launchersettings.cpp b/components/config/launchersettings.cpp index ff2c38438..025bc4382 100644 --- a/components/config/launchersettings.cpp +++ b/components/config/launchersettings.cpp @@ -12,10 +12,6 @@ const char Config::LauncherSettings::sLauncherConfigFileName[] = "launcher.cfg"; const char Config::LauncherSettings::sContentListsSectionPrefix[] = "Profiles/"; const char Config::LauncherSettings::sContentListSuffix[] = "/content"; -Config::LauncherSettings::LauncherSettings() = default; - -Config::LauncherSettings::~LauncherSettings() = default; - QStringList Config::LauncherSettings::subKeys(const QString &key) { QMultiMap settings = SettingsBase::getSettings(); diff --git a/components/config/launchersettings.hpp b/components/config/launchersettings.hpp index 1483052bb..da492c85c 100644 --- a/components/config/launchersettings.hpp +++ b/components/config/launchersettings.hpp @@ -9,9 +9,6 @@ namespace Config class LauncherSettings : public SettingsBase > { public: - LauncherSettings(); - ~LauncherSettings(); - bool writeFile(QTextStream &stream); /// \return names of all Content Lists in the launcher's .cfg file. diff --git a/components/crashcatcher/windows_crashcatcher.cpp b/components/crashcatcher/windows_crashcatcher.cpp new file mode 100644 index 000000000..4c3dfa8f6 --- /dev/null +++ b/components/crashcatcher/windows_crashcatcher.cpp @@ -0,0 +1,205 @@ +#include +#include +#include +#include +#include + +#include "windows_crashcatcher.hpp" +#include "windows_crashmonitor.hpp" +#include "windows_crashshm.hpp" +#include + +namespace Crash +{ + + HANDLE duplicateHandle(HANDLE handle) + { + HANDLE duplicate; + if (!DuplicateHandle(GetCurrentProcess(), handle, + GetCurrentProcess(), &duplicate, + 0, TRUE, DUPLICATE_SAME_ACCESS)) + { + throw std::runtime_error("Crash monitor could not duplicate handle"); + } + return duplicate; + } + + CrashCatcher* CrashCatcher::sInstance = nullptr; + + CrashCatcher::CrashCatcher(int argc, char **argv, const std::string& crashLogPath) + { + assert(sInstance == nullptr); // don't allow two instances + + sInstance = this; + + HANDLE shmHandle = nullptr; + for (int i=0; i= argc - 1) + throw std::runtime_error("Crash monitor is missing the SHM handle argument"); + + sscanf(argv[i + 1], "%p", &shmHandle); + break; + } + + if (!shmHandle) + { + setupIpc(); + startMonitorProcess(crashLogPath); + installHandler(); + } + else + { + CrashMonitor(shmHandle).run(); + exit(0); + } + } + + CrashCatcher::~CrashCatcher() + { + sInstance = nullptr; + + if (mShm && mSignalMonitorEvent) + { + shmLock(); + mShm->mEvent = CrashSHM::Event::Shutdown; + shmUnlock(); + + SetEvent(mSignalMonitorEvent); + } + + if (mShmHandle) + CloseHandle(mShmHandle); + } + + void CrashCatcher::setupIpc() + { + SECURITY_ATTRIBUTES attributes; + ZeroMemory(&attributes, sizeof(attributes)); + attributes.bInheritHandle = TRUE; + + mSignalAppEvent = CreateEventW(&attributes, FALSE, FALSE, NULL); + mSignalMonitorEvent = CreateEventW(&attributes, FALSE, FALSE, NULL); + + mShmHandle = CreateFileMappingW(INVALID_HANDLE_VALUE, &attributes, PAGE_READWRITE, HIWORD(sizeof(CrashSHM)), LOWORD(sizeof(CrashSHM)), NULL); + if (mShmHandle == nullptr) + throw std::runtime_error("Failed to allocate crash catcher shared memory"); + + mShm = reinterpret_cast(MapViewOfFile(mShmHandle, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(CrashSHM))); + if (mShm == nullptr) + throw std::runtime_error("Failed to map crash catcher shared memory"); + + mShmMutex = CreateMutexW(&attributes, FALSE, NULL); + if (mShmMutex == nullptr) + throw std::runtime_error("Failed to create crash catcher shared memory mutex"); + } + + void CrashCatcher::shmLock() + { + if (WaitForSingleObject(mShmMutex, CrashCatcherTimeout) != WAIT_OBJECT_0) + throw std::runtime_error("SHM lock timed out"); + } + + void CrashCatcher::shmUnlock() + { + ReleaseMutex(mShmMutex); + } + + void CrashCatcher::waitMonitor() + { + if (WaitForSingleObject(mSignalAppEvent, CrashCatcherTimeout) != WAIT_OBJECT_0) + throw std::runtime_error("Waiting for monitor failed"); + } + + void CrashCatcher::signalMonitor() + { + SetEvent(mSignalMonitorEvent); + } + + void CrashCatcher::installHandler() + { + SetUnhandledExceptionFilter(vectoredExceptionHandler); + } + + void CrashCatcher::startMonitorProcess(const std::string& crashLogPath) + { + std::wstring executablePath; + DWORD copied = 0; + do { + executablePath.resize(executablePath.size() + MAX_PATH); + copied = GetModuleFileNameW(nullptr, executablePath.data(), executablePath.size()); + } while (copied >= executablePath.size()); + executablePath.resize(copied); + + memset(mShm->mStartup.mLogFilePath, 0, sizeof(mShm->mStartup.mLogFilePath)); + int length = crashLogPath.length(); + if (length > MAX_LONG_PATH) length = MAX_LONG_PATH; + strncpy(mShm->mStartup.mLogFilePath, crashLogPath.c_str(), length); + mShm->mStartup.mLogFilePath[length] = '\0'; + + // note that we don't need to lock the SHM here, the other process has not started yet + mShm->mEvent = CrashSHM::Event::Startup; + mShm->mStartup.mShmMutex = duplicateHandle(mShmMutex); + mShm->mStartup.mAppProcessHandle = duplicateHandle(GetCurrentProcess()); + mShm->mStartup.mSignalApp = duplicateHandle(mSignalAppEvent); + mShm->mStartup.mSignalMonitor = duplicateHandle(mSignalMonitorEvent); + + std::wstringstream ss; + ss << "--crash-monitor " << std::hex << duplicateHandle(mShmHandle); + std::wstring arguments(ss.str()); + + STARTUPINFOW si; + ZeroMemory(&si, sizeof(si)); + + PROCESS_INFORMATION pi; + ZeroMemory(&pi, sizeof(pi)); + + if (!CreateProcessW(executablePath.data(), arguments.data(), NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) + throw std::runtime_error("Could not start crash monitor process"); + + waitMonitor(); + } + + LONG CrashCatcher::vectoredExceptionHandler(PEXCEPTION_POINTERS info) + { + switch (info->ExceptionRecord->ExceptionCode) + { + case EXCEPTION_SINGLE_STEP: + case EXCEPTION_BREAKPOINT: + case DBG_PRINTEXCEPTION_C: + return EXCEPTION_EXECUTE_HANDLER; + } + if (!sInstance) + return EXCEPTION_EXECUTE_HANDLER; + + sInstance->handleVectoredException(info); + + _Exit(1); + + return EXCEPTION_CONTINUE_SEARCH; + } + + void CrashCatcher::handleVectoredException(PEXCEPTION_POINTERS info) + { + shmLock(); + + mShm->mEvent = CrashSHM::Event::Crashed; + mShm->mCrashed.mThreadId = GetCurrentThreadId(); + mShm->mCrashed.mContext = *info->ContextRecord; + mShm->mCrashed.mExceptionRecord = *info->ExceptionRecord; + + shmUnlock(); + + signalMonitor(); + + // must remain until monitor has finished + waitMonitor(); + + std::string message = "OpenMW has encountered a fatal error.\nCrash log saved to '" + std::string(mShm->mStartup.mLogFilePath) + "'.\n Please report this to https://gitlab.com/OpenMW/openmw/issues !"; + SDL_ShowSimpleMessageBox(0, "Fatal Error", message.c_str(), nullptr); + } + +} // namespace Crash diff --git a/components/crashcatcher/windows_crashcatcher.hpp b/components/crashcatcher/windows_crashcatcher.hpp new file mode 100644 index 000000000..e1857271e --- /dev/null +++ b/components/crashcatcher/windows_crashcatcher.hpp @@ -0,0 +1,79 @@ +#ifndef WINDOWS_CRASHCATCHER_HPP +#define WINDOWS_CRASHCATCHER_HPP + +#include + +#undef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#include + +#include + +namespace Crash +{ + + // The implementation spawns the current executable as a monitor process which waits + // for a global synchronization event which is sent when the parent process crashes. + // The monitor process then extracts crash information from the parent process while + // the parent process waits for the monitor process to finish. The crashed process + // quits and the monitor writes the crash information to a file. + // + // To detect unexpected shutdowns of the application which are not handled by the + // crash handler, the monitor periodically checks the exit code of the parent + // process and exits if it does not return STILL_ACTIVE. You can test this by closing + // the main openmw process in task manager. + + static constexpr const int CrashCatcherTimeout = 2500; + + struct CrashSHM; + + class CrashCatcher final + { + public: + + CrashCatcher(int argc, char **argv, const std::string& crashLogPath); + ~CrashCatcher(); + + private: + + static CrashCatcher* sInstance; + + // mapped SHM area + CrashSHM* mShm = nullptr; + // the handle is allocated by the catcher and passed to the monitor + // process via the command line which maps the SHM and sends / receives + // events through it + HANDLE mShmHandle = nullptr; + // mutex which guards SHM area + HANDLE mShmMutex = nullptr; + + // triggered when the monitor signals the application + HANDLE mSignalAppEvent = INVALID_HANDLE_VALUE; + + // triggered when the application wants to wake the monitor process + HANDLE mSignalMonitorEvent = INVALID_HANDLE_VALUE; + + void setupIpc(); + + void shmLock(); + + void shmUnlock(); + + void startMonitorProcess(const std::string& crashLogPath); + + void waitMonitor(); + + void signalMonitor(); + + void installHandler(); + + void handleVectoredException(PEXCEPTION_POINTERS info); + + public: + + static LONG WINAPI vectoredExceptionHandler(PEXCEPTION_POINTERS info); + }; + +} // namespace Crash + +#endif // WINDOWS_CRASHCATCHER_HPP diff --git a/components/crashcatcher/windows_crashmonitor.cpp b/components/crashcatcher/windows_crashmonitor.cpp new file mode 100644 index 000000000..8976deb2e --- /dev/null +++ b/components/crashcatcher/windows_crashmonitor.cpp @@ -0,0 +1,188 @@ +#undef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include + +#include +#include +#include + +#include "windows_crashcatcher.hpp" +#include "windows_crashmonitor.hpp" +#include "windows_crashshm.hpp" +#include + +namespace Crash +{ + + CrashMonitor::CrashMonitor(HANDLE shmHandle) + : mShmHandle(shmHandle) + { + mShm = reinterpret_cast(MapViewOfFile(mShmHandle, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(CrashSHM))); + if (mShm == nullptr) + throw std::runtime_error("Failed to map crash monitor shared memory"); + + // accessing SHM without lock is OK here, the parent waits for a signal before continuing + + mShmMutex = mShm->mStartup.mShmMutex; + mAppProcessHandle = mShm->mStartup.mAppProcessHandle; + mSignalAppEvent = mShm->mStartup.mSignalApp; + mSignalMonitorEvent = mShm->mStartup.mSignalMonitor; + } + + CrashMonitor::~CrashMonitor() + { + if (mShm) + UnmapViewOfFile(mShm); + + // the handles received from the app are duplicates, we must close them + + if (mShmHandle) + CloseHandle(mShmHandle); + + if (mShmMutex) + CloseHandle(mShmMutex); + + if (mSignalAppEvent) + CloseHandle(mSignalAppEvent); + + if (mSignalMonitorEvent) + CloseHandle(mSignalMonitorEvent); + } + + void CrashMonitor::shmLock() + { + if (WaitForSingleObject(mShmMutex, CrashCatcherTimeout) != WAIT_OBJECT_0) + throw std::runtime_error("SHM monitor lock timed out"); + } + + void CrashMonitor::shmUnlock() + { + ReleaseMutex(mShmMutex); + } + + void CrashMonitor::signalApp() const + { + SetEvent(mSignalAppEvent); + } + + bool CrashMonitor::waitApp() const + { + return WaitForSingleObject(mSignalMonitorEvent, CrashCatcherTimeout) == WAIT_OBJECT_0; + } + + bool CrashMonitor::isAppAlive() const + { + DWORD code = 0; + GetExitCodeProcess(mAppProcessHandle, &code); + return code == STILL_ACTIVE; + } + + void CrashMonitor::run() + { + try + { + // app waits for monitor start up, let it continue + signalApp(); + + bool running = true; + while (isAppAlive() && running) + { + if (waitApp()) + { + shmLock(); + + switch (mShm->mEvent) + { + case CrashSHM::Event::None: + break; + case CrashSHM::Event::Crashed: + handleCrash(); + running = false; + break; + case CrashSHM::Event::Shutdown: + running = false; + break; + case CrashSHM::Event::Startup: + break; + } + + shmUnlock(); + } + } + + } + catch (...) + { + Log(Debug::Error) << "Exception in crash monitor, exiting"; + } + signalApp(); + } + + std::wstring utf8ToUtf16(const std::string& utf8) + { + const int nLenWide = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), utf8.size(), nullptr, 0); + + std::wstring utf16; + utf16.resize(nLenWide); + if (MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), utf8.size(), utf16.data(), nLenWide) != nLenWide) + return {}; + + return utf16; + } + + void CrashMonitor::handleCrash() + { + DWORD processId = GetProcessId(mAppProcessHandle); + + try + { + HMODULE dbghelp = LoadLibraryA("dbghelp.dll"); + if (dbghelp == NULL) + return; + + using MiniDumpWirteDumpFn = BOOL (WINAPI*)( + HANDLE hProcess, DWORD ProcessId, HANDLE hFile, MINIDUMP_TYPE DumpType, PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, + PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, PMINIDUMP_CALLBACK_INFORMATION CallbackParam + ); + + MiniDumpWirteDumpFn miniDumpWriteDump = (MiniDumpWirteDumpFn)GetProcAddress(dbghelp, "MiniDumpWriteDump"); + if (miniDumpWriteDump == NULL) + return; + + std::wstring utf16Path = utf8ToUtf16(mShm->mStartup.mLogFilePath); + if (utf16Path.empty()) + return; + + if (utf16Path.length() > MAX_PATH) + utf16Path = LR"(\\?\)" + utf16Path; + + HANDLE hCrashLog = CreateFileW(utf16Path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hCrashLog == NULL || hCrashLog == INVALID_HANDLE_VALUE) + return; + if (auto err = GetLastError(); err != ERROR_ALREADY_EXISTS && err != 0) + return; + + EXCEPTION_POINTERS exp; + exp.ContextRecord = &mShm->mCrashed.mContext; + exp.ExceptionRecord = &mShm->mCrashed.mExceptionRecord; + MINIDUMP_EXCEPTION_INFORMATION infos = {}; + infos.ThreadId = mShm->mCrashed.mThreadId; + infos.ExceptionPointers = &exp; + infos.ClientPointers = FALSE; + MINIDUMP_TYPE type = (MINIDUMP_TYPE)(MiniDumpWithDataSegs | MiniDumpWithHandleData); + miniDumpWriteDump(mAppProcessHandle, processId, hCrashLog, type, &infos, 0, 0); + } + catch (const std::exception&e) + { + Log(Debug::Error) << "CrashMonitor: " << e.what(); + } + catch (...) + { + Log(Debug::Error) << "CrashMonitor: unknown exception"; + } + } + +} // namespace Crash diff --git a/components/crashcatcher/windows_crashmonitor.hpp b/components/crashcatcher/windows_crashmonitor.hpp new file mode 100644 index 000000000..678d38435 --- /dev/null +++ b/components/crashcatcher/windows_crashmonitor.hpp @@ -0,0 +1,49 @@ +#ifndef WINDOWS_CRASHMONITOR_HPP +#define WINDOWS_CRASHMONITOR_HPP + +#include + +namespace Crash +{ + +struct CrashSHM; + +class CrashMonitor final +{ +public: + + CrashMonitor(HANDLE shmHandle); + + ~CrashMonitor(); + + void run(); + +private: + + HANDLE mAppProcessHandle = nullptr; + + // triggered when the monitor process wants to wake the parent process (received via SHM) + HANDLE mSignalAppEvent = nullptr; + // triggered when the application wants to wake the monitor process (received via SHM) + HANDLE mSignalMonitorEvent = nullptr; + + CrashSHM* mShm = nullptr; + HANDLE mShmHandle = nullptr; + HANDLE mShmMutex = nullptr; + + void signalApp() const; + + bool waitApp() const; + + bool isAppAlive() const; + + void shmLock(); + + void shmUnlock(); + + void handleCrash(); +}; + +} // namespace Crash + +#endif // WINDOWS_CRASHMONITOR_HPP diff --git a/components/crashcatcher/windows_crashshm.hpp b/components/crashcatcher/windows_crashshm.hpp new file mode 100644 index 000000000..47929a45f --- /dev/null +++ b/components/crashcatcher/windows_crashshm.hpp @@ -0,0 +1,45 @@ +#ifndef WINDOWS_CRASHSHM_HPP +#define WINDOWS_CRASHSHM_HPP + +#undef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#include + +namespace Crash +{ + + // Used to communicate between the app and the monitor, fields are is overwritten with each event. + static constexpr const int MAX_LONG_PATH = 0x7fff; + + struct CrashSHM + { + enum class Event + { + None, + Startup, + Crashed, + Shutdown + }; + + Event mEvent; + + struct Startup + { + HANDLE mAppProcessHandle; + HANDLE mSignalApp; + HANDLE mSignalMonitor; + HANDLE mShmMutex; + char mLogFilePath[MAX_LONG_PATH]; + } mStartup; + + struct Crashed + { + DWORD mThreadId; + CONTEXT mContext; + EXCEPTION_RECORD mExceptionRecord; + } mCrashed; + }; + +} // namespace Crash + +#endif // WINDOWS_CRASHSHM_HPP diff --git a/components/debug/debugging.cpp b/components/debug/debugging.cpp index c4f3af307..e9bcf8ad7 100644 --- a/components/debug/debugging.cpp +++ b/components/debug/debugging.cpp @@ -1,8 +1,13 @@ #include "debugging.hpp" +#include +#include +#include + #include #ifdef _WIN32 +# include # undef WIN32_LEAN_AND_MEAN # define WIN32_LEAN_AND_MEAN # include @@ -60,15 +65,40 @@ namespace Debug std::streamsize DebugOutputBase::write(const char *str, std::streamsize size) { + if (size <= 0) + return size; + std::string_view msg{str, size_t(size)}; + // Skip debug level marker Level level = getLevelMarker(str); if (level != NoLevel) + msg = msg.substr(1); + + char prefix[32]; + int prefixSize; { - writeImpl(str+1, size-1, level); - return size; + prefix[0] = '['; + uint64_t ms = std::chrono::duration_cast( + std::chrono::high_resolution_clock::now().time_since_epoch()).count(); + std::time_t t = ms / 1000; + prefixSize = std::strftime(prefix + 1, sizeof(prefix) - 1, "%T", std::localtime(&t)) + 1; + char levelLetter = " EWIVD*"[int(level)]; + prefixSize += snprintf(prefix + prefixSize, sizeof(prefix) - prefixSize, + ".%03u %c] ", static_cast(ms % 1000), levelLetter); + } + + while (!msg.empty()) + { + if (msg[0] == 0) + break; + size_t lineSize = 1; + while (lineSize < msg.size() && msg[lineSize - 1] != '\n') + lineSize++; + writeImpl(prefix, prefixSize, level); + writeImpl(msg.data(), lineSize, level); + msg = msg.substr(lineSize); } - writeImpl(str, size, NoLevel); return size; } @@ -106,11 +136,19 @@ namespace Debug } } +static std::unique_ptr rawStdout = nullptr; + +std::ostream& getRawStdout() +{ + return rawStdout ? *rawStdout : std::cout; +} + int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, char *argv[], const std::string& appName) { #if defined _WIN32 (void)Debug::attachParentConsole(); #endif + rawStdout = std::make_unique(std::cout.rdbuf()); // Some objects used to redirect cout and cerr // Scope must be here, so this still works inside the catch block for logging exceptions @@ -127,7 +165,6 @@ int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, c #endif const std::string logName = Misc::StringUtils::lowerCase(appName) + ".log"; - const std::string crashLogName = Misc::StringUtils::lowerCase(appName) + "-crash.log"; boost::filesystem::ofstream logfile; int ret = 0; @@ -151,13 +188,18 @@ int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, c std::cerr.rdbuf (&cerrsb); #endif +#if defined(_WIN32) + const std::string crashLogName = Misc::StringUtils::lowerCase(appName) + "-crash.dmp"; + Crash::CrashCatcher crashy(argc, argv, (cfgMgr.getLogPath() / crashLogName).make_preferred().string()); +#else + const std::string crashLogName = Misc::StringUtils::lowerCase(appName) + "-crash.log"; // install the crash handler as soon as possible. note that the log path // does not depend on config being read. crashCatcherInstall(argc, argv, (cfgMgr.getLogPath() / crashLogName).string()); - +#endif ret = innerApplication(argc, argv); } - catch (std::exception& e) + catch (const std::exception& e) { #if (defined(__APPLE__) || defined(__linux) || defined(__unix) || defined(__posix)) if (!isatty(fileno(stdin))) @@ -172,6 +214,7 @@ int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, c // Restore cout and cerr std::cout.rdbuf(cout_rdbuf); std::cerr.rdbuf(cerr_rdbuf); + Debug::CurrentDebugLevel = Debug::NoLevel; return ret; } diff --git a/components/debug/debugging.hpp b/components/debug/debugging.hpp index 39390446f..d8849cd89 100644 --- a/components/debug/debugging.hpp +++ b/components/debug/debugging.hpp @@ -135,6 +135,9 @@ namespace Debug #endif } +// Can be used to print messages without timestamps +std::ostream& getRawStdout(); + int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, char *argv[], const std::string& appName); #endif diff --git a/components/nif/base.hpp b/components/nif/base.hpp index 711272abb..022e5224a 100644 --- a/components/nif/base.hpp +++ b/components/nif/base.hpp @@ -11,9 +11,8 @@ namespace Nif { // An extra data record. All the extra data connected to an object form a linked list. -class Extra : public Record +struct Extra : public Record { -public: std::string name; ExtraPtr next; // Next extra data record in the list @@ -31,9 +30,8 @@ public: void post(NIFFile *nif) override { next.post(nif); } }; -class Controller : public Record +struct Controller : public Record { -public: ControllerPtr next; int flags; float frequency, phase; @@ -45,9 +43,8 @@ public: }; /// Has name, extra-data and controller -class Named : public Record +struct Named : public Record { -public: std::string name; ExtraPtr extra; ExtraList extralist; diff --git a/components/nif/controlled.cpp b/components/nif/controlled.cpp index ab2b8dc17..c8116ce74 100644 --- a/components/nif/controlled.cpp +++ b/components/nif/controlled.cpp @@ -47,6 +47,11 @@ namespace Nif data.post(nif); } + void BSShaderTextureSet::read(NIFStream *nif) + { + nif->getSizedStrings(textures, nif->getUInt()); + } + void NiParticleModifier::read(NIFStream *nif) { next.read(nif); diff --git a/components/nif/controlled.hpp b/components/nif/controlled.hpp index 57d538f83..0d93381ea 100644 --- a/components/nif/controlled.hpp +++ b/components/nif/controlled.hpp @@ -29,9 +29,8 @@ namespace Nif { -class NiSourceTexture : public Named +struct NiSourceTexture : public Named { -public: // Is this an external (references a separate texture file) or // internal (data is inside the nif itself) texture? bool external; @@ -66,6 +65,24 @@ public: void post(NIFFile *nif) override; }; +struct BSShaderTextureSet : public Record +{ + enum TextureType + { + TextureType_Base = 0, + TextureType_Normal = 1, + TextureType_Glow = 2, + TextureType_Parallax = 3, + TextureType_Env = 4, + TextureType_EnvMask = 5, + TextureType_Subsurface = 6, + TextureType_BackLighting = 7 + }; + std::vector textures; + + void read(NIFStream *nif) override; +}; + struct NiParticleModifier : public Record { NiParticleModifierPtr next; @@ -75,27 +92,24 @@ struct NiParticleModifier : public Record void post(NIFFile *nif) override; }; -class NiParticleGrowFade : public NiParticleModifier +struct NiParticleGrowFade : public NiParticleModifier { -public: float growTime; float fadeTime; void read(NIFStream *nif) override; }; -class NiParticleColorModifier : public NiParticleModifier +struct NiParticleColorModifier : public NiParticleModifier { -public: NiColorDataPtr data; void read(NIFStream *nif) override; void post(NIFFile *nif) override; }; -class NiGravity : public NiParticleModifier +struct NiGravity : public NiParticleModifier { -public: float mForce; /* 0 - Wind (fixed direction) * 1 - Point (fixed origin) @@ -115,27 +129,24 @@ struct NiParticleCollider : public NiParticleModifier }; // NiPinaColada -class NiPlanarCollider : public NiParticleCollider +struct NiPlanarCollider : public NiParticleCollider { -public: void read(NIFStream *nif) override; osg::Vec3f mPlaneNormal; float mPlaneDistance; }; -class NiSphericalCollider : public NiParticleCollider +struct NiSphericalCollider : public NiParticleCollider { -public: float mRadius; osg::Vec3f mCenter; void read(NIFStream *nif) override; }; -class NiParticleRotation : public NiParticleModifier +struct NiParticleRotation : public NiParticleModifier { -public: void read(NIFStream *nif) override; }; diff --git a/components/nif/controller.cpp b/components/nif/controller.cpp index 1e909120e..704c4928e 100644 --- a/components/nif/controller.cpp +++ b/components/nif/controller.cpp @@ -325,4 +325,15 @@ namespace Nif data.post(nif); } + void NiColorInterpolator::read(NIFStream *nif) + { + defaultVal = nif->getVector4(); + data.read(nif); + } + + void NiColorInterpolator::post(NIFFile *nif) + { + data.post(nif); + } + } diff --git a/components/nif/controller.hpp b/components/nif/controller.hpp index 6b84d3749..503710fe9 100644 --- a/components/nif/controller.hpp +++ b/components/nif/controller.hpp @@ -29,15 +29,14 @@ namespace Nif { -class NiParticleSystemController : public Controller +struct NiParticleSystemController : public Controller { -public: struct Particle { osg::Vec3f velocity; float lifetime; float lifespan; float timestamp; - int vertex; + unsigned short vertex; }; float velocity; @@ -80,9 +79,8 @@ public: }; using NiBSPArrayController = NiParticleSystemController; -class NiMaterialColorController : public Controller +struct NiMaterialColorController : public Controller { -public: NiPoint3InterpolatorPtr interpolator; NiPosDataPtr data; unsigned int targetColor; @@ -91,9 +89,8 @@ public: void post(NIFFile *nif) override; }; -class NiPathController : public Controller +struct NiPathController : public Controller { -public: NiPosDataPtr posData; NiFloatDataPtr floatData; @@ -115,9 +112,8 @@ public: void post(NIFFile *nif) override; }; -class NiLookAtController : public Controller +struct NiLookAtController : public Controller { -public: NodePtr target; unsigned short lookAtFlags{0}; @@ -125,9 +121,8 @@ public: void post(NIFFile *nif) override; }; -class NiUVController : public Controller +struct NiUVController : public Controller { -public: NiUVDataPtr data; unsigned int uvSet; @@ -135,9 +130,8 @@ public: void post(NIFFile *nif) override; }; -class NiKeyframeController : public Controller +struct NiKeyframeController : public Controller { -public: NiKeyframeDataPtr data; NiTransformInterpolatorPtr interpolator; @@ -154,12 +148,11 @@ struct NiFloatInterpController : public Controller void post(NIFFile *nif) override; }; -class NiAlphaController : public NiFloatInterpController { }; -class NiRollController : public NiFloatInterpController { }; +struct NiAlphaController : public NiFloatInterpController { }; +struct NiRollController : public NiFloatInterpController { }; -class NiGeomMorpherController : public Controller +struct NiGeomMorpherController : public Controller { -public: NiMorphDataPtr data; NiFloatInterpolatorList interpolators; @@ -167,18 +160,16 @@ public: void post(NIFFile *nif) override; }; -class NiVisController : public Controller +struct NiVisController : public Controller { -public: NiVisDataPtr data; void read(NIFStream *nif) override; void post(NIFFile *nif) override; }; -class NiFlipController : public Controller +struct NiFlipController : public Controller { -public: NiFloatInterpolatorPtr mInterpolator; int mTexSlot; // NiTexturingProperty::TextureType float mDelta; // Time between two flips. delta = (start_time - stop_time) / num_sources @@ -230,5 +221,13 @@ struct NiTransformInterpolator : public Interpolator void post(NIFFile *nif) override; }; +struct NiColorInterpolator : public Interpolator +{ + osg::Vec4f defaultVal; + NiColorDataPtr data; + void read(NIFStream *nif) override; + void post(NIFFile *nif) override; +}; + } // Namespace #endif diff --git a/components/nif/data.hpp b/components/nif/data.hpp index 908313f50..66a391afc 100644 --- a/components/nif/data.hpp +++ b/components/nif/data.hpp @@ -32,9 +32,8 @@ namespace Nif { // Common ancestor for several data classes -class NiGeometryData : public Record +struct NiGeometryData : public Record { -public: std::vector vertices, normals, tangents, bitangents; std::vector colors; std::vector< std::vector > uvlist; @@ -44,18 +43,16 @@ public: void read(NIFStream *nif) override; }; -class NiTriShapeData : public NiGeometryData +struct NiTriShapeData : public NiGeometryData { -public: // Triangles, three vertex indices per triangle std::vector triangles; void read(NIFStream *nif) override; }; -class NiTriStripsData : public NiGeometryData +struct NiTriStripsData : public NiGeometryData { -public: // Triangle strips, series of vertex indices. std::vector> strips; @@ -70,9 +67,8 @@ struct NiLinesData : public NiGeometryData void read(NIFStream *nif) override; }; -class NiParticlesData : public NiGeometryData +struct NiParticlesData : public NiGeometryData { -public: int numParticles{0}; int activeCount; @@ -84,39 +80,34 @@ public: void read(NIFStream *nif) override; }; -class NiRotatingParticlesData : public NiParticlesData +struct NiRotatingParticlesData : public NiParticlesData { -public: void read(NIFStream *nif) override; }; -class NiPosData : public Record +struct NiPosData : public Record { -public: Vector3KeyMapPtr mKeyList; void read(NIFStream *nif) override; }; -class NiUVData : public Record +struct NiUVData : public Record { -public: FloatKeyMapPtr mKeyList[4]; void read(NIFStream *nif) override; }; -class NiFloatData : public Record +struct NiFloatData : public Record { -public: FloatKeyMapPtr mKeyList; void read(NIFStream *nif) override; }; -class NiPixelData : public Record +struct NiPixelData : public Record { -public: enum Format { NIPXFMT_RGB8, @@ -150,17 +141,15 @@ public: void post(NIFFile *nif) override; }; -class NiColorData : public Record +struct NiColorData : public Record { -public: Vector4KeyMapPtr mKeyMap; void read(NIFStream *nif) override; }; -class NiVisData : public Record +struct NiVisData : public Record { -public: struct VisData { float time; bool isSet; @@ -170,9 +159,8 @@ public: void read(NIFStream *nif) override; }; -class NiSkinInstance : public Record +struct NiSkinInstance : public Record { -public: NiSkinDataPtr data; NiSkinPartitionPtr partitions; NodePtr root; @@ -182,9 +170,8 @@ public: void post(NIFFile *nif) override; }; -class NiSkinData : public Record +struct NiSkinData : public Record { -public: struct VertWeight { unsigned short vertex; @@ -251,9 +238,8 @@ struct NiKeyframeData : public Record void read(NIFStream *nif) override; }; -class NiPalette : public Record +struct NiPalette : public Record { -public: // 32-bit RGBA colors that correspond to 8-bit indices std::vector colors; diff --git a/components/nif/extra.hpp b/components/nif/extra.hpp index 5d7aa0c3b..6d345a18e 100644 --- a/components/nif/extra.hpp +++ b/components/nif/extra.hpp @@ -29,15 +29,13 @@ namespace Nif { -class NiVertWeightsExtraData : public Extra +struct NiVertWeightsExtraData : public Extra { -public: void read(NIFStream *nif) override; }; -class NiTextKeyExtraData : public Extra +struct NiTextKeyExtraData : public Extra { -public: struct TextKey { float time; @@ -48,9 +46,8 @@ public: void read(NIFStream *nif) override; }; -class NiStringExtraData : public Extra +struct NiStringExtraData : public Extra { -public: /* Two known meanings: "MRK" - marker, only visible in the editor, not rendered in-game "NCO" - no collision diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index 67c864f47..665533c91 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -129,6 +129,10 @@ static std::map makeFactory() factory["NiPoint3Interpolator"] = {&construct , RC_NiPoint3Interpolator }; factory["NiTransformController"] = {&construct , RC_NiKeyframeController }; factory["NiTransformInterpolator"] = {&construct , RC_NiTransformInterpolator }; + factory["NiColorInterpolator"] = {&construct , RC_NiColorInterpolator }; + factory["BSShaderTextureSet"] = {&construct , RC_BSShaderTextureSet }; + factory["BSLODTriShape"] = {&construct , RC_BSLODTriShape }; + factory["BSShaderProperty"] = {&construct , RC_BSShaderProperty }; return factory; } diff --git a/components/nif/node.hpp b/components/nif/node.hpp index 58397bdca..1d082b8f9 100644 --- a/components/nif/node.hpp +++ b/components/nif/node.hpp @@ -131,9 +131,8 @@ struct NiBoundingVolume parent node (unless it's the root), and transformation (location and rotation) relative to it's parent. */ -class Node : public Named +struct Node : public Named { -public: // Node flags. Interpretation depends somewhat on the type of node. unsigned int flags; Transformation trafo; @@ -240,43 +239,6 @@ struct NiNode : Node }; struct NiGeometry : Node -{ - struct MaterialData - { - std::vector materialNames; - std::vector materialExtraData; - unsigned int activeMaterial{0}; - bool materialNeedsUpdate{false}; - void read(NIFStream *nif) - { - if (nif->getVersion() <= NIFStream::generateVersion(10,0,1,0)) - return; - unsigned int numMaterials = 0; - if (nif->getVersion() <= NIFStream::generateVersion(20,1,0,3)) - numMaterials = nif->getBoolean(); // Has Shader - else if (nif->getVersion() >= NIFStream::generateVersion(20,2,0,5)) - numMaterials = nif->getUInt(); - if (numMaterials) - { - nif->getStrings(materialNames, numMaterials); - nif->getInts(materialExtraData, numMaterials); - } - if (nif->getVersion() >= NIFStream::generateVersion(20,2,0,5)) - activeMaterial = nif->getUInt(); - if (nif->getVersion() >= NIFFile::NIFVersion::VER_BGS) - { - materialNeedsUpdate = nif->getBoolean(); - if (nif->getVersion() == NIFFile::NIFVersion::VER_BGS && nif->getBethVersion() > NIFFile::BethVersion::BETHVER_FO3) - nif->skip(8); - } - } - }; - - NiSkinInstancePtr skin; - MaterialData materialData; -}; - -struct NiTriShape : NiGeometry { /* Possible flags: 0x40 - mesh has no vertex normals ? @@ -285,14 +247,50 @@ struct NiTriShape : NiGeometry been observed so far. */ - NiTriShapeDataPtr data; + struct MaterialData + { + std::vector names; + std::vector extra; + unsigned int active{0}; + bool needsUpdate{false}; + void read(NIFStream *nif) + { + if (nif->getVersion() <= NIFStream::generateVersion(10,0,1,0)) + return; + unsigned int num = 0; + if (nif->getVersion() <= NIFStream::generateVersion(20,1,0,3)) + num = nif->getBoolean(); // Has Shader + else if (nif->getVersion() >= NIFStream::generateVersion(20,2,0,5)) + num = nif->getUInt(); + if (num) + { + nif->getStrings(names, num); + nif->getInts(extra, num); + } + if (nif->getVersion() >= NIFStream::generateVersion(20,2,0,5)) + active = nif->getUInt(); + if (nif->getVersion() >= NIFFile::NIFVersion::VER_BGS) + needsUpdate = nif->getBoolean(); + } + }; + + NiGeometryDataPtr data; + NiSkinInstancePtr skin; + MaterialData material; + BSShaderPropertyPtr shaderprop; + NiAlphaPropertyPtr alphaprop; void read(NIFStream *nif) override { Node::read(nif); data.read(nif); skin.read(nif); - materialData.read(nif); + material.read(nif); + if (nif->getVersion() == NIFFile::NIFVersion::VER_BGS && nif->getBethVersion() > NIFFile::BethVersion::BETHVER_FO3) + { + shaderprop.read(nif); + alphaprop.read(nif); + } } void post(NIFFile *nif) override @@ -300,53 +298,28 @@ struct NiTriShape : NiGeometry Node::post(nif); data.post(nif); skin.post(nif); - if (!skin.empty()) + shaderprop.post(nif); + alphaprop.post(nif); + if (recType != RC_NiParticles && !skin.empty()) nif->setUseSkinning(true); } }; -struct NiTriStrips : NiGeometry +struct NiTriShape : NiGeometry {}; +struct BSLODTriShape : NiTriShape { - NiTriStripsDataPtr data; - + unsigned int lod0, lod1, lod2; void read(NIFStream *nif) override { - Node::read(nif); - data.read(nif); - skin.read(nif); - materialData.read(nif); - } - - void post(NIFFile *nif) override - { - Node::post(nif); - data.post(nif); - skin.post(nif); - if (!skin.empty()) - nif->setUseSkinning(true); - } -}; - -struct NiLines : NiGeometry -{ - NiLinesDataPtr data; - - void read(NIFStream *nif) override - { - Node::read(nif); - data.read(nif); - skin.read(nif); - } - - void post(NIFFile *nif) override - { - Node::post(nif); - data.post(nif); - skin.post(nif); - if (!skin.empty()) - nif->setUseSkinning(true); + NiTriShape::read(nif); + lod0 = nif->getUInt(); + lod1 = nif->getUInt(); + lod2 = nif->getUInt(); } }; +struct NiTriStrips : NiGeometry {}; +struct NiLines : NiGeometry {}; +struct NiParticles : NiGeometry { }; struct NiCamera : Node { @@ -401,25 +374,6 @@ struct NiCamera : Node } }; -struct NiParticles : NiGeometry -{ - NiParticlesDataPtr data; - void read(NIFStream *nif) override - { - Node::read(nif); - data.read(nif); - skin.read(nif); - materialData.read(nif); - } - - void post(NIFFile *nif) override - { - Node::post(nif); - data.post(nif); - skin.post(nif); - } -}; - // A node used as the base to switch between child nodes, such as for LOD levels. struct NiSwitchNode : public NiNode { diff --git a/components/nif/property.cpp b/components/nif/property.cpp index e6ae71c24..d5357e123 100644 --- a/components/nif/property.cpp +++ b/components/nif/property.cpp @@ -99,6 +99,25 @@ void NiTexturingProperty::post(NIFFile *nif) shaderTextures[i].post(nif); } +void BSShaderProperty::read(NIFStream *nif) +{ + NiShadeProperty::read(nif); + if (nif->getBethVersion() <= NIFFile::BethVersion::BETHVER_FO3) + { + type = nif->getUInt(); + flags1 = nif->getUInt(); + flags2 = nif->getUInt(); + envMapIntensity = nif->getFloat(); + } +} + +void BSShaderLightingProperty::read(NIFStream *nif) +{ + BSShaderProperty::read(nif); + if (nif->getBethVersion() <= NIFFile::BethVersion::BETHVER_FO3) + clamp = nif->getUInt(); +} + void NiFogProperty::read(NIFStream *nif) { Property::read(nif); diff --git a/components/nif/property.hpp b/components/nif/property.hpp index c821b7c37..eccb442f7 100644 --- a/components/nif/property.hpp +++ b/components/nif/property.hpp @@ -29,11 +29,10 @@ namespace Nif { -class Property : public Named { }; +struct Property : public Named { }; -class NiTexturingProperty : public Property +struct NiTexturingProperty : public Property { -public: unsigned short flags{0u}; // A sub-texture @@ -96,9 +95,8 @@ public: void post(NIFFile *nif) override; }; -class NiFogProperty : public Property +struct NiFogProperty : public Property { -public: unsigned short mFlags; float mFogDepth; osg::Vec3f mColour; @@ -118,6 +116,19 @@ struct NiShadeProperty : public Property } }; +struct BSShaderProperty : public NiShadeProperty +{ + unsigned int type{0u}, flags1{0u}, flags2{0u}; + float envMapIntensity{0.f}; + void read(NIFStream *nif) override; +}; + +struct BSShaderLightingProperty : public BSShaderProperty +{ + unsigned int clamp{0u}; + void read(NIFStream *nif) override; +}; + struct NiDitherProperty : public Property { unsigned short flags; @@ -294,8 +305,8 @@ struct S_StencilProperty void read(NIFStream *nif); }; -class NiAlphaProperty : public StructPropT { }; -class NiVertexColorProperty : public StructPropT { }; +struct NiAlphaProperty : public StructPropT { }; +struct NiVertexColorProperty : public StructPropT { }; struct NiStencilProperty : public Property { S_StencilProperty data; diff --git a/components/nif/record.hpp b/components/nif/record.hpp index 2217c588e..efacd8246 100644 --- a/components/nif/record.hpp +++ b/components/nif/record.hpp @@ -118,7 +118,11 @@ enum RecordType RC_NiFloatInterpolator, RC_NiPoint3Interpolator, RC_NiBoolInterpolator, - RC_NiTransformInterpolator + RC_NiTransformInterpolator, + RC_NiColorInterpolator, + RC_BSShaderTextureSet, + RC_BSLODTriShape, + RC_BSShaderProperty }; /// Base class for all records diff --git a/components/nif/recordptr.hpp b/components/nif/recordptr.hpp index ed8f7ef6b..b30d99fbe 100644 --- a/components/nif/recordptr.hpp +++ b/components/nif/recordptr.hpp @@ -120,33 +120,34 @@ public: }; -class Node; -class Extra; -class Property; -class NiUVData; -class NiPosData; -class NiVisData; -class Controller; -class Named; -class NiSkinData; -class NiFloatData; +struct Node; +struct Extra; +struct Property; +struct NiUVData; +struct NiPosData; +struct NiVisData; +struct Controller; +struct Named; +struct NiSkinData; +struct NiFloatData; struct NiMorphData; -class NiPixelData; -class NiColorData; +struct NiPixelData; +struct NiColorData; struct NiKeyframeData; -class NiTriShapeData; -class NiTriStripsData; -class NiSkinInstance; -class NiSourceTexture; -class NiParticlesData; -class NiPalette; +struct NiTriStripsData; +struct NiSkinInstance; +struct NiSourceTexture; +struct NiPalette; struct NiParticleModifier; -struct NiLinesData; struct NiBoolData; struct NiSkinPartition; struct NiFloatInterpolator; struct NiPoint3Interpolator; struct NiTransformInterpolator; +struct BSShaderTextureSet; +struct NiGeometryData; +struct BSShaderProperty; +struct NiAlphaProperty; using NodePtr = RecordPtrT; using ExtraPtr = RecordPtrT; @@ -161,12 +162,8 @@ using NiPixelDataPtr = RecordPtrT; using NiFloatDataPtr = RecordPtrT; using NiColorDataPtr = RecordPtrT; using NiKeyframeDataPtr = RecordPtrT; -using NiTriShapeDataPtr = RecordPtrT; -using NiTriStripsDataPtr = RecordPtrT; -using NiLinesDataPtr = RecordPtrT; using NiSkinInstancePtr = RecordPtrT; using NiSourceTexturePtr = RecordPtrT; -using NiParticlesDataPtr = RecordPtrT; using NiPalettePtr = RecordPtrT; using NiParticleModifierPtr = RecordPtrT; using NiBoolDataPtr = RecordPtrT; @@ -174,12 +171,17 @@ using NiSkinPartitionPtr = RecordPtrT; using NiFloatInterpolatorPtr = RecordPtrT; using NiPoint3InterpolatorPtr = RecordPtrT; using NiTransformInterpolatorPtr = RecordPtrT; +using BSShaderTextureSetPtr = RecordPtrT; +using NiGeometryDataPtr = RecordPtrT; +using BSShaderPropertyPtr = RecordPtrT; +using NiAlphaPropertyPtr = RecordPtrT; using NodeList = RecordListT; using PropertyList = RecordListT; using ExtraList = RecordListT; using NiSourceTextureList = RecordListT; using NiFloatInterpolatorList = RecordListT; +using NiTriStripsDataList = RecordListT; } // Namespace #endif diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index db9d5ae43..d72cef194 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -34,11 +34,10 @@ bool pathFileNameStartsWithX(const std::string& path) void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriShapeData& data, const osg::Matrixf &transform) { - mesh.preallocateVertices(static_cast(data.vertices.size())); - mesh.preallocateIndices(static_cast(data.triangles.size())); - const std::vector &vertices = data.vertices; const std::vector &triangles = data.triangles; + mesh.preallocateVertices(static_cast(vertices.size())); + mesh.preallocateIndices(static_cast(triangles.size())); for (std::size_t i = 0; i < triangles.size(); i += 3) { @@ -54,8 +53,6 @@ void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriStripsData& data, co { const std::vector &vertices = data.vertices; const std::vector> &strips = data.strips; - if (vertices.empty() || strips.empty()) - return; mesh.preallocateVertices(static_cast(vertices.size())); int numTriangles = 0; for (const std::vector& strip : strips) @@ -102,12 +99,12 @@ void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriStripsData& data, co } } -void fillTriangleMesh(btTriangleMesh& mesh, const Nif::Node* nifNode, const osg::Matrixf &transform = osg::Matrixf()) +void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiGeometry* geometry, const osg::Matrixf &transform = osg::Matrixf()) { - if (nifNode->recType == Nif::RC_NiTriShape) - fillTriangleMesh(mesh, static_cast(nifNode)->data.get(), transform); - else if (nifNode->recType == Nif::RC_NiTriStrips) - fillTriangleMesh(mesh, static_cast(nifNode)->data.get(), transform); + if (geometry->recType == Nif::RC_NiTriShape || geometry->recType == Nif::RC_BSLODTriShape) + fillTriangleMesh(mesh, static_cast(geometry->data.get()), transform); + else if (geometry->recType == Nif::RC_NiTriStrips) + fillTriangleMesh(mesh, static_cast(geometry->data.get()), transform); } } @@ -145,12 +142,12 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) { if (findBoundingBox(node, filename)) { - const btVector3 halfExtents = Misc::Convert::toBullet(mShape->mCollisionBoxHalfExtents); - const btVector3 origin = Misc::Convert::toBullet(mShape->mCollisionBoxTranslate); + const btVector3 extents = Misc::Convert::toBullet(mShape->mCollisionBox.extents); + const btVector3 center = Misc::Convert::toBullet(mShape->mCollisionBox.center); std::unique_ptr compound (new btCompoundShape); - std::unique_ptr boxShape(new btBoxShape(halfExtents)); + std::unique_ptr boxShape(new btBoxShape(extents)); btTransform transform = btTransform::getIdentity(); - transform.setOrigin(origin); + transform.setOrigin(center); compound->addChildShape(transform, boxShape.get()); boxShape.release(); @@ -208,8 +205,8 @@ bool BulletNifLoader::findBoundingBox(const Nif::Node* node, const std::string& switch (type) { case Nif::NiBoundingVolume::Type::BOX_BV: - mShape->mCollisionBoxHalfExtents = node->bounds.box.extents; - mShape->mCollisionBoxTranslate = node->bounds.box.center; + mShape->mCollisionBox.extents = node->bounds.box.extents; + mShape->mCollisionBox.center = node->bounds.box.center; break; default: { @@ -312,7 +309,9 @@ void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node *n // NOTE: a trishape with hasBounds=true, but no BBoxCollision flag should NOT go through handleNiTriShape! // It must be ignored completely. // (occurs in tr_ex_imp_wall_arch_04.nif) - if(!node->hasBounds && (node->recType == Nif::RC_NiTriShape || node->recType == Nif::RC_NiTriStrips)) + if(!node->hasBounds && (node->recType == Nif::RC_NiTriShape + || node->recType == Nif::RC_NiTriStrips + || node->recType == Nif::RC_BSLODTriShape)) { handleNiTriShape(node, flags, getWorldTransform(node), isAnimated, avoid); } @@ -341,23 +340,31 @@ void BulletNifLoader::handleNiTriShape(const Nif::Node *nifNode, int flags, cons if ((flags & 0x800)) return; - if (nifNode->recType == Nif::RC_NiTriShape) + auto niGeometry = static_cast(nifNode); + if (niGeometry->data.empty() || niGeometry->data->vertices.empty()) + return; + + if (niGeometry->recType == Nif::RC_NiTriShape || niGeometry->recType == Nif::RC_BSLODTriShape) { - const Nif::NiTriShape* shape = static_cast(nifNode); - if (!shape->skin.empty()) - isAnimated = false; - if (shape->data.empty() || shape->data->triangles.empty()) + if (niGeometry->data->recType != Nif::RC_NiTriShapeData) + return; + + auto data = static_cast(niGeometry->data.getPtr()); + if (data->triangles.empty()) return; } - else + else if (niGeometry->recType == Nif::RC_NiTriStrips) { - const Nif::NiTriStrips* shape = static_cast(nifNode); - if (!shape->skin.empty()) - isAnimated = false; - if (shape->data.empty() || shape->data->strips.empty()) + if (niGeometry->data->recType != Nif::RC_NiTriStripsData) + return; + + auto data = static_cast(niGeometry->data.getPtr()); + if (data->strips.empty()) return; } + if (!niGeometry->skin.empty()) + isAnimated = false; if (isAnimated) { @@ -366,7 +373,7 @@ void BulletNifLoader::handleNiTriShape(const Nif::Node *nifNode, int flags, cons std::unique_ptr childMesh(new btTriangleMesh); - fillTriangleMesh(*childMesh, nifNode); + fillTriangleMesh(*childMesh, niGeometry); std::unique_ptr childShape(new Resource::TriangleMeshShape(childMesh.get(), true)); childMesh.release(); @@ -394,7 +401,7 @@ void BulletNifLoader::handleNiTriShape(const Nif::Node *nifNode, int flags, cons if (!mAvoidStaticMesh) mAvoidStaticMesh.reset(new btTriangleMesh(false)); - fillTriangleMesh(*mAvoidStaticMesh, nifNode, transform); + fillTriangleMesh(*mAvoidStaticMesh, niGeometry, transform); } else { @@ -402,7 +409,7 @@ void BulletNifLoader::handleNiTriShape(const Nif::Node *nifNode, int flags, cons mStaticMesh.reset(new btTriangleMesh(false)); // Static shape, just transform all vertices into position - fillTriangleMesh(*mStaticMesh, nifNode, transform); + fillTriangleMesh(*mStaticMesh, niGeometry, transform); } } diff --git a/components/nifbullet/bulletnifloader.hpp b/components/nifbullet/bulletnifloader.hpp index 054b33fed..17a0d3e8b 100644 --- a/components/nifbullet/bulletnifloader.hpp +++ b/components/nifbullet/bulletnifloader.hpp @@ -23,7 +23,7 @@ class btCollisionShape; namespace Nif { - class Node; + struct Node; struct Transformation; struct NiTriShape; struct NiTriStrips; diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp index 0063b2ec0..96beafcbb 100644 --- a/components/nifosg/controller.hpp +++ b/components/nifosg/controller.hpp @@ -66,7 +66,9 @@ namespace NifOsg std::conjunction_v< std::disjunction< std::is_same, - std::is_same + std::is_same, + std::is_same, + std::is_same >, std::is_same >, diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 0db15237e..902f15fb3 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -67,6 +67,7 @@ namespace case Nif::RC_NiTriShape: case Nif::RC_NiTriStrips: case Nif::RC_NiLines: + case Nif::RC_BSLODTriShape: return true; } return false; @@ -95,6 +96,15 @@ namespace } } } + + auto geometry = dynamic_cast(nifNode); + if (geometry) + { + if (!geometry->shaderprop.empty()) + out.emplace_back(geometry->shaderprop.getPtr()); + if (!geometry->alphaprop.empty()) + out.emplace_back(geometry->alphaprop.getPtr()); + } } // NodeCallback used to have a node always oriented towards the camera. The node can have translation and scale @@ -365,6 +375,11 @@ namespace NifOsg handleProperty(props[i].getPtr(), applyTo, composite, imageManager, boundTextures, animflags); } } + + auto geometry = dynamic_cast(nifNode); + // NiGeometry's NiAlphaProperty doesn't get handled here because it's a drawable property + if (geometry && !geometry->shaderprop.empty()) + handleProperty(geometry->shaderprop.getPtr(), applyTo, composite, imageManager, boundTextures, animflags); } void setupController(const Nif::Controller* ctrl, SceneUtil::Controller* toSetup, int animflags) @@ -466,19 +481,12 @@ namespace NifOsg texture2d->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); - osg::ref_ptr texEnv = new osg::TexEnvCombine; - texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); - texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnv->setCombine_RGB(osg::TexEnvCombine::ADD); - texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); - int texUnit = 3; // FIXME osg::StateSet* stateset = node->getOrCreateStateSet(); stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); stateset->setTextureAttributeAndModes(texUnit, texGen, osg::StateAttribute::ON); - stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(texUnit, createEmissiveTexEnv(), osg::StateAttribute::ON); stateset->addUniform(new osg::Uniform("envMapColor", osg::Vec4f(1,1,1,1))); } @@ -946,11 +954,11 @@ namespace NifOsg // Load the initial state of the particle system, i.e. the initial particles and their positions, velocity and colors. void handleParticleInitialState(const Nif::Node* nifNode, osgParticle::ParticleSystem* partsys, const Nif::NiParticleSystemController* partctrl) { - const auto particleNode = static_cast(nifNode); - if (particleNode->data.empty()) + auto particleNode = static_cast(nifNode); + if (particleNode->data.empty() || particleNode->data->recType != Nif::RC_NiParticlesData) return; - const Nif::NiParticlesData* particledata = particleNode->data.getPtr(); + auto particledata = static_cast(particleNode->data.getPtr()); osg::BoundingBox box; @@ -963,6 +971,9 @@ namespace NifOsg if (particle.lifespan <= 0) continue; + if (particle.vertex >= particledata->vertices.size()) + continue; + ParticleAgeSetter particletemplate(std::max(0.f, particle.lifetime)); osgParticle::Particle* created = partsys->createParticle(&particletemplate); @@ -971,16 +982,16 @@ namespace NifOsg // Note this position and velocity is not correct for a particle system with absolute reference frame, // which can not be done in this loader since we are not attached to the scene yet. Will be fixed up post-load in the SceneManager. created->setVelocity(particle.velocity); - const osg::Vec3f& position = particledata->vertices.at(particle.vertex); + const osg::Vec3f& position = particledata->vertices[particle.vertex]; created->setPosition(position); osg::Vec4f partcolor (1.f,1.f,1.f,1.f); - if (particle.vertex < int(particledata->colors.size())) - partcolor = particledata->colors.at(particle.vertex); + if (particle.vertex < particledata->colors.size()) + partcolor = particledata->colors[particle.vertex]; float size = partctrl->size; - if (particle.vertex < int(particledata->sizes.size())) - size *= particledata->sizes.at(particle.vertex); + if (particle.vertex < particledata->sizes.size()) + size *= particledata->sizes[particle.vertex]; created->setSizeRange(osgParticle::rangef(size, size)); box.expandBy(osg::BoundingSphere(position, size)); @@ -1177,51 +1188,50 @@ namespace NifOsg void handleNiGeometry(const Nif::Node *nifNode, osg::Geometry *geometry, osg::Node* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) { - const Nif::NiGeometryData* niGeometryData = nullptr; - if (nifNode->recType == Nif::RC_NiTriShape) + const Nif::NiGeometry* niGeometry = static_cast(nifNode); + if (niGeometry->data.empty()) + return; + const Nif::NiGeometryData* niGeometryData = niGeometry->data.getPtr(); + + if (niGeometry->recType == Nif::RC_NiTriShape || nifNode->recType == Nif::RC_BSLODTriShape) { - const Nif::NiTriShape* triShape = static_cast(nifNode); - if (!triShape->data.empty()) - { - const Nif::NiTriShapeData* data = triShape->data.getPtr(); - niGeometryData = static_cast(data); - if (!data->triangles.empty()) - geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, data->triangles.size(), - (unsigned short*)data->triangles.data())); - } + if (niGeometryData->recType != Nif::RC_NiTriShapeData) + return; + auto triangles = static_cast(niGeometryData)->triangles; + if (triangles.empty()) + return; + geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, triangles.size(), + (unsigned short*)triangles.data())); } - else if (nifNode->recType == Nif::RC_NiTriStrips) + else if (niGeometry->recType == Nif::RC_NiTriStrips) { - const Nif::NiTriStrips* triStrips = static_cast(nifNode); - if (!triStrips->data.empty()) + if (niGeometryData->recType != Nif::RC_NiTriStripsData) + return; + auto data = static_cast(niGeometryData); + bool hasGeometry = false; + for (const auto& strip : data->strips) { - const Nif::NiTriStripsData* data = triStrips->data.getPtr(); - niGeometryData = static_cast(data); - if (!data->strips.empty()) - { - for (const auto& strip : data->strips) - { - if (strip.size() >= 3) - geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLE_STRIP, strip.size(), - (unsigned short*)strip.data())); - } - } + if (strip.size() < 3) + continue; + geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLE_STRIP, strip.size(), + (unsigned short*)strip.data())); + hasGeometry = true; } + if (!hasGeometry) + return; } - else if (nifNode->recType == Nif::RC_NiLines) + else if (niGeometry->recType == Nif::RC_NiLines) { - const Nif::NiLines* lines = static_cast(nifNode); - if (!lines->data.empty()) - { - const Nif::NiLinesData* data = lines->data.getPtr(); - niGeometryData = static_cast(data); - const auto& line = data->lines; - if (!line.empty()) - geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::LINES, line.size(), (unsigned short*)line.data())); - } + if (niGeometryData->recType != Nif::RC_NiLinesData) + return; + auto data = static_cast(niGeometryData); + const auto& line = data->lines; + if (line.empty()) + return; + geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::LINES, line.size(), + (unsigned short*)line.data())); } - if (niGeometryData) - handleNiGeometryData(geometry, niGeometryData, boundTextures, nifNode->name); + handleNiGeometryData(geometry, niGeometryData, boundTextures, nifNode->name); // osg::Material properties are handled here for two reasons: // - if there are no vertex colors, we need to disable colorMode. @@ -1229,15 +1239,18 @@ namespace NifOsg // above the actual renderable would be tedious. std::vector drawableProps; collectDrawableProperties(nifNode, drawableProps); - applyDrawableProperties(parentNode, drawableProps, composite, niGeometryData && !niGeometryData->colors.empty(), animflags); + applyDrawableProperties(parentNode, drawableProps, composite, !niGeometryData->colors.empty(), animflags); } void handleGeometry(const Nif::Node* nifNode, osg::Group* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) { assert(isTypeGeometry(nifNode->recType)); - osg::ref_ptr drawable; osg::ref_ptr geom (new osg::Geometry); handleNiGeometry(nifNode, geom, parentNode, composite, boundTextures, animflags); + // If the record had no valid geometry data in it, early-out + if (geom->empty()) + return; + osg::ref_ptr drawable; for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next) { if (!(ctrl->flags & Nif::NiNode::ControllerFlag_Active)) @@ -1282,6 +1295,8 @@ namespace NifOsg assert(isTypeGeometry(nifNode->recType)); osg::ref_ptr geometry (new osg::Geometry); handleNiGeometry(nifNode, geometry, parentNode, composite, boundTextures, animflags); + if (geometry->empty()) + return; osg::ref_ptr rig(new SceneUtil::RigGeometry); rig->setSourceGeometry(geometry); rig->setName(nifNode->name); @@ -1481,6 +1496,17 @@ namespace NifOsg return image; } + osg::ref_ptr createEmissiveTexEnv() + { + osg::ref_ptr texEnv(new osg::TexEnvCombine); + texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); + texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnv->setCombine_RGB(osg::TexEnvCombine::ADD); + texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); + return texEnv; + } + void handleTextureProperty(const Nif::NiTexturingProperty* texprop, const std::string& nodeName, osg::StateSet* stateset, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags) { if (!boundTextures.empty()) @@ -1567,14 +1593,7 @@ namespace NifOsg if (i == Nif::NiTexturingProperty::GlowTexture) { - osg::TexEnvCombine* texEnv = new osg::TexEnvCombine; - texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); - texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnv->setCombine_RGB(osg::TexEnvCombine::ADD); - texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); - - stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(texUnit, createEmissiveTexEnv(), osg::StateAttribute::ON); } else if (i == Nif::NiTexturingProperty::DarkTexture) { diff --git a/components/nifosg/particle.hpp b/components/nifosg/particle.hpp index dd89f4501..b0a46f0ca 100644 --- a/components/nifosg/particle.hpp +++ b/components/nifosg/particle.hpp @@ -14,10 +14,10 @@ namespace Nif { - class NiGravity; - class NiPlanarCollider; - class NiSphericalCollider; - class NiColorData; + struct NiGravity; + struct NiPlanarCollider; + struct NiSphericalCollider; + struct NiColorData; } namespace NifOsg diff --git a/components/resource/animation.cpp b/components/resource/animation.cpp index 34ae162ee..d2d7d08d5 100644 --- a/components/resource/animation.cpp +++ b/components/resource/animation.cpp @@ -10,7 +10,7 @@ namespace Resource mStartTime(0.0f) { const osgAnimation::ChannelList& channels = anim.getChannels(); - for (const osg::ref_ptr channel: channels) + for (const auto& channel: channels) addChannel(channel.get()->clone()); } @@ -31,7 +31,7 @@ namespace Resource bool Animation::update (double time) { - for (const osg::ref_ptr channel: mChannels) + for (const auto& channel: mChannels) { channel->update(time, 1.0f, 0); } diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp index 7dd0964e8..fc68c5545 100644 --- a/components/resource/bulletshape.cpp +++ b/components/resource/bulletshape.cpp @@ -20,8 +20,7 @@ BulletShape::BulletShape() BulletShape::BulletShape(const BulletShape ©, const osg::CopyOp ©op) : mCollisionShape(duplicateCollisionShape(copy.mCollisionShape)) , mAvoidCollisionShape(duplicateCollisionShape(copy.mAvoidCollisionShape)) - , mCollisionBoxHalfExtents(copy.mCollisionBoxHalfExtents) - , mCollisionBoxTranslate(copy.mCollisionBoxTranslate) + , mCollisionBox(copy.mCollisionBox) , mAnimatedShapes(copy.mAnimatedShapes) { } @@ -106,8 +105,7 @@ BulletShapeInstance::BulletShapeInstance(osg::ref_ptr source) : BulletShape() , mSource(source) { - mCollisionBoxHalfExtents = source->mCollisionBoxHalfExtents; - mCollisionBoxTranslate = source->mCollisionBoxTranslate; + mCollisionBox = source->mCollisionBox; mAnimatedShapes = source->mAnimatedShapes; diff --git a/components/resource/bulletshape.hpp b/components/resource/bulletshape.hpp index e77c96327..7d9577ba0 100644 --- a/components/resource/bulletshape.hpp +++ b/components/resource/bulletshape.hpp @@ -27,10 +27,14 @@ namespace Resource btCollisionShape* mCollisionShape; btCollisionShape* mAvoidCollisionShape; + struct CollisionBox + { + osg::Vec3f extents; + osg::Vec3f center; + }; // Used for actors. mCollisionShape is used for actors only when we need to autogenerate collision box for creatures. // For now, use one file <-> one resource for simplicity. - osg::Vec3f mCollisionBoxHalfExtents; - osg::Vec3f mCollisionBoxTranslate; + CollisionBox mCollisionBox; // Stores animated collision shapes. If any collision nodes in the NIF are animated, then mCollisionShape // will be a btCompoundShape (which consists of one or more child shapes). diff --git a/components/resource/keyframemanager.cpp b/components/resource/keyframemanager.cpp index af0f365ee..d739392e8 100644 --- a/components/resource/keyframemanager.cpp +++ b/components/resource/keyframemanager.cpp @@ -21,7 +21,7 @@ namespace Resource void RetrieveAnimationsVisitor::apply(osg::Node& node) { - if (node.libraryName() == std::string("osgAnimation") && node.className() == std::string("Bone") && node.getName() == std::string("root")) + if (node.libraryName() == std::string("osgAnimation") && node.className() == std::string("Bone") && node.getName() == std::string("bip01")) { osg::ref_ptr callback = new SceneUtil::OsgAnimationController(); @@ -40,11 +40,9 @@ namespace Resource std::string animationName = animation->getName(); std::string start = animationName + std::string(": start"); std::string stop = animationName + std::string(": stop"); - std::string loopstart = animationName + std::string(": loop start"); - std::string loopstop = animationName + std::string(": loop stop"); const osgAnimation::ChannelList& channels = animation->getChannels(); - for (const osg::ref_ptr channel: channels) + for (const auto& channel: channels) { mergedAnimationTrack->addChannel(channel.get()->clone()); // is ->clone needed? } @@ -60,8 +58,6 @@ namespace Resource // Keywords can be stuff like Loop, Equip, Unequip, Block, InventoryHandtoHand, InventoryWeaponOneHand, PickProbe, Slash, Thrust, Chop... even "Slash Small Follow" mTarget.mTextKeys.emplace(startTime, std::move(start)); mTarget.mTextKeys.emplace(stopTime, std::move(stop)); - mTarget.mTextKeys.emplace(startTime, std::move(loopstart)); - mTarget.mTextKeys.emplace(stopTime, std::move(loopstop)); SceneUtil::EmulatedAnimation emulatedAnimation; emulatedAnimation.mStartTime = startTime; diff --git a/components/sceneutil/osgacontroller.cpp b/components/sceneutil/osgacontroller.cpp index 87e6f02fe..37aa525af 100644 --- a/components/sceneutil/osgacontroller.cpp +++ b/components/sceneutil/osgacontroller.cpp @@ -31,7 +31,7 @@ namespace SceneUtil void LinkVisitor::link(osgAnimation::UpdateMatrixTransform* umt) { const osgAnimation::ChannelList& channels = mAnimation->getChannels(); - for (const osg::ref_ptr channel: channels) + for (const auto& channel: channels) { const std::string& channelName = channel->getName(); const std::string& channelTargetName = channel->getTargetName(); @@ -83,7 +83,7 @@ namespace SceneUtil { osgAnimation::UpdateMatrixTransform* umt = dynamic_cast(cb); if (umt) - if (node.getName() != "root") link(umt); + if (node.getName() != "bip01") link(umt); cb = cb->getNestedCallback(); } @@ -117,7 +117,7 @@ namespace SceneUtil //Find the correct animation based on time for (const EmulatedAnimation& emulatedAnimation : mEmulatedAnimations) { - if (time > emulatedAnimation.mStartTime && time < emulatedAnimation.mStopTime) + if (time >= emulatedAnimation.mStartTime && time <= emulatedAnimation.mStopTime) { newTime = time - emulatedAnimation.mStartTime; animationName = emulatedAnimation.mName; @@ -125,15 +125,15 @@ namespace SceneUtil } //Find the root transform track in animation - for (const osg::ref_ptr mergedAnimationTrack : mMergedAnimationTracks) + for (const auto& mergedAnimationTrack : mMergedAnimationTracks) { if (mergedAnimationTrack->getName() != animationName) continue; const osgAnimation::ChannelList& channels = mergedAnimationTrack->getChannels(); - for (const osg::ref_ptr channel: channels) + for (const auto& channel: channels) { - if (channel->getTargetName() != "root" || channel->getName() != "transform") continue; + if (channel->getTargetName() != "bip01" || channel->getName() != "transform") continue; if ( osgAnimation::MatrixLinearSampler* templateSampler = dynamic_cast (channel->getSampler()) ) { @@ -150,7 +150,7 @@ namespace SceneUtil void OsgAnimationController::update(float time, std::string animationName) { - for (const osg::ref_ptr mergedAnimationTrack : mMergedAnimationTracks) + for (const auto& mergedAnimationTrack : mMergedAnimationTracks) { if (mergedAnimationTrack->getName() == animationName) mergedAnimationTrack->update(time); } @@ -162,7 +162,7 @@ namespace SceneUtil { if (mNeedToLink) { - for (const osg::ref_ptr mergedAnimationTrack : mMergedAnimationTracks) + for (const auto& mergedAnimationTrack : mMergedAnimationTracks) { if (!mLinker.valid()) mLinker = new LinkVisitor(); mLinker->setAnimation(mergedAnimationTrack); diff --git a/components/sceneutil/util.cpp b/components/sceneutil/util.cpp index a23f3f109..2c0d8efa0 100644 --- a/components/sceneutil/util.cpp +++ b/components/sceneutil/util.cpp @@ -112,6 +112,8 @@ void GlowUpdater::apply(osg::StateSet *stateset, osg::NodeVisitor *nv) removeTexture(stateset); this->reset(); mDone = true; + // normally done in StateSetUpdater::operator(), but needs doing here so the shader visitor sees the right StateSet + mNode->setStateSet(stateset); mResourceSystem->getSceneManager()->recreateShaders(mNode); } if (mOriginalDuration < 0) // if this glowupdater was originally a permanent glow diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index 10e9e606e..e908b6aaa 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -294,7 +294,10 @@ namespace Shader void ShaderVisitor::createProgram(const ShaderRequirements &reqs) { if (!reqs.mShaderRequired && !mForceShaders) + { + ensureFFP(*reqs.mNode); return; + } osg::Node& node = *reqs.mNode; osg::StateSet* writableStateSet = nullptr; @@ -333,6 +336,19 @@ namespace Shader } } + void ShaderVisitor::ensureFFP(osg::Node& node) + { + if (!node.getStateSet() || !node.getStateSet()->getAttribute(osg::StateAttribute::PROGRAM)) + return; + osg::StateSet* writableStateSet = nullptr; + if (mAllowedToModifyStateSets) + writableStateSet = node.getStateSet(); + else + writableStateSet = getWritableStateSet(node); + + writableStateSet->removeAttribute(osg::StateAttribute::PROGRAM); + } + bool ShaderVisitor::adjustGeometry(osg::Geometry& sourceGeometry, const ShaderRequirements& reqs) { bool useShader = reqs.mShaderRequired || mForceShaders; @@ -380,6 +396,8 @@ namespace Shader createProgram(reqs); } + else + ensureFFP(geometry); if (needPop) popRequirements(); @@ -414,6 +432,8 @@ namespace Shader morph->setSourceGeometry(sourceGeometry); } } + else + ensureFFP(drawable); if (needPop) popRequirements(); diff --git a/components/shader/shadervisitor.hpp b/components/shader/shadervisitor.hpp index 6b2353b66..6031dbfe6 100644 --- a/components/shader/shadervisitor.hpp +++ b/components/shader/shadervisitor.hpp @@ -94,6 +94,7 @@ namespace Shader std::string mDefaultFsTemplate; void createProgram(const ShaderRequirements& reqs); + void ensureFFP(osg::Node& node); bool adjustGeometry(osg::Geometry& sourceGeometry, const ShaderRequirements& reqs); }; diff --git a/components/widgets/box.cpp b/components/widgets/box.cpp index 23a108ee9..3e8f62b4b 100644 --- a/components/widgets/box.cpp +++ b/components/widgets/box.cpp @@ -32,7 +32,7 @@ namespace Gui MyGUI::IntSize AutoSizedTextBox::getRequestedSize() { - return getTextSize(); + return getCaption().empty() ? MyGUI::IntSize{0, 0} : getTextSize(); } void AutoSizedTextBox::setCaption(const MyGUI::UString& _value) diff --git a/docs/source/reference/modding/font.rst b/docs/source/reference/modding/font.rst index fa89b33ea..3778ac5ed 100644 --- a/docs/source/reference/modding/font.rst +++ b/docs/source/reference/modding/font.rst @@ -26,7 +26,7 @@ Now Fonts folder should include ``openmw_font.xml`` file and three ``.ttf`` file If desired, you can now delete the ``Data Files/Fonts`` directory. -It is also possible to adjust the font size and resolution:: +It is also possible to adjust the font size and resolution via ``settings.cfg`` file:: [GUI] font size = 16 diff --git a/docs/source/reference/modding/settings/camera.rst b/docs/source/reference/modding/settings/camera.rst index 5701947dc..c30dcb3f3 100644 --- a/docs/source/reference/modding/settings/camera.rst +++ b/docs/source/reference/modding/settings/camera.rst @@ -46,7 +46,7 @@ viewing distance :Type: floating point :Range: > 0 -:Default: 6656.0 +:Default: 7168.0 This value controls the maximum visible distance (also called the far clipping plane). Larger values significantly improve rendering in exterior spaces, diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index fd93eba61..4e1fe1318 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -428,3 +428,14 @@ even if the fighting NPC is knocked out. This setting allows the player to steal items from fighting NPCs that were knocked out if enabled. This setting can be controlled in Advanced tab of the launcher. + +graphic herbalism +----------------- + +:Type: boolean +:Range: True/False +:Default: True + +Some mods add harvestable container models. When this setting is enabled, activating a container using a harvestable model will visually harvest from it instead of opening the menu. + +When this setting is turned off or when activating a regular container, the menu will open as usual. diff --git a/docs/source/reference/modding/settings/water.rst b/docs/source/reference/modding/settings/water.rst index 3581efb99..b79daacb7 100644 --- a/docs/source/reference/modding/settings/water.rst +++ b/docs/source/reference/modding/settings/water.rst @@ -59,7 +59,7 @@ This setting has no effect if the shader setting is false. This setting can be toggled with the 'Refraction' button in the Water tab of the Video panel of the Options menu. reflection detail --------------- +----------------- :Type: integer :Range: 0, 1, 2, 3, 4 @@ -111,3 +111,6 @@ This setting only applies if water shader is on and refractions are enabled. Not setting if off, there will still be small refractions caused by the water waves, which however do not cause such significant distortion. +.. warning:: + The `refraction scale` is currently mutually exclusive to underwater shadows. Setting this to any value except 1.0 + will cause underwater shadows to be disabled. This will be addressed in issue https://gitlab.com/OpenMW/openmw/-/issues/5709 diff --git a/files/mygui/openmw_settings_window.layout b/files/mygui/openmw_settings_window.layout index f61af5a10..14ab7c9de 100644 --- a/files/mygui/openmw_settings_window.layout +++ b/files/mygui/openmw_settings_window.layout @@ -377,25 +377,25 @@ - + - - + + - + - + diff --git a/files/mygui/openmw_tooltips.layout b/files/mygui/openmw_tooltips.layout index 70246fa4e..bb505d253 100644 --- a/files/mygui/openmw_tooltips.layout +++ b/files/mygui/openmw_tooltips.layout @@ -267,6 +267,13 @@ + + + + + + + diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 0b307ba09..d0793fc81 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -23,7 +23,7 @@ small feature culling pixel size = 2.0 # Maximum visible distance. Caution: this setting # can dramatically affect performance, see documentation for details. -viewing distance = 6656.0 +viewing distance = 7168.0 # Camera field of view in degrees (e.g. 30.0 to 110.0). # Does not affect the player's hands in the first person camera. @@ -361,6 +361,9 @@ trainers training skills based on base skill = false # Make stealing items from NPCs that were knocked down possible during combat. always allow stealing from knocked out actors = false +# Enables visually harvesting plants for models that support it. +graphic herbalism = true + [General] # Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16). diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt index 8012c2bc1..47670e7a0 100644 --- a/files/shaders/CMakeLists.txt +++ b/files/shaders/CMakeLists.txt @@ -22,6 +22,7 @@ set(SHADER_FILES shadows_fragment.glsl shadowcasting_vertex.glsl shadowcasting_fragment.glsl + vertexcolors.glsl ) copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_SHADERS_ROOT} ${DDIRRELATIVE} "${SHADER_FILES}") diff --git a/files/shaders/lighting.glsl b/files/shaders/lighting.glsl index 1ed162eea..930f4de26 100644 --- a/files/shaders/lighting.glsl +++ b/files/shaders/lighting.glsl @@ -1,85 +1,38 @@ #define MAX_LIGHTS 8 -uniform int colorMode; - -const int ColorMode_None = 0; -const int ColorMode_Emission = 1; -const int ColorMode_AmbientAndDiffuse = 2; -const int ColorMode_Ambient = 3; -const int ColorMode_Diffuse = 4; -const int ColorMode_Specular = 5; - -void perLight(out vec3 ambientOut, out vec3 diffuseOut, int lightIndex, vec3 viewPos, vec3 viewNormal, vec4 diffuse, vec3 ambient) +void perLight(out vec3 ambientOut, out vec3 diffuseOut, int lightIndex, vec3 viewPos, vec3 viewNormal) { - vec3 lightDir; - float lightDistance; - - lightDir = gl_LightSource[lightIndex].position.xyz - (viewPos.xyz * gl_LightSource[lightIndex].position.w); - lightDistance = length(lightDir); + vec3 lightDir = gl_LightSource[lightIndex].position.xyz - viewPos * gl_LightSource[lightIndex].position.w; + float lightDistance = length(lightDir); lightDir = normalize(lightDir); float illumination = clamp(1.0 / (gl_LightSource[lightIndex].constantAttenuation + gl_LightSource[lightIndex].linearAttenuation * lightDistance + gl_LightSource[lightIndex].quadraticAttenuation * lightDistance * lightDistance), 0.0, 1.0); - ambientOut = ambient * gl_LightSource[lightIndex].ambient.xyz * illumination; - diffuseOut = diffuse.xyz * gl_LightSource[lightIndex].diffuse.xyz * max(dot(viewNormal.xyz, lightDir), 0.0) * illumination; + ambientOut = gl_LightSource[lightIndex].ambient.xyz * illumination; + diffuseOut = gl_LightSource[lightIndex].diffuse.xyz * max(dot(viewNormal, lightDir), 0.0) * illumination; } #if PER_PIXEL_LIGHTING -vec4 doLighting(vec3 viewPos, vec3 viewNormal, vec4 vertexColor, float shadowing) +void doLighting(vec3 viewPos, vec3 viewNormal, float shadowing, out vec3 diffuseLight, out vec3 ambientLight) #else -vec4 doLighting(vec3 viewPos, vec3 viewNormal, vec4 vertexColor, out vec3 shadowDiffuse) +void doLighting(vec3 viewPos, vec3 viewNormal, out vec3 diffuseLight, out vec3 ambientLight, out vec3 shadowDiffuse) #endif { - vec4 diffuse; - vec3 ambient; - if (colorMode == ColorMode_AmbientAndDiffuse) - { - diffuse = vertexColor; - ambient = vertexColor.xyz; - } - else if (colorMode == ColorMode_Diffuse) - { - diffuse = vertexColor; - ambient = gl_FrontMaterial.ambient.xyz; - } - else if (colorMode == ColorMode_Ambient) - { - diffuse = gl_FrontMaterial.diffuse; - ambient = vertexColor.xyz; - } - else - { - diffuse = gl_FrontMaterial.diffuse; - ambient = gl_FrontMaterial.ambient.xyz; - } - vec4 lightResult = vec4(0.0, 0.0, 0.0, diffuse.a); - - vec3 diffuseLight, ambientLight; - perLight(ambientLight, diffuseLight, 0, viewPos, viewNormal, diffuse, ambient); + vec3 ambientOut, diffuseOut; + // This light gets added a second time in the loop to fix Mesa users' slowdown, so we need to negate its contribution here. + perLight(ambientOut, diffuseOut, 0, viewPos, viewNormal); #if PER_PIXEL_LIGHTING - lightResult.xyz += diffuseLight * shadowing - diffuseLight; // This light gets added a second time in the loop to fix Mesa users' slowdown, so we need to negate its contribution here. + diffuseLight = diffuseOut * shadowing - diffuseOut; #else - shadowDiffuse = diffuseLight; - lightResult.xyz -= shadowDiffuse; // This light gets added a second time in the loop to fix Mesa users' slowdown, so we need to negate its contribution here. + shadowDiffuse = diffuseOut; + diffuseLight = -diffuseOut; #endif + ambientLight = gl_LightModel.ambient.xyz; for (int i=0; i + + + + <html><head/><body><p>If this setting is true, containers supporting graphic herbalism will do so instead of opening the menu.</p></body></html> + + + Enable graphic herbalism + + +