diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c9b8cf9341..a07dec1d68 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,6 +8,9 @@ stages: - docker - linux image: debian:bullseye + rules: + - if: $CI_PIPELINE_SOURCE == "push" + .Debian: extends: .Debian_Image @@ -52,7 +55,7 @@ Coverity: extends: .Debian_Image stage: build rules: - - if: '$CI_PIPELINE_SOURCE == "schedule"' + - if: $CI_PIPELINE_SOURCE == "schedule" before_script: - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic coverity - curl -o /tmp/cov-analysis-linux64.tgz https://scan.coverity.com/download/linux64 --form project=$COVERITY_SCAN_PROJECT_NAME --form token=$COVERITY_SCAN_TOKEN @@ -70,7 +73,6 @@ Coverity: variables: CC: gcc CXX: g++ - artifacts: Debian_GCC: extends: .Debian @@ -162,6 +164,7 @@ Debian_Clang_tests_Debug: only: variables: - $CI_PROJECT_ID == "7107382" + - $CI_PIPELINE_SOURCE == "push" cache: paths: - ccache/ @@ -184,7 +187,6 @@ Debian_Clang_tests_Debug: macOS11_Xcode12: extends: .MacOS image: macos-11-xcode-12 - allow_failure: true cache: key: macOS11_Xcode12.v1 variables: @@ -193,6 +195,7 @@ macOS11_Xcode12: macOS10.15_Xcode11: extends: .MacOS image: macos-10.15-xcode-11 + allow_failure: true cache: key: macOS10.15_Xcode11.v1 variables: @@ -213,6 +216,8 @@ variables: &tests-targets .Windows_Ninja_Base: tags: - windows + rules: + - if: $CI_PIPELINE_SOURCE == "push" before_script: - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1 @@ -329,6 +334,8 @@ Windows_Ninja_Tests_RelWithDebInfo: .Windows_MSBuild_Base: tags: - windows + rules: + - if: $CI_PIPELINE_SOURCE == "push" before_script: - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1 @@ -389,6 +396,15 @@ Windows_Ninja_Tests_RelWithDebInfo: - MSVC2019_64/*/*/*/*/*/*/*.log - MSVC2019_64/*/*/*/*/*/*/*/*.log +Daily_Windows_MSBuild_Engine_Release:on-schedule: + extends: + - .Windows_MSBuild_Base + variables: + <<: *engine-targets + config: "Release" + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + Windows_MSBuild_Engine_Release: extends: - .Windows_MSBuild_Base @@ -444,6 +460,8 @@ Debian_AndroidNDK_arm64-v8a: tags: - linux image: debian:bullseye + rules: + - if: $CI_PIPELINE_SOURCE == "push" variables: CCACHE_SIZE: 3G cache: diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..e0b39ec495 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,10 @@ +version: 2 + +sphinx: + configuration: docs/source/conf.py + +python: + version: 3.8 + install: + - requirements: docs/requirements.txt + diff --git a/CHANGELOG.md b/CHANGELOG.md index d38729248e..c491cb83c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,14 @@ Bug #3246: ESSImporter: Most NPCs are dead on save load Bug #3514: Editing a reference's position after loading an esp file makes the reference disappear Bug #3737: Scripts from The Underground 2 .esp do not play (all patched versions) + Bug #3792: 1 frame late magicka recalc breaks early scripted magicka reactions to Intelligence change Bug #3846: Strings starting with "-" fail to compile if not enclosed in quotes Bug #3905: Great House Dagoth issues Bug #4203: Resurrecting an actor should close the loot GUI Bug #4602: Robert's Bodies: crash inside createInstance() Bug #4700: Editor: Incorrect command implementation Bug #4744: Invisible particles must still be processed + Bug #5088: Sky abruptly changes direction during certain weather transitions Bug #5100: Persuasion doesn't always clamp the resulting disposition Bug #5120: Scripted object spawning updates physics system Bug #5207: Loose summons can be present in scene @@ -25,6 +27,8 @@ Bug #5801: A multi-effect spell with the intervention effects and recall always favors Almsivi intervention Bug #5842: GetDisposition adds temporary disposition change from different actors Bug #5863: GetEffect should return true after the player has teleported + Bug #5913: Failed assertion during Ritual of Trees quest + Bug #5937: Lights always need to be rotated by 90 degrees Bug #6037: Morrowind Content Language Cannot be Set to English in OpenMW Launcher Bug #6051: NaN water height in ESM file is not handled gracefully Bug #6066: addtopic "return" does not work from within script. No errors thrown @@ -40,10 +44,13 @@ Bug #6133: Cannot reliably sneak or steal in the sight of the NPCs siding with player Bug #6143: Capturing a screenshot makes engine to be a temporary unresponsive Bug #6165: Paralyzed player character can pickup items when the inventory is open + Bug #6168: Weather particles flicker for a frame at start of storms Bug #6172: Some creatures can't open doors Bug #6174: Spellmaking and Enchanting sliders differences from vanilla Bug #6184: Command and Calm and Demoralize and Frenzy and Rally magic effects inconsistencies with vanilla Bug #6197: Infinite Casting Loop + Bug #6253: Multiple instances of Reflect stack additively + Bug #6255: Reflect is different from vanilla Bug #6258: Barter menu glitches out when modifying prices Bug #6273: Respawning NPCs rotation is inconsistent Bug #6282: Laura craft doesn't follow the player character @@ -57,6 +64,9 @@ Bug #6322: Total sold/cost should reset to 0 when there are no items offered Bug #6323: Wyrmhaven: Alboin doesn't follower the player character out of his house Bug #6326: Detect Enchantment/Key should detect items in unresolved containers + Bug #6347: PlaceItem/PlaceItemCell/PlaceAt should work with levelled creatures + Bug #6363: Some scripts in Morrowland fail to work + Bug #6376: Creatures should be able to use torches Feature #890: OpenMW-CS: Column filtering Feature #2554: Modifying an object triggers the instances table to scroll to the corresponding record Feature #2780: A way to see current OpenMW version in the console @@ -72,6 +82,7 @@ Feature #6017: Separate persistent and temporary cell references when saving Feature #6032: Reverse-z depth buffer Feature #6078: First person should not clear depth buffer + Feature #6161: Refactor Sky to use shaders and GLES/GL3 friendly Feature #6162: Refactor GUI to use shaders and to be GLES and GL3+ friendly Feature #6199: Support FBO Rendering Feature #6249: Alpha testing support for Collada @@ -80,7 +91,6 @@ Task #6201: Remove the "Note: No relevant classes found. No output generated" warnings Task #6264: Remove the old classes in animation.cpp - 0.47.0 ------ diff --git a/CMakeLists.txt b/CMakeLists.txt index 0451b639e5..f2ee87b2ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -199,6 +199,10 @@ if (WIN32) option(USE_DEBUG_CONSOLE "whether a debug console should be enabled for debug builds, if false debug output is redirected to Visual Studio output" ON) endif() +if(MSVC) + add_compile_options("/utf-8") +endif() + # Dependencies find_package(OpenGL REQUIRED) @@ -707,6 +711,8 @@ endif() if (BUILD_OPENMW AND APPLE) # Without these flags LuaJit crashes on startup on OSX set_target_properties(openmw PROPERTIES LINK_FLAGS "-pagezero_size 10000 -image_base 100000000") + target_compile_definitions(components PRIVATE GL_SILENCE_DEPRECATION=1) + target_compile_definitions(openmw PRIVATE GL_SILENCE_DEPRECATION=1) endif() # Apple bundling diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 24729d5096..4c66668e4a 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -32,7 +32,7 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: { ui.setupUi (this); setObjectName ("DataFilesPage"); - mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); + mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget, /*showOMWScripts=*/true); const QString encoding = mGameSettings.value("encoding", "win1252"); mSelector->setEncoding(encoding); diff --git a/apps/launcher/utils/cellnameloader.cpp b/apps/launcher/utils/cellnameloader.cpp index 594946394c..e8ba54651c 100644 --- a/apps/launcher/utils/cellnameloader.cpp +++ b/apps/launcher/utils/cellnameloader.cpp @@ -10,6 +10,8 @@ QSet CellNameLoader::getCellNames(QStringList &contentPaths) // Loop through all content files for (auto &contentPath : contentPaths) { + if (contentPath.endsWith(".omwscripts", Qt::CaseInsensitive)) + continue; esmReader.open(contentPath.toStdString()); // Loop through all records diff --git a/apps/opencs/model/world/collection.hpp b/apps/opencs/model/world/collection.hpp index 6ab9d7ff9d..d15a4ff54d 100644 --- a/apps/opencs/model/world/collection.hpp +++ b/apps/opencs/model/world/collection.hpp @@ -296,7 +296,7 @@ namespace CSMWorld const std::string& destination, const UniversalId::Type type) { int index = cloneRecordImp(origin, destination, type); - mRecords.at(index)->get().mPlugin = 0; + mRecords.at(index)->get().setPlugin(0); } template @@ -311,7 +311,7 @@ namespace CSMWorld int index = touchRecordImp(id); if (index >= 0) { - mRecords.at(index)->get().mPlugin = 0; + mRecords.at(index)->get().setPlugin(0); return true; } diff --git a/apps/opencs/model/world/columnimp.cpp b/apps/opencs/model/world/columnimp.cpp index bec5008d35..0244ab1e8d 100644 --- a/apps/opencs/model/world/columnimp.cpp +++ b/apps/opencs/model/world/columnimp.cpp @@ -52,7 +52,7 @@ namespace CSMWorld QVariant LandPluginIndexColumn::get(const Record& record) const { - return record.get().mPlugin; + return record.get().getPlugin(); } bool LandPluginIndexColumn::isEditable() const diff --git a/apps/opencs/model/world/scriptcontext.cpp b/apps/opencs/model/world/scriptcontext.cpp index 344ae322e9..9b465d7ffb 100644 --- a/apps/opencs/model/world/scriptcontext.cpp +++ b/apps/opencs/model/world/scriptcontext.cpp @@ -102,11 +102,6 @@ bool CSMWorld::ScriptContext::isId (const std::string& name) const return std::binary_search (mIds.begin(), mIds.end(), Misc::StringUtils::lowerCase (name)); } -bool CSMWorld::ScriptContext::isJournalId (const std::string& name) const -{ - return mData.getJournals().searchId (name)!=-1; -} - void CSMWorld::ScriptContext::invalidateIds() { mIdsUpdated = false; diff --git a/apps/opencs/model/world/scriptcontext.hpp b/apps/opencs/model/world/scriptcontext.hpp index 8e1a5e57b8..cb08fc70bd 100644 --- a/apps/opencs/model/world/scriptcontext.hpp +++ b/apps/opencs/model/world/scriptcontext.hpp @@ -39,9 +39,6 @@ namespace CSMWorld bool isId (const std::string& name) const override; ///< Does \a name match an ID, that can be referenced? - bool isJournalId (const std::string& name) const override; - ///< Does \a name match a journal ID? - void invalidateIds(); void clear(); diff --git a/apps/opencs/view/doc/filedialog.cpp b/apps/opencs/view/doc/filedialog.cpp index 8ff063ed31..c3d0d8cc50 100644 --- a/apps/opencs/view/doc/filedialog.cpp +++ b/apps/opencs/view/doc/filedialog.cpp @@ -24,7 +24,7 @@ CSVDoc::FileDialog::FileDialog(QWidget *parent) : resize(400, 400); setObjectName ("FileDialog"); - mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); + mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget, /*showOMWScripts=*/false); mAdjusterWidget = new AdjusterWidget (this); } diff --git a/apps/opencs/view/render/actor.cpp b/apps/opencs/view/render/actor.cpp index 271ca2365a..94b82c96cd 100644 --- a/apps/opencs/view/render/actor.cpp +++ b/apps/opencs/view/render/actor.cpp @@ -111,7 +111,7 @@ namespace CSVRender if (!mesh.empty() && node != mNodeMap.end()) { auto instance = sceneMgr->getInstance(mesh); - SceneUtil::attach(instance, mSkeleton, boneName, node->second); + SceneUtil::attach(instance, mSkeleton, boneName, node->second, sceneMgr); } } diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index 789fad0587..9f4fd29966 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -308,7 +308,7 @@ osg::ref_ptr CSVRender::Object::makeRotateMarker (int axis) const float OuterRadius = InnerRadius + MarkerShaftWidth; const float SegmentDistance = 100.f; - const size_t SegmentCount = std::min(64, std::max(24, (int)(OuterRadius * 2 * osg::PI / SegmentDistance))); + const size_t SegmentCount = std::clamp(OuterRadius * 2 * osg::PI / SegmentDistance, 24, 64); const size_t VerticesPerSegment = 4; const size_t IndicesPerSegment = 24; diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 7334d893db..f22204eb8c 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -19,7 +19,7 @@ set(GAME_HEADER source_group(game FILES ${GAME} ${GAME_HEADER}) add_openmw_dir (mwrender - actors objects renderingmanager animation rotatecontroller sky npcanimation vismask + actors objects renderingmanager animation rotatecontroller sky skyutil npcanimation vismask creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager bulletdebugdraw globalmap characterpreview camera viewovershoulder localmap water terrainstorage ripplesimulation renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover postprocessor @@ -72,7 +72,7 @@ add_openmw_dir (mwworld containerstore actiontalk actiontake manualref player cellvisitors failedaction cells localscripts customdata inventorystore ptr actionopen actionread actionharvest actionequip timestamp actionalchemy cellstore actionapply actioneat - store esmstore recordcmp fallback actionrepair actionsoulgem livecellref actiondoor + store esmstore fallback actionrepair actionsoulgem livecellref actiondoor contentloader esmloader actiontrap cellreflist cellref weather projectilemanager cellpreloader datetimemanager ) @@ -94,7 +94,7 @@ add_openmw_dir (mwmechanics aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning character actors objects aistate trading weaponpriority spellpriority weapontype spellutil - spellabsorption spelleffects + spelleffects ) add_openmw_dir (mwstate diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index bbe4f25f69..58dabd2cee 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -495,11 +495,6 @@ void OMW::Engine::addGroundcoverFile(const std::string& file) mGroundcoverFiles.emplace_back(file); } -void OMW::Engine::addLuaScriptListFile(const std::string& file) -{ - mLuaScriptListFiles.push_back(file); -} - void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame) { mSkipMenu = skipMenu; @@ -674,7 +669,7 @@ void OMW::Engine::setWindowIcon() void OMW::Engine::prepareEngine (Settings::Manager & settings) { mEnvironment.setStateManager ( - new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles.at (0))); + new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles)); createWindow(settings); @@ -714,7 +709,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) mViewer->addEventHandler(mScreenCaptureHandler); - mLuaManager = new MWLua::LuaManager(mVFS.get(), mLuaScriptListFiles); + mLuaManager = new MWLua::LuaManager(mVFS.get()); mEnvironment.setLuaManager(mLuaManager); // Create input and UI first to set up a bootstrapping environment for diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 290fd890a6..5fe032d27e 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -72,7 +72,6 @@ namespace OMW std::string mCellName; std::vector mContentFiles; std::vector mGroundcoverFiles; - std::vector mLuaScriptListFiles; bool mSkipMenu; bool mUseSound; bool mCompileAll; @@ -146,7 +145,6 @@ namespace OMW */ void addContentFile(const std::string& file); void addGroundcoverFile(const std::string& file); - void addLuaScriptListFile(const std::string& file); /// Disable or enable all sounds void setSoundUsage(bool soundUsage); diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index d1f9fd1787..1cf7abe2f2 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -124,9 +124,11 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat engine.addGroundcoverFile(file); } - StringsVector luaScriptLists = variables["lua-scripts"].as().toStdStringVector(); - for (const auto& file : luaScriptLists) - engine.addLuaScriptListFile(file); + if (variables.count("lua-scripts")) + { + Log(Debug::Warning) << "Lua scripts have been specified via the old lua-scripts option and will not be loaded. " + "Please update them to a version which uses the new omwscripts format."; + } // startup-settings engine.setCell(variables["start"].as().toStdString()); diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index ebcd8f50b3..cc479a2937 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -30,6 +30,7 @@ namespace MWBase virtual ~LuaManager() = default; virtual void newGameStarted() = 0; + virtual void gameLoaded() = 0; virtual void registerObject(const MWWorld::Ptr& ptr) = 0; virtual void deregisterObject(const MWWorld::Ptr& ptr) = 0; virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0; diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 8b33095fde..c1a8d09120 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -289,7 +289,7 @@ namespace MWBase virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) = 0; ///< @return an updated Ptr - virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) = 0; + virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, const osg::Vec3f& vec) = 0; ///< @return an updated Ptr virtual void scaleObject (const MWWorld::Ptr& ptr, float scale) = 0; diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index a9b1f461ca..0b3ffa924f 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -82,7 +82,7 @@ namespace MWClass const Creature::GMST& Creature::getGmst() { - static const GMST gmst = [] + static const GMST staticGmst = [] { GMST gmst; @@ -105,14 +105,17 @@ namespace MWClass return gmst; } (); - return gmst; + return staticGmst; } void Creature::ensureCustomData (const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { - std::unique_ptr data (new CreatureCustomData); + auto tempData = std::make_unique(); + CreatureCustomData* data = tempData.get(); + MWMechanics::CreatureCustomDataResetter resetter(ptr); + ptr.getRefData().setCustomData(std::move(tempData)); MWWorld::LiveCellRef *ref = ptr.get(); @@ -156,10 +159,7 @@ namespace MWClass data->mCreatureStats.setGoldPool(ref->mBase->mData.mGold); - data->mCreatureStats.setNeedRecalcDynamicStats(false); - - // store - ptr.getRefData().setCustomData(std::move(data)); + resetter.mPtr = {}; getContainerStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId()); diff --git a/apps/openmw/mwclass/creaturelevlist.cpp b/apps/openmw/mwclass/creaturelevlist.cpp index e8023ce26b..ddb5fd2b38 100644 --- a/apps/openmw/mwclass/creaturelevlist.cpp +++ b/apps/openmw/mwclass/creaturelevlist.cpp @@ -5,6 +5,7 @@ #include "../mwmechanics/levelledlist.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwworld/customdata.hpp" #include "../mwmechanics/creaturestats.hpp" @@ -27,6 +28,24 @@ namespace MWClass } }; + MWWorld::Ptr CreatureLevList::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + { + const MWWorld::LiveCellRef *ref = ptr.get(); + + return MWWorld::Ptr(cell.insert(ref), &cell); + } + + void CreatureLevList::adjustPosition(const MWWorld::Ptr& ptr, bool force) const + { + if (ptr.getRefData().getCustomData() == nullptr) + return; + + CreatureLevListCustomData& customData = ptr.getRefData().getCustomData()->asCreatureLevListCustomData(); + MWWorld::Ptr creature = (customData.mSpawnActorId == -1) ? MWWorld::Ptr() : MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); + if (!creature.isEmpty()) + MWBase::Environment::get().getWorld()->adjustPosition(creature, force); + } + std::string CreatureLevList::getName (const MWWorld::ConstPtr& ptr) const { return ""; diff --git a/apps/openmw/mwclass/creaturelevlist.hpp b/apps/openmw/mwclass/creaturelevlist.hpp index 35152a9422..b3a940682c 100644 --- a/apps/openmw/mwclass/creaturelevlist.hpp +++ b/apps/openmw/mwclass/creaturelevlist.hpp @@ -32,6 +32,10 @@ namespace MWClass ///< Write additional state from \a ptr into \a state. void respawn (const MWWorld::Ptr& ptr) const override; + + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + + void adjustPosition(const MWWorld::Ptr& ptr, bool force) const override; }; } diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index becfb8719f..5d50ba558f 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -266,7 +266,7 @@ namespace MWClass const Npc::GMST& Npc::getGmst() { - static const GMST gmst = [] + static const GMST staticGmst = [] { GMST gmst; @@ -296,14 +296,18 @@ namespace MWClass return gmst; } (); - return gmst; + return staticGmst; } void Npc::ensureCustomData (const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { - std::unique_ptr data(new NpcCustomData); + bool recalculate = false; + auto tempData = std::make_unique(); + NpcCustomData* data = tempData.get(); + MWMechanics::CreatureCustomDataResetter resetter(ptr); + ptr.getRefData().setCustomData(std::move(tempData)); MWWorld::LiveCellRef *ref = ptr.get(); @@ -334,8 +338,6 @@ namespace MWClass data->mNpcStats.setLevel(ref->mBase->mNpdt.mLevel); data->mNpcStats.setBaseDisposition(ref->mBase->mNpdt.mDisposition); data->mNpcStats.setReputation(ref->mBase->mNpdt.mReputation); - - data->mNpcStats.setNeedRecalcDynamicStats(false); } else { @@ -351,7 +353,7 @@ namespace MWClass autoCalculateAttributes(ref->mBase, data->mNpcStats); autoCalculateSkills(ref->mBase, data->mNpcStats, ptr, spellsInitialised); - data->mNpcStats.setNeedRecalcDynamicStats(true); + recalculate = true; } // Persistent actors with 0 health do not play death animation @@ -387,7 +389,9 @@ namespace MWClass data->mNpcStats.setGoldPool(gold); // store - ptr.getRefData().setCustomData(std::move(data)); + resetter.mPtr = {}; + if(recalculate) + data->mNpcStats.recalculateMagicka(); // inventory // setting ownership is used to make the NPC auto-equip his initial equipment only, and not bartered items diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index 8aa6acd8ed..5270c0143a 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -421,7 +421,7 @@ namespace MWDialogue // Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with intimidate) npcStats.setBaseDisposition(0); int zero = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false); - int disposition = std::min(100 - zero, std::max(mOriginalDisposition + mPermanentDispositionChange, -zero)); + int disposition = std::clamp(mOriginalDisposition + mPermanentDispositionChange, -zero, 100 - zero); npcStats.setBaseDisposition(disposition); } diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 1da77f5d06..86cad0fa7a 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -347,8 +347,7 @@ namespace MWGui { if (!mScrollBar->getVisible()) return; - mScrollBar->setScrollPosition(std::min(static_cast(mScrollBar->getScrollRange()-1), - std::max(0, static_cast(mScrollBar->getScrollPosition() - _rel*0.3)))); + mScrollBar->setScrollPosition(std::clamp(mScrollBar->getScrollPosition() - _rel*0.3, 0, mScrollBar->getScrollRange() - 1)); onScrollbarMoved(mScrollBar, mScrollBar->getScrollPosition()); } diff --git a/apps/openmw/mwgui/enchantingdialog.cpp b/apps/openmw/mwgui/enchantingdialog.cpp index d0d2118c6e..ee0067f082 100644 --- a/apps/openmw/mwgui/enchantingdialog.cpp +++ b/apps/openmw/mwgui/enchantingdialog.cpp @@ -109,7 +109,7 @@ namespace MWGui { mEnchantmentPoints->setCaption(std::to_string(static_cast(mEnchanting.getEnchantPoints(false))) + " / " + std::to_string(mEnchanting.getMaxEnchantValue())); mCharge->setCaption(std::to_string(mEnchanting.getGemCharge())); - mSuccessChance->setCaption(std::to_string(std::max(0, std::min(100, mEnchanting.getEnchantChance())))); + mSuccessChance->setCaption(std::to_string(std::clamp(mEnchanting.getEnchantChance(), 0, 100))); mCastCost->setCaption(std::to_string(mEnchanting.getEffectiveCastCost())); mPrice->setCaption(std::to_string(mEnchanting.getEnchantPrice())); diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index 5a7b5a9590..ab596137cd 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -610,7 +610,7 @@ namespace MWGui static const float fNPCHealthBarFade = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCHealthBarFade")->mValue.getFloat(); if (fNPCHealthBarFade > 0.f) - mEnemyHealth->setAlpha(std::max(0.f, std::min(1.f, mEnemyHealthTimer/fNPCHealthBarFade))); + mEnemyHealth->setAlpha(std::clamp(mEnemyHealthTimer / fNPCHealthBarFade, 0.f, 1.f)); } diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index a586991888..0e76c2429f 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -70,7 +70,7 @@ namespace MWGui , mTrading(false) , mUpdateTimer(0.f) { - mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); + mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture(), mPreview->getTextureStateSet())); mPreview->rebuild(); mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &InventoryWindow::onWindowResize); diff --git a/apps/openmw/mwgui/journalviewmodel.cpp b/apps/openmw/mwgui/journalviewmodel.cpp index 6b38cd0d9d..fc3fcc3efe 100644 --- a/apps/openmw/mwgui/journalviewmodel.cpp +++ b/apps/openmw/mwgui/journalviewmodel.cpp @@ -313,9 +313,9 @@ struct JournalViewModelImpl : JournalViewModel for (MWBase::Journal::TTopicIter i = journal->topicBegin (); i != journal->topicEnd (); ++i) { Utf8Stream stream (i->first.c_str()); - Utf8Stream::UnicodeChar first = Misc::StringUtils::toLowerUtf8(stream.peek()); + Utf8Stream::UnicodeChar first = Utf8Stream::toLowerUtf8(stream.peek()); - if (first != Misc::StringUtils::toLowerUtf8(character)) + if (first != Utf8Stream::toLowerUtf8(character)) continue; visitor (i->second.getName()); diff --git a/apps/openmw/mwgui/keyboardnavigation.cpp b/apps/openmw/mwgui/keyboardnavigation.cpp index b718b712c0..3220e16b94 100644 --- a/apps/openmw/mwgui/keyboardnavigation.cpp +++ b/apps/openmw/mwgui/keyboardnavigation.cpp @@ -273,7 +273,7 @@ bool KeyboardNavigation::switchFocus(int direction, bool wrap) if (wrap) index = (index + keyFocusList.size())%keyFocusList.size(); else - index = std::min(std::max(0, index), static_cast(keyFocusList.size())-1); + index = std::clamp(index, 0, keyFocusList.size() - 1); MyGUI::Widget* next = keyFocusList[index]; int vertdiff = next->getTop() - focus->getTop(); diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index 457594697d..d30eb65eb3 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -138,7 +138,7 @@ namespace MWGui mPreview->rebuild(); mPreview->setAngle (mCurrentAngle); - mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); + mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture(), mPreview->getTextureStateSet())); mPreviewImage->setRenderItemTexture(mPreviewTexture.get()); mPreviewImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 28143c1af4..28d20def6d 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -171,7 +171,7 @@ namespace MWGui else valueStr = MyGUI::utility::toString(int(value)); - value = std::max(min, std::min(value, max)); + value = std::clamp(value, min, max); value = (value-min)/(max-min); scroll->setScrollPosition(static_cast(value * (scroll->getScrollRange() - 1))); diff --git a/apps/openmw/mwgui/sortfilteritemmodel.cpp b/apps/openmw/mwgui/sortfilteritemmodel.cpp index eb7ebd0e43..9d6ed49d3d 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.cpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.cpp @@ -1,6 +1,7 @@ #include "sortfilteritemmodel.hpp" #include +#include #include #include #include @@ -69,8 +70,8 @@ namespace return compareType(leftType, rightType); // compare items by name - std::string leftName = Misc::StringUtils::lowerCaseUtf8(left.mBase.getClass().getName(left.mBase)); - std::string rightName = Misc::StringUtils::lowerCaseUtf8(right.mBase.getClass().getName(right.mBase)); + std::string leftName = Utf8Stream::lowerCaseUtf8(left.mBase.getClass().getName(left.mBase)); + std::string rightName = Utf8Stream::lowerCaseUtf8(right.mBase.getClass().getName(right.mBase)); result = leftName.compare(rightName); if (result != 0) @@ -213,7 +214,7 @@ namespace MWGui if (!mNameFilter.empty()) { - const auto itemName = Misc::StringUtils::lowerCaseUtf8(base.getClass().getName(base)); + const auto itemName = Utf8Stream::lowerCaseUtf8(base.getClass().getName(base)); return itemName.find(mNameFilter) != std::string::npos; } @@ -226,7 +227,7 @@ namespace MWGui for (const auto& effect : effects) { - const auto ciEffect = Misc::StringUtils::lowerCaseUtf8(effect); + const auto ciEffect = Utf8Stream::lowerCaseUtf8(effect); if (ciEffect.find(mEffectFilter) != std::string::npos) return true; @@ -285,7 +286,7 @@ namespace MWGui return false; } - std::string compare = Misc::StringUtils::lowerCaseUtf8(item.mBase.getClass().getName(item.mBase)); + std::string compare = Utf8Stream::lowerCaseUtf8(item.mBase.getClass().getName(item.mBase)); if(compare.find(mNameFilter) == std::string::npos) return false; @@ -318,12 +319,12 @@ namespace MWGui void SortFilterItemModel::setNameFilter (const std::string& filter) { - mNameFilter = Misc::StringUtils::lowerCaseUtf8(filter); + mNameFilter = Utf8Stream::lowerCaseUtf8(filter); } void SortFilterItemModel::setEffectFilter (const std::string& filter) { - mEffectFilter = Misc::StringUtils::lowerCaseUtf8(filter); + mEffectFilter = Utf8Stream::lowerCaseUtf8(filter); } void SortFilterItemModel::update() diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index 61ea9ce93a..455f167415 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -1,6 +1,7 @@ #include "spellmodel.hpp" #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -69,7 +70,7 @@ namespace MWGui fullEffectName += " " + wm->getGameSettingString(ESM::Attribute::sGmstAttributeIds[effect.mAttribute], ""); } - std::string convert = Misc::StringUtils::lowerCaseUtf8(fullEffectName); + std::string convert = Utf8Stream::lowerCaseUtf8(fullEffectName); if (convert.find(filter) != std::string::npos) { return true; @@ -90,14 +91,14 @@ namespace MWGui const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); - std::string filter = Misc::StringUtils::lowerCaseUtf8(mFilter); + std::string filter = Utf8Stream::lowerCaseUtf8(mFilter); for (const ESM::Spell* spell : spells) { if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) continue; - std::string name = Misc::StringUtils::lowerCaseUtf8(spell->mName); + std::string name = Utf8Stream::lowerCaseUtf8(spell->mName); if (name.find(filter) == std::string::npos && !matchingEffectExists(filter, spell->mEffects)) @@ -139,7 +140,7 @@ namespace MWGui if (enchant->mData.mType != ESM::Enchantment::WhenUsed && enchant->mData.mType != ESM::Enchantment::CastOnce) continue; - std::string name = Misc::StringUtils::lowerCaseUtf8(item.getClass().getName(item)); + std::string name = Utf8Stream::lowerCaseUtf8(item.getClass().getName(item)); if (name.find(filter) == std::string::npos && !matchingEffectExists(filter, enchant->mEffects)) diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp index 8e6f951291..0830af0744 100644 --- a/apps/openmw/mwgui/statswindow.cpp +++ b/apps/openmw/mwgui/statswindow.cpp @@ -599,8 +599,7 @@ namespace MWGui text += "\n#{fontcolourhtml=normal}#{sExpelled}"; else { - int rank = factionPair.second; - rank = std::max(0, std::min(9, rank)); + const int rank = std::clamp(factionPair.second, 0, 9); text += std::string("\n#{fontcolourhtml=normal}") + faction->mRanks[rank]; if (rank < 9) diff --git a/apps/openmw/mwinput/controllermanager.cpp b/apps/openmw/mwinput/controllermanager.cpp index fa10ce03cd..bdb46e31a8 100644 --- a/apps/openmw/mwinput/controllermanager.cpp +++ b/apps/openmw/mwinput/controllermanager.cpp @@ -70,7 +70,7 @@ namespace MWInput } float deadZoneRadius = Settings::Manager::getFloat("joystick dead zone", "Input"); - deadZoneRadius = std::min(std::max(deadZoneRadius, 0.0f), 0.5f); + deadZoneRadius = std::clamp(deadZoneRadius, 0.f, 0.5f); mBindingsManager->setJoystickDeadZone(deadZoneRadius); } diff --git a/apps/openmw/mwinput/mousemanager.cpp b/apps/openmw/mwinput/mousemanager.cpp index 7810a40ad2..f2bd4505d1 100644 --- a/apps/openmw/mwinput/mousemanager.cpp +++ b/apps/openmw/mwinput/mousemanager.cpp @@ -245,8 +245,8 @@ namespace MWInput mMouseWheel += mouseWheelMove; const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - mGuiCursorX = std::max(0.f, std::min(mGuiCursorX, float(viewSize.width - 1))); - mGuiCursorY = std::max(0.f, std::min(mGuiCursorY, float(viewSize.height - 1))); + mGuiCursorX = std::clamp(mGuiCursorX, 0.f, viewSize.width - 1); + mGuiCursorY = std::clamp(mGuiCursorY, 0.f, viewSize.height - 1); MyGUI::InputManager::getInstance().injectMouseMove(static_cast(mGuiCursorX), static_cast(mGuiCursorY), static_cast(mMouseWheel)); } diff --git a/apps/openmw/mwlua/actions.cpp b/apps/openmw/mwlua/actions.cpp index aad70d1a57..92a7f915bb 100644 --- a/apps/openmw/mwlua/actions.cpp +++ b/apps/openmw/mwlua/actions.cpp @@ -1,5 +1,7 @@ #include "actions.hpp" +#include + #include #include "../mwworld/cellstore.hpp" diff --git a/apps/openmw/mwlua/asyncbindings.cpp b/apps/openmw/mwlua/asyncbindings.cpp index 9fdda53d9d..9bddf75ee4 100644 --- a/apps/openmw/mwlua/asyncbindings.cpp +++ b/apps/openmw/mwlua/asyncbindings.cpp @@ -23,7 +23,7 @@ namespace MWLua sol::usertype api = context.mLua->sol().new_usertype("AsyncPackage"); api["registerTimerCallback"] = [](const AsyncPackageId& asyncId, std::string_view name, sol::function callback) { - asyncId.mContainer->registerTimerCallback(asyncId.mScript, name, std::move(callback)); + asyncId.mContainer->registerTimerCallback(asyncId.mScriptId, name, std::move(callback)); return TimerCallback{asyncId, std::string(name)}; }; api["newTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId&, double delay, @@ -31,35 +31,34 @@ namespace MWLua { callback.mAsyncId.mContainer->setupSerializableTimer( TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, - callback.mAsyncId.mScript, callback.mName, std::move(callbackArg)); + callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg)); }; api["newTimerInHours"] = [world=context.mWorldView](const AsyncPackageId&, double delay, const TimerCallback& callback, sol::object callbackArg) { callback.mAsyncId.mContainer->setupSerializableTimer( TimeUnit::HOURS, world->getGameTimeInHours() + delay, - callback.mAsyncId.mScript, callback.mName, std::move(callbackArg)); + callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg)); }; api["newUnsavableTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) { asyncId.mContainer->setupUnsavableTimer( - TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, asyncId.mScript, std::move(callback)); + TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, asyncId.mScriptId, std::move(callback)); }; api["newUnsavableTimerInHours"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) { asyncId.mContainer->setupUnsavableTimer( - TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScript, std::move(callback)); + TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScriptId, std::move(callback)); }; api["callback"] = [](const AsyncPackageId& asyncId, sol::function fn) { - return Callback{std::move(fn), asyncId.mHiddenData}; + return LuaUtil::Callback{std::move(fn), asyncId.mHiddenData}; }; auto initializer = [](sol::table hiddenData) { - LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY]; - hiddenData[Callback::SCRIPT_NAME_KEY] = id.toString(); - return AsyncPackageId{id.mContainer, id.mPath, hiddenData}; + LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey]; + return AsyncPackageId{id.mContainer, id.mIndex, hiddenData}; }; return sol::make_object(context.mLua->sol(), initializer); } diff --git a/apps/openmw/mwlua/globalscripts.hpp b/apps/openmw/mwlua/globalscripts.hpp index 9a371809ac..2737dabaca 100644 --- a/apps/openmw/mwlua/globalscripts.hpp +++ b/apps/openmw/mwlua/globalscripts.hpp @@ -16,7 +16,8 @@ namespace MWLua class GlobalScripts : public LuaUtil::ScriptsContainer { public: - GlobalScripts(LuaUtil::LuaState* lua) : LuaUtil::ScriptsContainer(lua, "Global") + GlobalScripts(LuaUtil::LuaState* lua) : + LuaUtil::ScriptsContainer(lua, "Global", ESM::LuaScriptCfg::sGlobal) { registerEngineHandlers({&mActorActiveHandlers, &mNewGameHandlers, &mPlayerAddedHandlers}); } diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 8a1b76a8ce..ee23b4b90c 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -82,14 +82,14 @@ namespace MWLua }; } - LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj) - : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id())), mData(obj) + LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode) + : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id()), autoStartMode), mData(obj) { this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData)); registerEngineHandlers({&mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers}); } - void LocalScripts::receiveEngineEvent(const EngineEvent& event, ObjectRegistry*) + void LocalScripts::receiveEngineEvent(const EngineEvent& event) { std::visit([this](auto&& arg) { diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index 80d04b7a40..68da0b8b03 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -20,7 +20,7 @@ namespace MWLua { public: static void initializeSelfPackage(const Context&); - LocalScripts(LuaUtil::LuaState* lua, const LObject& obj); + LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode); MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; } @@ -39,7 +39,7 @@ namespace MWLua }; using EngineEvent = std::variant; - void receiveEngineEvent(const EngineEvent&, ObjectRegistry*); + void receiveEngineEvent(const EngineEvent&); protected: SelfObject mData; diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index aceffc24db..1c05debc73 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -25,7 +25,7 @@ namespace MWLua { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 7; + api["API_REVISION"] = 8; api["quit"] = [lua]() { std::string traceback = lua->sol()["debug"]["traceback"]().get(); diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp index d1c62e43e3..aad3183734 100644 --- a/apps/openmw/mwlua/luabindings.hpp +++ b/apps/openmw/mwlua/luabindings.hpp @@ -48,7 +48,7 @@ namespace MWLua struct AsyncPackageId { LuaUtil::ScriptsContainer* mContainer; - std::string mScript; + int mScriptId; sol::table mHiddenData; }; sol::function getAsyncPackageInitializer(const Context&); diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 38055c99b7..4e47bf7167 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -7,11 +7,11 @@ #include #include -#include #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/ptr.hpp" #include "luabindings.hpp" @@ -20,10 +20,9 @@ namespace MWLua { - LuaManager::LuaManager(const VFS::Manager* vfs, const std::vector& scriptLists) : mLua(vfs) + LuaManager::LuaManager(const VFS::Manager* vfs) : mLua(vfs, &mConfiguration) { Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion(); - mGlobalScriptList = LuaUtil::parseOMWScriptsFiles(vfs, scriptLists); mGlobalSerializer = createUserdataSerializer(false, mWorldView.getObjectRegistry()); mLocalSerializer = createUserdataSerializer(true, mWorldView.getObjectRegistry()); @@ -33,6 +32,14 @@ namespace MWLua mGlobalScripts.setSerializer(mGlobalSerializer.get()); } + void LuaManager::initConfiguration() + { + mConfiguration.init(MWBase::Environment::get().getWorld()->getStore().getLuaScriptsCfg()); + Log(Debug::Verbose) << "Lua scripts configuration (" << mConfiguration.size() << " scripts):"; + for (size_t i = 0; i < mConfiguration.size(); ++i) + Log(Debug::Verbose) << "#" << i << " " << LuaUtil::scriptCfgToString(mConfiguration[i]); + } + void LuaManager::init() { Context context; @@ -67,23 +74,10 @@ namespace MWLua mLocalSettingsPackage = initLocalSettingsPackage(localContext); mPlayerSettingsPackage = initPlayerSettingsPackage(localContext); - mInputEvents.clear(); - for (const std::string& path : mGlobalScriptList) - if (mGlobalScripts.addNewScript(path)) - Log(Debug::Info) << "Global script started: " << path; + initConfiguration(); mInitialized = true; } - void Callback::operator()(sol::object arg) const - { - if (mHiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY] != sol::nil) - LuaUtil::call(mFunc, std::move(arg)); - else - { - Log(Debug::Debug) << "Ignored callback to removed script " << mHiddenData.get(SCRIPT_NAME_KEY); - } - } - void LuaManager::update(bool paused, float dt) { ObjectRegistry* objectRegistry = mWorldView.getObjectRegistry(); @@ -160,7 +154,7 @@ namespace MWLua } LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts(); if (scripts) - scripts->receiveEngineEvent(e.mEvent, objectRegistry); + scripts->receiveEngineEvent(e.mEvent); } mLocalEngineEvents.clear(); @@ -173,6 +167,11 @@ namespace MWLua mPlayerChanged = false; mGlobalScripts.playerAdded(GObject(getId(mPlayer), objectRegistry)); } + if (mNewGameStarted) + { + mNewGameStarted = false; + mGlobalScripts.newGameStarted(); + } for (ObjectId id : mActorAddedEvents) mGlobalScripts.actorActive(GObject(id, objectRegistry)); @@ -205,8 +204,11 @@ namespace MWLua mInputEvents.clear(); mActorAddedEvents.clear(); mLocalEngineEvents.clear(); + mNewGameStarted = false; mPlayerChanged = false; mWorldView.clear(); + mGlobalScripts.removeAllScripts(); + mGlobalScriptsStarted = false; if (!mPlayer.isEmpty()) { mPlayer.getCellRef().unsetRefNum(); @@ -225,17 +227,38 @@ namespace MWLua mPlayer = ptr; LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) - localScripts = createLocalScripts(ptr); + localScripts = createLocalScripts(ptr, ESM::LuaScriptCfg::sPlayer); mActiveLocalScripts.insert(localScripts); mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}}); mPlayerChanged = true; } + void LuaManager::newGameStarted() + { + mNewGameStarted = true; + mInputEvents.clear(); + mGlobalScripts.addAutoStartedScripts(); + mGlobalScriptsStarted = true; + } + + void LuaManager::gameLoaded() + { + if (!mGlobalScriptsStarted) + mGlobalScripts.addAutoStartedScripts(); + mGlobalScriptsStarted = true; + } + void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) { mWorldView.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts) + { + ESM::LuaScriptCfg::Flags flag = getLuaScriptFlag(ptr); + if (!mConfiguration.getListByFlag(flag).empty()) + localScripts = createLocalScripts(ptr, flag); // TODO: put to a queue and apply on next `update()` + } if (localScripts) { mActiveLocalScripts.insert(localScripts); @@ -281,26 +304,26 @@ namespace MWLua return localScripts->getActorControls(); } - void LuaManager::addLocalScript(const MWWorld::Ptr& ptr, const std::string& scriptPath) + void LuaManager::addCustomLocalScript(const MWWorld::Ptr& ptr, int scriptId) { LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) { - localScripts = createLocalScripts(ptr); + localScripts = createLocalScripts(ptr, getLuaScriptFlag(ptr)); if (ptr.isInCell() && MWBase::Environment::get().getWorld()->isCellActive(ptr.getCell())) mActiveLocalScripts.insert(localScripts); } - localScripts->addNewScript(scriptPath); + localScripts->addCustomScript(scriptId); } - LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr) + LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags flag) { assert(mInitialized); + assert(flag != ESM::LuaScriptCfg::sGlobal); std::shared_ptr scripts; - // When loading a game, it can be called before LuaManager::setPlayer, - // so we can't just check ptr == mPlayer here. - if (ptr.getCellRef().getRefIdRef() == "player") + if (flag == ESM::LuaScriptCfg::sPlayer) { + assert(ptr.getCellRef().getRefIdRef() == "player"); scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); scripts->addPackage("openmw.ui", mUserInterfacePackage); scripts->addPackage("openmw.camera", mCameraPackage); @@ -309,11 +332,12 @@ namespace MWLua } else { - scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); + scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()), flag); scripts->addPackage("openmw.settings", mLocalSettingsPackage); } scripts->addPackage("openmw.nearby", mNearbyPackage); scripts->setSerializer(mLocalSerializer.get()); + scripts->addAutoStartedScripts(); MWWorld::RefData& refData = ptr.getRefData(); refData.setLuaScripts(std::move(scripts)); @@ -344,8 +368,9 @@ namespace MWLua loadEvents(mLua.sol(), reader, mGlobalEvents, mLocalEvents, mContentFileMapping, mGlobalLoader.get()); mGlobalScripts.setSerializer(mGlobalLoader.get()); - mGlobalScripts.load(globalScripts, false); + mGlobalScripts.load(globalScripts); mGlobalScripts.setSerializer(mGlobalSerializer.get()); + mGlobalScriptsStarted = true; } void LuaManager::saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) @@ -366,10 +391,10 @@ namespace MWLua } mWorldView.getObjectRegistry()->registerPtr(ptr); - LocalScripts* scripts = createLocalScripts(ptr); + LocalScripts* scripts = createLocalScripts(ptr, getLuaScriptFlag(ptr)); scripts->setSerializer(mLocalLoader.get()); - scripts->load(data, true); + scripts->load(data); scripts->setSerializer(mLocalSerializer.get()); // LiveCellRef is usually copied after loading, so this Ptr will become invalid and should be deregistered. @@ -380,15 +405,12 @@ namespace MWLua { Log(Debug::Info) << "Reload Lua"; mLua.dropScriptCache(); + initConfiguration(); { // Reload global scripts ESM::LuaScripts data; mGlobalScripts.save(data); - mGlobalScripts.removeAllScripts(); - for (const std::string& path : mGlobalScriptList) - if (mGlobalScripts.addNewScript(path)) - Log(Debug::Info) << "Global script restarted: " << path; - mGlobalScripts.load(data, false); + mGlobalScripts.load(data); } for (const auto& [id, ptr] : mWorldView.getObjectRegistry()->mObjectMapping) @@ -398,8 +420,10 @@ namespace MWLua continue; ESM::LuaScripts data; scripts->save(data); - scripts->load(data, true); + scripts->load(data); } + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->receiveEngineEvent(LocalScripts::OnActive()); } } diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 91f48171f3..f5ffe9d258 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -19,25 +19,12 @@ namespace MWLua { - // Wrapper for a single-argument Lua function. - // Holds information about the script the function belongs to. - // Needed to prevent callback calls if the script was removed. - struct Callback - { - static constexpr std::string_view SCRIPT_NAME_KEY = "name"; - - sol::function mFunc; - sol::table mHiddenData; - - void operator()(sol::object arg) const; - }; - class LuaManager : public MWBase::LuaManager { public: - LuaManager(const VFS::Manager* vfs, const std::vector& globalScriptLists); + LuaManager(const VFS::Manager* vfs); - // Called by engine.cpp when environment is fully initialized. + // Called by engine.cpp when the environment is fully initialized. void init(); // Called by engine.cpp every frame. For performance reasons it works in a separate @@ -49,7 +36,8 @@ namespace MWLua // Available everywhere through the MWBase::LuaManager interface. // LuaManager queues these events and propagates to scripts on the next `update` call. - void newGameStarted() override { mGlobalScripts.newGameStarted(); } + void newGameStarted() override; + void gameLoaded() override; void objectAddedToScene(const MWWorld::Ptr& ptr) override; void objectRemovedFromScene(const MWWorld::Ptr& ptr) override; void registerObject(const MWWorld::Ptr& ptr) override; @@ -62,8 +50,8 @@ namespace MWLua void clear() override; // should be called before loading game or starting a new game to reset internal state. void setupPlayer(const MWWorld::Ptr& ptr) override; // Should be called once after each "clear". - // Used only in luabindings - void addLocalScript(const MWWorld::Ptr&, const std::string& scriptPath); + // Used only in Lua bindings + void addCustomLocalScript(const MWWorld::Ptr&, int scriptId); void addAction(std::unique_ptr&& action) { mActionQueue.push_back(std::move(action)); } void addTeleportPlayerAction(std::unique_ptr&& action) { mTeleportPlayerAction = std::move(action); } void addUIMessage(std::string_view message) { mUIMessages.emplace_back(message); } @@ -81,21 +69,27 @@ namespace MWLua void reloadAllScripts() override; // Used to call Lua callbacks from C++ - void queueCallback(Callback callback, sol::object arg) { mQueuedCallbacks.push_back({std::move(callback), std::move(arg)}); } + void queueCallback(LuaUtil::Callback callback, sol::object arg) + { + mQueuedCallbacks.push_back({std::move(callback), std::move(arg)}); + } // Wraps Lua callback into an std::function. // NOTE: Resulted function is not thread safe. Can not be used while LuaManager::update() or // any other Lua-related function is running. template - std::function wrapLuaCallback(const Callback& c) + std::function wrapLuaCallback(const LuaUtil::Callback& c) { return [this, c](Arg arg) { this->queueCallback(c, sol::make_object(c.mFunc.lua_state(), arg)); }; } private: - LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr); + void initConfiguration(); + LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags); bool mInitialized = false; + bool mGlobalScriptsStarted = false; + LuaUtil::ScriptsConfiguration mConfiguration; LuaUtil::LuaState mLua; sol::table mNearbyPackage; sol::table mUserInterfacePackage; @@ -104,12 +98,12 @@ namespace MWLua sol::table mLocalSettingsPackage; sol::table mPlayerSettingsPackage; - std::vector mGlobalScriptList; GlobalScripts mGlobalScripts{&mLua}; std::set mActiveLocalScripts; WorldView mWorldView; bool mPlayerChanged = false; + bool mNewGameStarted = false; MWWorld::Ptr mPlayer; GlobalEventQueue mGlobalEvents; @@ -127,7 +121,7 @@ namespace MWLua struct CallbackWithData { - Callback mCallback; + LuaUtil::Callback mCallback; sol::object mArg; }; std::vector mQueuedCallbacks; diff --git a/apps/openmw/mwlua/object.cpp b/apps/openmw/mwlua/object.cpp index 266e628dd6..69206e8c37 100644 --- a/apps/openmw/mwlua/object.cpp +++ b/apps/openmw/mwlua/object.cpp @@ -1,19 +1,6 @@ #include "object.hpp" -#include "../mwclass/activator.hpp" -#include "../mwclass/armor.hpp" -#include "../mwclass/book.hpp" -#include "../mwclass/clothing.hpp" -#include "../mwclass/container.hpp" -#include "../mwclass/creature.hpp" -#include "../mwclass/door.hpp" -#include "../mwclass/ingredient.hpp" -#include "../mwclass/light.hpp" -#include "../mwclass/misc.hpp" -#include "../mwclass/npc.hpp" -#include "../mwclass/potion.hpp" -#include "../mwclass/static.hpp" -#include "../mwclass/weapon.hpp" +#include namespace MWLua { @@ -23,28 +10,34 @@ namespace MWLua return std::to_string(id.mIndex) + "_" + std::to_string(id.mContentFile); } - const static std::map classNames = { - {typeid(MWClass::Activator), "Activator"}, - {typeid(MWClass::Armor), "Armor"}, - {typeid(MWClass::Book), "Book"}, - {typeid(MWClass::Clothing), "Clothing"}, - {typeid(MWClass::Container), "Container"}, - {typeid(MWClass::Creature), "Creature"}, - {typeid(MWClass::Door), "Door"}, - {typeid(MWClass::Ingredient), "Ingredient"}, - {typeid(MWClass::Light), "Light"}, - {typeid(MWClass::Miscellaneous), "Miscellaneous"}, - {typeid(MWClass::Npc), "NPC"}, - {typeid(MWClass::Potion), "Potion"}, - {typeid(MWClass::Static), "Static"}, - {typeid(MWClass::Weapon), "Weapon"}, + struct LuaObjectTypeInfo + { + std::string_view mName; + ESM::LuaScriptCfg::Flags mFlag = 0; }; - std::string_view getMWClassName(const std::type_index& cls_type, std::string_view fallback) + const static std::unordered_map luaObjectTypeInfo = { + {ESM::REC_ACTI, {"Activator", ESM::LuaScriptCfg::sActivator}}, + {ESM::REC_ARMO, {"Armor", ESM::LuaScriptCfg::sArmor}}, + {ESM::REC_BOOK, {"Book", ESM::LuaScriptCfg::sBook}}, + {ESM::REC_CLOT, {"Clothing", ESM::LuaScriptCfg::sClothing}}, + {ESM::REC_CONT, {"Container", ESM::LuaScriptCfg::sContainer}}, + {ESM::REC_CREA, {"Creature", ESM::LuaScriptCfg::sCreature}}, + {ESM::REC_DOOR, {"Door", ESM::LuaScriptCfg::sDoor}}, + {ESM::REC_INGR, {"Ingredient", ESM::LuaScriptCfg::sIngredient}}, + {ESM::REC_LIGH, {"Light", ESM::LuaScriptCfg::sLight}}, + {ESM::REC_MISC, {"Miscellaneous", ESM::LuaScriptCfg::sMiscItem}}, + {ESM::REC_NPC_, {"NPC", ESM::LuaScriptCfg::sNPC}}, + {ESM::REC_ALCH, {"Potion", ESM::LuaScriptCfg::sPotion}}, + {ESM::REC_STAT, {"Static"}}, + {ESM::REC_WEAP, {"Weapon", ESM::LuaScriptCfg::sWeapon}}, + }; + + std::string_view getLuaObjectTypeName(ESM::RecNameInts type, std::string_view fallback) { - auto it = classNames.find(cls_type); - if (it != classNames.end()) - return it->second; + auto it = luaObjectTypeInfo.find(type); + if (it != luaObjectTypeInfo.end()) + return it->second.mName; else return fallback; } @@ -55,13 +48,31 @@ namespace MWLua return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker"; } - std::string_view getMWClassName(const MWWorld::Ptr& ptr) + std::string_view getLuaObjectTypeName(const MWWorld::Ptr& ptr) { + // Behaviour of this function is a part of OpenMW Lua API. We can not just return + // `ptr.getTypeDescription()` because its implementation is distributed over many files + // and can be accidentally changed. We use `ptr.getTypeDescription()` only as a fallback + // for types that are not present in `luaObjectTypeInfo` (for such types result stability + // is not necessary because they are not listed in OpenMW Lua documentation). if (ptr.getCellRef().getRefIdRef() == "player") return "Player"; if (isMarker(ptr)) return "Marker"; - return getMWClassName(typeid(ptr.getClass())); + return getLuaObjectTypeName(static_cast(ptr.getType()), /*fallback=*/ptr.getTypeDescription()); + } + + ESM::LuaScriptCfg::Flags getLuaScriptFlag(const MWWorld::Ptr& ptr) + { + if (ptr.getCellRef().getRefIdRef() == "player") + return ESM::LuaScriptCfg::sPlayer; + if (isMarker(ptr)) + return 0; + auto it = luaObjectTypeInfo.find(static_cast(ptr.getType())); + if (it != luaObjectTypeInfo.end()) + return it->second.mFlag; + else + return 0; } std::string ptrToString(const MWWorld::Ptr& ptr) @@ -69,7 +80,7 @@ namespace MWLua std::string res = "object"; res.append(idToString(getId(ptr))); res.append(" ("); - res.append(getMWClassName(ptr)); + res.append(getLuaObjectTypeName(ptr)); res.append(", "); res.append(ptr.getCellRef().getRefIdRef()); res.append(")"); diff --git a/apps/openmw/mwlua/object.hpp b/apps/openmw/mwlua/object.hpp index c0b6bf1919..5b1b5df74e 100644 --- a/apps/openmw/mwlua/object.hpp +++ b/apps/openmw/mwlua/object.hpp @@ -4,6 +4,8 @@ #include #include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -19,8 +21,12 @@ namespace MWLua std::string idToString(const ObjectId& id); std::string ptrToString(const MWWorld::Ptr& ptr); bool isMarker(const MWWorld::Ptr& ptr); - std::string_view getMWClassName(const std::type_index& cls_type, std::string_view fallback = "Unknown"); - std::string_view getMWClassName(const MWWorld::Ptr& ptr); + std::string_view getLuaObjectTypeName(ESM::RecNameInts recordType, std::string_view fallback = "Unknown"); + std::string_view getLuaObjectTypeName(const MWWorld::Ptr& ptr); + + // Each script has a set of flags that controls to which objects the script should be + // automatically attached. This function maps each object types to one of the flags. + ESM::LuaScriptCfg::Flags getLuaScriptFlag(const MWWorld::Ptr& ptr); // Holds a mapping ObjectId -> MWWord::Ptr. class ObjectRegistry @@ -64,7 +70,7 @@ namespace MWLua ObjectId id() const { return mId; } std::string toString() const; - std::string_view type() const { return getMWClassName(ptr()); } + std::string_view type() const { return getLuaObjectTypeName(ptr()); } // Updates and returns the underlying Ptr. Throws an exception if object is not available. const MWWorld::Ptr& ptr() const; diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index b7607c8b2c..2eceecc061 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -42,13 +42,12 @@ namespace MWLua template using Cell = std::conditional_t, LCell, GCell>; - template - static const MWWorld::Ptr& requireClass(const MWWorld::Ptr& ptr) + static const MWWorld::Ptr& requireRecord(ESM::RecNameInts recordType, const MWWorld::Ptr& ptr) { - if (typeid(Class) != typeid(ptr.getClass())) + if (ptr.getType() != recordType) { std::string msg = "Requires type '"; - msg.append(getMWClassName(typeid(Class))); + msg.append(getLuaObjectTypeName(recordType)); msg.append("', but applied to "); msg.append(ptrToString(ptr)); throw std::runtime_error(msg); @@ -141,9 +140,43 @@ namespace MWLua if constexpr (std::is_same_v) { // Only for global scripts - objectT["addScript"] = [luaManager=context.mLuaManager](const GObject& object, const std::string& path) + objectT["addScript"] = [lua=context.mLua, luaManager=context.mLuaManager](const GObject& object, std::string_view path) { - luaManager->addLocalScript(object.ptr(), path); + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + throw std::runtime_error("Unknown script: " + std::string(path)); + if (!(cfg[*scriptId].mFlags & ESM::LuaScriptCfg::sCustom)) + throw std::runtime_error("Script without CUSTOM tag can not be added dynamically: " + std::string(path)); + luaManager->addCustomLocalScript(object.ptr(), *scriptId); + }; + objectT["hasScript"] = [lua=context.mLua](const GObject& object, std::string_view path) + { + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + return false; + MWWorld::Ptr ptr = object.ptr(); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (localScripts) + return localScripts->hasScript(*scriptId); + else + return false; + }; + objectT["removeScript"] = [lua=context.mLua](const GObject& object, std::string_view path) + { + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + throw std::runtime_error("Unknown script: " + std::string(path)); + MWWorld::Ptr ptr = object.ptr(); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts || !localScripts->hasScript(*scriptId)) + throw std::runtime_error("There is no script " + std::string(path) + " on " + ptrToString(ptr)); + ESM::LuaScriptCfg::Flags flags = cfg[*scriptId].mFlags; + if ((flags & (localScripts->getAutoStartMode() | ESM::LuaScriptCfg::sCustom)) != ESM::LuaScriptCfg::sCustom) + throw std::runtime_error("Autostarted script can not be removed: " + std::string(path)); + localScripts->removeScript(*scriptId); }; objectT["teleport"] = [luaManager=context.mLuaManager](const GObject& object, std::string_view cell, @@ -189,7 +222,7 @@ namespace MWLua template static void addDoorBindings(sol::usertype& objectT, const Context& context) { - auto ptr = [](const ObjectT& o) -> const MWWorld::Ptr& { return requireClass(o.ptr()); }; + auto ptr = [](const ObjectT& o) -> const MWWorld::Ptr& { return requireRecord(ESM::REC_DOOR, o.ptr()); }; objectT["isTeleport"] = sol::readonly_property([ptr](const ObjectT& o) { diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp index ff0349b3c6..0393a1375d 100644 --- a/apps/openmw/mwlua/playerscripts.hpp +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -13,7 +13,7 @@ namespace MWLua class PlayerScripts : public LocalScripts { public: - PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj) + PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj, ESM::LuaScriptCfg::sPlayer) { registerEngineHandlers({&mKeyPressHandlers, &mKeyReleaseHandlers, &mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index fc35e40b74..d435bd6c44 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -1,5 +1,7 @@ #include "activespells.hpp" +#include + #include #include @@ -14,6 +16,8 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwrender/animation.hpp" + #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" @@ -96,6 +100,11 @@ namespace MWMechanics , mType(params.mType), mWorsenings(params.mWorsenings), mNextWorsening({params.mNextWorsening}) {} + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor) + : mId(params.mId), mDisplayName(params.mDisplayName), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) + , mSlot(params.mSlot), mType(params.mType), mWorsenings(-1) + {} + ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const { ESM::ActiveSpells::ActiveSpellParams params; @@ -220,10 +229,19 @@ namespace MWMechanics { const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spellIt->mCasterActorId); //Maybe make this search outside active grid? bool removedSpell = false; + std::optional reflected; for(auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) { - bool remove = applyMagicEffect(ptr, caster, *spellIt, *it, duration); - if(remove) + auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration); + if(result == MagicApplicationResult::REFLECTED) + { + if(!reflected) + reflected = {*spellIt, ptr}; + auto& reflectedEffect = reflected->mEffects.emplace_back(*it); + reflectedEffect.mFlags = ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + it = spellIt->mEffects.erase(it); + } + else if(result == MagicApplicationResult::REMOVED) it = spellIt->mEffects.erase(it); else ++it; @@ -231,6 +249,14 @@ namespace MWMechanics if(removedSpell) break; } + if(reflected) + { + const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find("VFX_Reflect"); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); + if(animation && !reflectStatic->mModel.empty()) + animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string()); + caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); + } if(removedSpell) continue; diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 0538d7a8b7..524bdc0475 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -50,6 +50,8 @@ namespace MWMechanics ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor); + ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor); + ESM::ActiveSpells::ActiveSpellParams toEsm() const; friend class ActiveSpells; diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index d85946dbd5..32562591a5 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -240,10 +240,7 @@ namespace MWMechanics { // magic effects adjustMagicEffects (ptr, duration); - if (ptr.getClass().getCreatureStats(ptr).needToRecalcDynamicStats()) - calculateDynamicStats (ptr); - calculateCreatureStatModifiers (ptr, duration); // fatigue restoration calculateRestoration(ptr, duration); } @@ -654,29 +651,6 @@ namespace MWMechanics updateSummons(creature, mTimerDisposeSummonsCorpses == 0.f); } - void Actors::calculateDynamicStats (const MWWorld::Ptr& ptr) - { - CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); - - float intelligence = creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified(); - - float base = 1.f; - if (ptr == getPlayer()) - base = MWBase::Environment::get().getWorld()->getStore().get().find("fPCbaseMagickaMult")->mValue.getFloat(); - else - base = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCbaseMagickaMult")->mValue.getFloat(); - - double magickaFactor = base + - creatureStats.getMagicEffects().get (EffectKey (ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1; - - DynamicStat magicka = creatureStats.getMagicka(); - float diff = (static_cast(magickaFactor*intelligence)) - magicka.getBase(); - float currentToBaseRatio = magicka.getBase() > 0 ? magicka.getCurrent() / magicka.getBase() : 0; - magicka.setModified(magicka.getModified() + diff, 0); - magicka.setCurrent(magicka.getBase() * currentToBaseRatio, false, true); - creatureStats.setMagicka(magicka); - } - void Actors::restoreDynamicStats (const MWWorld::Ptr& ptr, double hours, bool sleep) { MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); @@ -771,14 +745,6 @@ namespace MWMechanics stats.setFatigue (fatigue); } - void Actors::calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration) - { - CreatureStats &creatureStats = ptr.getClass().getCreatureStats(ptr); - - if (creatureStats.needToRecalcDynamicStats()) - calculateDynamicStats(ptr); - } - bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr) { PtrActorMap::iterator it = mActors.find(ptr); @@ -1047,13 +1013,10 @@ namespace MWMechanics void Actors::updateProcessingRange() { // We have to cap it since using high values (larger than 7168) will make some quests harder or impossible to complete (bug #1876) - static const float maxProcessingRange = 7168.f; - static const float minProcessingRange = maxProcessingRange / 2.f; + static const float maxRange = 7168.f; + static const float minRange = maxRange / 2.f; - float actorsProcessingRange = Settings::Manager::getFloat("actors processing range", "Game"); - actorsProcessingRange = std::min(actorsProcessingRange, maxProcessingRange); - actorsProcessingRange = std::max(actorsProcessingRange, minProcessingRange); - mActorsProcessingRange = actorsProcessingRange; + mActorsProcessingRange = std::clamp(Settings::Manager::getFloat("actors processing range", "Game"), minRange, maxRange); } void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately) @@ -1349,7 +1312,7 @@ namespace MWMechanics angleToApproachingActor = std::atan2(deltaPos.x(), deltaPos.y()); osg::Vec2f posAtT = relPos + relSpeed * t; float coef = (posAtT.x() * relSpeed.x() + posAtT.y() * relSpeed.y()) / (collisionDist * collisionDist * maxSpeed); - coef *= osg::clampBetween((maxDistForPartialAvoiding - dist) / (maxDistForPartialAvoiding - maxDistForStrictAvoiding), 0.f, 1.f); + coef *= std::clamp((maxDistForPartialAvoiding - dist) / (maxDistForPartialAvoiding - maxDistForStrictAvoiding), 0.f, 1.f); movementCorrection = posAtT * coef; if (otherPtr.getClass().getCreatureStats(otherPtr).isDead()) // In case of dead body still try to go around (it looks natural), but reduce the correction twice. @@ -1530,15 +1493,13 @@ namespace MWMechanics stats.getAiSequence().execute(iter->first, *ctrl, duration, /*outOfRange*/true); } - if(iter->first.getClass().isNpc()) + if(inProcessingRange && iter->first.getClass().isNpc()) { // We can not update drowning state for actors outside of AI distance - they can not resurface to breathe - if (inProcessingRange) - updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer); - - if (timerUpdateEquippedLight == 0) - updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches); + updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer); } + if(timerUpdateEquippedLight == 0 && iter->first.getClass().hasInventoryStore(iter->first)) + updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches); if (luaControls && isConscious(iter->first)) { @@ -1711,10 +1672,6 @@ namespace MWMechanics if (iter->first.getType() == ESM::Creature::sRecordId) soulTrap(iter->first); - // Magic effects will be reset later, and the magic effect that could kill the actor - // needs to be determined now - calculateCreatureStatModifiers(iter->first, 0); - if (cls.isEssential(iter->first)) MWBase::Environment::get().getWindowManager()->messageBox("#{sKilledEssential}"); } @@ -1730,8 +1687,6 @@ namespace MWMechanics // Make sure spell effects are removed purgeSpellEffects(stats.getActorId()); - // Reset dynamic stats, attributes and skills - calculateCreatureStatModifiers(iter->first, 0); stats.getMagicEffects().add(ESM::MagicEffect::Vampirism, vampirism); if (isPlayer) @@ -1816,10 +1771,6 @@ namespace MWMechanics continue; adjustMagicEffects (iter->first, duration); - if (iter->first.getClass().getCreatureStats(iter->first).needToRecalcDynamicStats()) - calculateDynamicStats (iter->first); - - calculateCreatureStatModifiers (iter->first, duration); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(iter->first); if (animation) @@ -2209,7 +2160,6 @@ namespace MWMechanics void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) { adjustMagicEffects(ptr, 0.f); - calculateCreatureStatModifiers(ptr, 0.f); } bool Actors::isReadyToBlock(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 9950a591ab..7d44fd06cd 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -43,10 +43,6 @@ namespace MWMechanics void adjustMagicEffects (const MWWorld::Ptr& creature, float duration); - void calculateDynamicStats (const MWWorld::Ptr& ptr); - - void calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration); - void calculateRestoration (const MWWorld::Ptr& ptr, float duration); void updateDrowning (const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer); diff --git a/apps/openmw/mwmechanics/actorutil.cpp b/apps/openmw/mwmechanics/actorutil.cpp index 04cbb8e9f5..4a587dd662 100644 --- a/apps/openmw/mwmechanics/actorutil.cpp +++ b/apps/openmw/mwmechanics/actorutil.cpp @@ -29,4 +29,12 @@ namespace MWMechanics const MWMechanics::MagicEffects& effects = actor.getClass().getCreatureStats(actor).getMagicEffects(); return effects.get(ESM::MagicEffect::WaterWalking).getMagnitude() > 0; } + + CreatureCustomDataResetter::CreatureCustomDataResetter(const MWWorld::Ptr& ptr) : mPtr(ptr) {} + + CreatureCustomDataResetter::~CreatureCustomDataResetter() + { + if(!mPtr.isEmpty()) + mPtr.getRefData().setCustomData({}); + } } diff --git a/apps/openmw/mwmechanics/actorutil.hpp b/apps/openmw/mwmechanics/actorutil.hpp index a226fc9cb6..a66d8866bf 100644 --- a/apps/openmw/mwmechanics/actorutil.hpp +++ b/apps/openmw/mwmechanics/actorutil.hpp @@ -86,6 +86,14 @@ namespace MWMechanics template void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount); template void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount); template void modifyBaseInventory(const std::string& containerId, const std::string& itemId, int amount); + + struct CreatureCustomDataResetter + { + MWWorld::Ptr mPtr; + + CreatureCustomDataResetter(const MWWorld::Ptr& ptr); + ~CreatureCustomDataResetter(); + }; } #endif diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 58339d0228..36d4446580 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -2044,7 +2044,7 @@ void CharacterController::update(float duration) mIsMovingBackward = vec.y() < 0; float maxDelta = osg::PI * duration * (2.5f - cosDelta); - delta = osg::clampBetween(delta, -maxDelta, maxDelta); + delta = std::clamp(delta, -maxDelta, maxDelta); stats.setSideMovementAngle(stats.getSideMovementAngle() + delta); effectiveRotation += delta; } @@ -2286,7 +2286,7 @@ void CharacterController::update(float duration) float swimmingPitch = mAnimation->getBodyPitchRadians(); float targetSwimmingPitch = -mPtr.getRefData().getPosition().rot[0]; float maxSwimPitchDelta = 3.0f * duration; - swimmingPitch += osg::clampBetween(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta); + swimmingPitch += std::clamp(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta); mAnimation->setBodyPitchRadians(swimmingPitch); } else @@ -2522,7 +2522,7 @@ void CharacterController::unpersistAnimationState() { float start = mAnimation->getTextKeyTime(anim.mGroup+": start"); float stop = mAnimation->getTextKeyTime(anim.mGroup+": stop"); - float time = std::max(start, std::min(stop, anim.mTime)); + float time = std::clamp(anim.mTime, start, stop); complete = (time - start) / (stop - start); } @@ -2746,7 +2746,7 @@ void CharacterController::setVisibility(float visibility) float chameleon = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude(); if (chameleon) { - alpha *= std::min(0.75f, std::max(0.25f, (100.f - chameleon)/100.f)); + alpha *= std::clamp(1.f - chameleon / 100.f, 0.25f, 0.75f); } visibility = std::min(visibility, alpha); @@ -2965,8 +2965,8 @@ void CharacterController::updateHeadTracking(float duration) const double xLimit = osg::DegreesToRadians(40.0); const double zLimit = osg::DegreesToRadians(30.0); double zLimitOffset = mAnimation->getUpperBodyYawRadians(); - xAngleRadians = osg::clampBetween(xAngleRadians, -xLimit, xLimit); - zAngleRadians = osg::clampBetween(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset); + xAngleRadians = std::clamp(xAngleRadians, -xLimit, xLimit); + zAngleRadians = std::clamp(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset); float factor = duration*5; factor = std::min(factor, 1.f); diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index c1d5f711fc..2962a25ede 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -113,10 +113,9 @@ namespace MWMechanics + 0.1f * attackerStats.getAttribute(ESM::Attribute::Luck).getModified(); attackerTerm *= attackerStats.getFatigueTerm(); - int x = int(blockerTerm - attackerTerm); - int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger(); - int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger(); - x = std::min(iBlockMaxChance, std::max(iBlockMinChance, x)); + const int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger(); + const int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger(); + int x = std::clamp(blockerTerm - attackerTerm, iBlockMinChance, iBlockMaxChance); if (Misc::Rng::roll0to99() < x) { diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 8ca6e8b766..d832c47c87 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -7,6 +7,7 @@ #include #include +#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" @@ -22,7 +23,7 @@ namespace MWMechanics mTalkedTo (false), mAlarmed (false), mAttacked (false), mKnockdown(false), mKnockdownOneFrame(false), mKnockdownOverOneFrame(false), mHitRecovery(false), mBlock(false), mMovementFlags(0), - mFallHeight(0), mRecalcMagicka(false), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mHitAttemptActorId(-1), + mFallHeight(0), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mHitAttemptActorId(-1), mDeathAnimation(-1), mTimeOfDeath(), mSideMovementAngle(0), mLevel (0) { for (int i=0; i<4; ++i) @@ -146,7 +147,7 @@ namespace MWMechanics mAttributes[index] = value; if (index == ESM::Attribute::Intelligence) - mRecalcMagicka = true; + recalculateMagicka(); else if (index == ESM::Attribute::Strength || index == ESM::Attribute::Willpower || index == ESM::Attribute::Agility || @@ -208,11 +209,10 @@ namespace MWMechanics void CreatureStats::modifyMagicEffects(const MagicEffects &effects) { - if (effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() - != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier()) - mRecalcMagicka = true; - + bool recalc = effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier(); mMagicEffects.setModifiers(effects); + if(recalc) + recalculateMagicka(); } void CreatureStats::setAiSetting (AiSetting index, Stat value) @@ -400,19 +400,26 @@ namespace MWMechanics return height; } - bool CreatureStats::needToRecalcDynamicStats() + void CreatureStats::recalculateMagicka() { - if (mRecalcMagicka) - { - mRecalcMagicka = false; - return true; - } - return false; - } + auto world = MWBase::Environment::get().getWorld(); + float intelligence = getAttribute(ESM::Attribute::Intelligence).getModified(); - void CreatureStats::setNeedRecalcDynamicStats(bool val) - { - mRecalcMagicka = val; + float base = 1.f; + const auto& player = world->getPlayerPtr(); + if (this == &player.getClass().getCreatureStats(player)) + base = world->getStore().get().find("fPCbaseMagickaMult")->mValue.getFloat(); + else + base = world->getStore().get().find("fNPCbaseMagickaMult")->mValue.getFloat(); + + double magickaFactor = base + mMagicEffects.get(EffectKey(ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1; + + DynamicStat magicka = getMagicka(); + float diff = (static_cast(magickaFactor*intelligence)) - magicka.getBase(); + float currentToBaseRatio = magicka.getBase() > 0 ? magicka.getCurrent() / magicka.getBase() : 0; + magicka.setModified(magicka.getModified() + diff, 0); + magicka.setCurrent(magicka.getBase() * currentToBaseRatio, false, true); + setMagicka(magicka); } void CreatureStats::setKnockedDown(bool value) @@ -532,7 +539,7 @@ namespace MWMechanics state.mFallHeight = mFallHeight; // TODO: vertical velocity (move from PhysicActor to CreatureStats?) state.mLastHitObject = mLastHitObject; state.mLastHitAttemptObject = mLastHitAttemptObject; - state.mRecalcDynamicStats = mRecalcMagicka; + state.mRecalcDynamicStats = false; state.mDrawState = mDrawState; state.mLevel = mLevel; state.mActorId = mActorId; @@ -586,7 +593,6 @@ namespace MWMechanics mFallHeight = state.mFallHeight; mLastHitObject = state.mLastHitObject; mLastHitAttemptObject = state.mLastHitAttemptObject; - mRecalcMagicka = state.mRecalcDynamicStats; mDrawState = DrawState_(state.mDrawState); mLevel = state.mLevel; mActorId = state.mActorId; @@ -627,6 +633,8 @@ namespace MWMechanics if (state.mHasAiSettings) for (int i=0; i<4; ++i) mAiSettings[i].readState(state.mAiSettings[i]); + if(state.mRecalcDynamicStats) + recalculateMagicka(); } void CreatureStats::setLastRestockTime(MWWorld::TimeStamp tradeTime) diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index d234d14486..e5c4f6fa41 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -65,8 +65,6 @@ namespace MWMechanics std::string mLastHitObject; // The last object to hit this actor std::string mLastHitAttemptObject; // The last object to attempt to hit this actor - bool mRecalcMagicka; - // For merchants: the last time items were restocked and gold pool refilled. MWWorld::TimeStamp mLastRestock; @@ -103,8 +101,7 @@ namespace MWMechanics DrawState_ getDrawState() const; void setDrawState(DrawState_ state); - bool needToRecalcDynamicStats(); - void setNeedRecalcDynamicStats(bool val); + void recalculateMagicka(); float getFallHeight() const; void addToFallHeight(float height); diff --git a/apps/openmw/mwmechanics/difficultyscaling.cpp b/apps/openmw/mwmechanics/difficultyscaling.cpp index 2376989745..e973e0ed52 100644 --- a/apps/openmw/mwmechanics/difficultyscaling.cpp +++ b/apps/openmw/mwmechanics/difficultyscaling.cpp @@ -13,9 +13,7 @@ float scaleDamage(float damage, const MWWorld::Ptr& attacker, const MWWorld::Ptr const MWWorld::Ptr& player = MWMechanics::getPlayer(); // [-500, 500] - int difficultySetting = Settings::Manager::getInt("difficulty", "Game"); - difficultySetting = std::min(difficultySetting, 500); - difficultySetting = std::max(difficultySetting, -500); + const int difficultySetting = std::clamp(Settings::Manager::getInt("difficulty", "Game"), -500, 500); static const float fDifficultyMult = MWBase::Environment::get().getWorld()->getStore().get().find("fDifficultyMult")->mValue.getFloat(); diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index 078cbc5f43..a1870cbdb0 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -356,10 +356,10 @@ namespace MWMechanics ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo) { - static const float multiplier = std::max(0.f, std::min(1.0f, Settings::Manager::getFloat("projectiles enchant multiplier", "Game"))); + static const float multiplier = std::clamp(Settings::Manager::getFloat("projectiles enchant multiplier", "Game"), 0.f, 1.f); MWWorld::Ptr player = getPlayer(); - int itemsInInventoryCount = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId()); - count = std::min(itemsInInventoryCount, std::max(1, int(getGemCharge() * multiplier / enchantPoints))); + count = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId()); + count = std::clamp(getGemCharge() * multiplier / enchantPoints, 1, count); } } diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp index e75361628b..b01c5446a7 100644 --- a/apps/openmw/mwmechanics/magiceffects.cpp +++ b/apps/openmw/mwmechanics/magiceffects.cpp @@ -1,10 +1,20 @@ #include "magiceffects.hpp" +#include #include #include #include +namespace +{ + // Round value to prevent precision issues + void truncate(float& value) + { + value = std::roundf(value * 1024.f) / 1024.f; + } +} + namespace MWMechanics { EffectKey::EffectKey() : mId (0), mArg (-1) {} @@ -74,6 +84,7 @@ namespace MWMechanics { mModifier += param.mModifier; mBase += param.mBase; + truncate(mModifier); return *this; } @@ -81,6 +92,7 @@ namespace MWMechanics { mModifier -= param.mModifier; mBase -= param.mBase; + truncate(mModifier); return *this; } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 149056e605..362df54ab7 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -77,7 +77,7 @@ namespace MWMechanics MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats (ptr); - npcStats.setNeedRecalcDynamicStats(true); + npcStats.recalculateMagicka(); const ESM::NPC *player = ptr.get()->mBase; @@ -222,7 +222,6 @@ namespace MWMechanics // forced update and current value adjustments mActors.updateActor (ptr, 0); - mActors.updateActor (ptr, 0); for (int i=0; i<3; ++i) { @@ -546,9 +545,9 @@ namespace MWMechanics x += ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Charm).getMagnitude(); - if(clamp) - return std::max(0,std::min(int(x),100));//, normally clamped to [0..100] when used - return int(x); + if (clamp) + return std::clamp(x, 0, 100);//, normally clamped to [0..100] when used + return static_cast(x); } int MechanicsManager::getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) @@ -650,9 +649,9 @@ namespace MWMechanics int flee = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Flee).getBase(); int fight = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Fight).getBase(); npcStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, - std::max(0, std::min(100, flee + int(std::max(iPerMinChange, s))))); + std::clamp(flee + int(std::max(iPerMinChange, s)), 0, 100)); npcStats.setAiSetting (MWMechanics::CreatureStats::AI_Fight, - std::max(0, std::min(100, fight + int(std::min(-iPerMinChange, -s))))); + std::clamp(fight + int(std::min(-iPerMinChange, -s)), 0, 100)); } float c = -std::abs(floor(r * fPerDieRollMult)); @@ -690,10 +689,10 @@ namespace MWMechanics float s = c * fPerDieRollMult * fPerTempMult; int flee = npcStats.getAiSetting (CreatureStats::AI_Flee).getBase(); int fight = npcStats.getAiSetting (CreatureStats::AI_Fight).getBase(); - npcStats.setAiSetting (CreatureStats::AI_Flee, - std::max(0, std::min(100, flee + std::min(-int(iPerMinChange), int(-s))))); - npcStats.setAiSetting (CreatureStats::AI_Fight, - std::max(0, std::min(100, fight + std::max(int(iPerMinChange), int(s))))); + npcStats.setAiSetting(CreatureStats::AI_Flee, + std::clamp(flee + std::min(-int(iPerMinChange), int(-s)), 0, 100)); + npcStats.setAiSetting(CreatureStats::AI_Fight, + std::clamp(fight + std::max(int(iPerMinChange), int(s)), 0, 100)); } x = floor(-c * fPerDieRollMult); diff --git a/apps/openmw/mwmechanics/npcstats.cpp b/apps/openmw/mwmechanics/npcstats.cpp index 5d19368bf6..1d1dfacce8 100644 --- a/apps/openmw/mwmechanics/npcstats.cpp +++ b/apps/openmw/mwmechanics/npcstats.cpp @@ -371,7 +371,7 @@ int MWMechanics::NpcStats::getReputation() const void MWMechanics::NpcStats::setReputation(int reputation) { // Reputation is capped in original engine - mReputation = std::min(255, std::max(0, reputation)); + mReputation = std::clamp(reputation, 0, 255); } int MWMechanics::NpcStats::getCrimeId() const diff --git a/apps/openmw/mwmechanics/spellabsorption.cpp b/apps/openmw/mwmechanics/spellabsorption.cpp deleted file mode 100644 index 82531132ca..0000000000 --- a/apps/openmw/mwmechanics/spellabsorption.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include "spellabsorption.hpp" - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwrender/animation.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/inventorystore.hpp" - -#include "creaturestats.hpp" -#include "spellutil.hpp" - -namespace MWMechanics -{ - float getProbability(const MWMechanics::ActiveSpells& activeSpells) - { - float probability = 0.f; - for(const auto& params : activeSpells) - { - for(const auto& effect : params.getEffects()) - { - if(effect.mEffectId == ESM::MagicEffect::SpellAbsorption) - { - if(probability == 0.f) - probability = effect.mMagnitude / 100; - else - { - // If there are different sources of SpellAbsorption effect, multiply failing probability for all effects. - // Real absorption probability will be the (1 - total fail chance) in this case. - float failProbability = 1.f - probability; - failProbability *= 1.f - effect.mMagnitude / 100; - probability = 1.f - failProbability; - } - } - } - } - return static_cast(probability * 100); - } - - bool absorbSpell (const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) - { - if (spellId.empty() || target.isEmpty() || caster == target || !target.getClass().isActor()) - return false; - - CreatureStats& stats = target.getClass().getCreatureStats(target); - if (stats.getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude() <= 0.f) - return false; - - int chance = getProbability(stats.getActiveSpells()); - if (Misc::Rng::roll0to99() >= chance) - return false; - - const auto& esmStore = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Static* absorbStatic = esmStore.get().find("VFX_Absorb"); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation && !absorbStatic->mModel.empty()) - animation->addEffect( "meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, std::string()); - const ESM::Spell* spell = esmStore.get().search(spellId); - int spellCost = 0; - if (spell) - { - spellCost = MWMechanics::calcSpellCost(*spell); - } - else - { - const ESM::Enchantment* enchantment = esmStore.get().search(spellId); - if (enchantment) - spellCost = getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), caster); - } - - // Magicka is increased by the cost of the spell - DynamicStat magicka = stats.getMagicka(); - magicka.setCurrent(magicka.getCurrent() + spellCost); - stats.setMagicka(magicka); - return true; - } - -} diff --git a/apps/openmw/mwmechanics/spellabsorption.hpp b/apps/openmw/mwmechanics/spellabsorption.hpp deleted file mode 100644 index 0fe501df91..0000000000 --- a/apps/openmw/mwmechanics/spellabsorption.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef MWMECHANICS_SPELLABSORPTION_H -#define MWMECHANICS_SPELLABSORPTION_H - -#include - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - // Try to absorb a spell based on the magnitude of every Spell Absorption effect source on the target. - bool absorbSpell(const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target); -} - -#endif diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 026f6b5f19..2edc437752 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -22,38 +22,11 @@ #include "actorutil.hpp" #include "aifollow.hpp" #include "creaturestats.hpp" -#include "spellabsorption.hpp" #include "spelleffects.hpp" #include "spellutil.hpp" #include "summoning.hpp" #include "weapontype.hpp" -namespace -{ - bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects) - { - if (caster.isEmpty() || caster == target || !target.getClass().isActor()) - return false; - - bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; - bool isUnreflectable = magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable; - if (!isHarmful || isUnreflectable) - return false; - - float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude(); - if (Misc::Rng::roll0to99() >= reflect) - return false; - - const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Reflect"); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation && !reflectStatic->mModel.empty()) - animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string()); - reflectedEffects.mList.emplace_back(effect); - return true; - } -} - namespace MWMechanics { CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool manualSpell) @@ -82,7 +55,7 @@ namespace MWMechanics } void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, - const ESM::EffectList &effects, ESM::RangeType range, bool reflected, bool exploded) + const ESM::EffectList &effects, ESM::RangeType range, bool exploded) { const bool targetIsActor = !target.isEmpty() && target.getClass().isActor(); if (targetIsActor) @@ -123,7 +96,6 @@ namespace MWMechanics } } - ESM::EffectList reflectedEffects; ActiveSpells::ActiveSpellParams params(*this, caster); bool castByPlayer = (!caster.isEmpty() && caster == getPlayer()); @@ -136,9 +108,6 @@ namespace MWMechanics // throughout the iteration of this spell's // effects, we display a "can't re-cast" message - // Try absorbing the spell. Some handling must still happen for absorbed effects. - bool absorbed = absorbSpell(mId, caster, target); - int currentEffectIndex = 0; for (std::vector::const_iterator effectIt (effects.mList.begin()); !target.isEmpty() && effectIt != effects.mList.end(); ++effectIt, ++currentEffectIndex) @@ -167,19 +136,6 @@ namespace MWMechanics && (caster.isEmpty() || !caster.getClass().isActor())) continue; - // Notify the target actor they've been hit - bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; - if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) - target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true); - - // Avoid proceeding further for absorbed spells. - if (absorbed) - continue; - - // Reflect harmful effects - if (!reflected && reflectEffect(*effectIt, magicEffect, caster, target, reflectedEffects)) - continue; - ActiveSpells::ActiveEffect effect; effect.mEffectId = effectIt->mEffectID; effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; @@ -189,13 +145,8 @@ namespace MWMechanics effect.mTimeLeft = 0.f; effect.mEffectIndex = currentEffectIndex; effect.mFlags = ESM::ActiveEffect::Flag_None; - - // Avoid applying harmful effects to the player in god mode - if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful) - { - effect.mMinMagnitude = 0; - effect.mMaxMagnitude = 0; - } + if(mManualSpell) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect; bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); effect.mDuration = hasDuration ? static_cast(effectIt->mDuration) : 1.f; @@ -209,14 +160,14 @@ namespace MWMechanics // add to list of active effects, to apply in next frame params.getEffects().emplace_back(effect); - bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; + bool effectAffectsHealth = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; if (castByPlayer && target != caster && targetIsActor && effectAffectsHealth) { // If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar. MWBase::Environment::get().getWindowManager()->setEnemy(target); } - if (targetIsActor || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) + if (!targetIsActor && magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) { playEffects(target, *magicEffect); } @@ -227,9 +178,6 @@ namespace MWMechanics if (!target.isEmpty()) { - if (!reflectedEffects.mList.empty()) - inflict(caster, target, reflectedEffects, range, true, exploded); - if (!params.getEffects().empty()) { if(targetIsActor) @@ -237,6 +185,7 @@ namespace MWMechanics else { // Apply effects instantly. We can ignore effect deletion since the entire params object gets deleted afterwards anyway + // and we can ignore reflection since non-actors cannot reflect spells for(auto& effect : params.getEffects()) applyMagicEffect(target, caster, params, effect, 0.f); } diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index a21ea33e0b..4d9cc3a7de 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -62,7 +62,7 @@ namespace MWMechanics /// @note \a target can be any type of object, not just actors. /// @note \a caster can be any type of object, or even an empty object. void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, - const ESM::EffectList& effects, ESM::RangeType range, bool reflected=false, bool exploded=false); + const ESM::EffectList& effects, ESM::RangeType range, bool exploded=false); }; void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping = true); diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 0e67f06ba2..566fb9eded 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -16,6 +16,7 @@ #include "../mwmechanics/aifollow.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellresistance.hpp" +#include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/summoning.hpp" #include "../mwrender/animation.hpp" @@ -261,6 +262,97 @@ namespace return false; } + void absorbSpell(const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) + { + const auto& esmStore = MWBase::Environment::get().getWorld()->getStore(); + const ESM::Static* absorbStatic = esmStore.get().find("VFX_Absorb"); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); + if (animation && !absorbStatic->mModel.empty()) + animation->addEffect( "meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, std::string()); + const ESM::Spell* spell = esmStore.get().search(spellId); + int spellCost = 0; + if (spell) + { + spellCost = MWMechanics::calcSpellCost(*spell); + } + else + { + const ESM::Enchantment* enchantment = esmStore.get().search(spellId); + if (enchantment) + spellCost = MWMechanics::getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), caster); + } + + // Magicka is increased by the cost of the spell + auto& stats = target.getClass().getCreatureStats(target); + auto magicka = stats.getMagicka(); + magicka.setCurrent(magicka.getCurrent() + spellCost); + stats.setMagicka(magicka); + } + + MWMechanics::MagicApplicationResult applyProtections(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, + const MWMechanics::ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, const ESM::MagicEffect* magicEffect) + { + auto& stats = target.getClass().getCreatureStats(target); + auto& magnitudes = stats.getMagicEffects(); + // Apply reflect and spell absorption + if(target != caster && spellParams.getType() != ESM::ActiveSpells::Type_Enchantment && spellParams.getType() != ESM::ActiveSpells::Type_Permanent) + { + bool canReflect = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful && !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable) && + !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Reflect) && magnitudes.get(ESM::MagicEffect::Reflect).getMagnitude() > 0.f; + bool canAbsorb = !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_SpellAbsorption) && magnitudes.get(ESM::MagicEffect::SpellAbsorption).getMagnitude() > 0.f; + if(canReflect || canAbsorb) + { + for(const auto& activeParam : stats.getActiveSpells()) + { + for(const auto& activeEffect : activeParam.getEffects()) + { + if(!(activeEffect.mFlags & ESM::ActiveEffect::Flag_Applied)) + continue; + if(activeEffect.mEffectId == ESM::MagicEffect::Reflect) + { + if(canReflect && Misc::Rng::roll0to99() < activeEffect.mMagnitude) + { + return MWMechanics::MagicApplicationResult::REFLECTED; + } + } + else if(activeEffect.mEffectId == ESM::MagicEffect::SpellAbsorption) + { + if(canAbsorb && Misc::Rng::roll0to99() < activeEffect.mMagnitude) + { + absorbSpell(spellParams.getId(), caster, target); + return MWMechanics::MagicApplicationResult::REMOVED; + } + } + } + } + } + } + // Notify the target actor they've been hit + bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; + if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) + target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true); + // Apply resistances + if(!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances)) + { + const ESM::Spell* spell = nullptr; + if(spellParams.getType() == ESM::ActiveSpells::Type_Temporary) + spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellParams.getId()); + float magnitudeMult = MWMechanics::getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes); + if (magnitudeMult == 0) + { + // Fully resisted, show message + if (target == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); + else if (caster == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); + return MWMechanics::MagicApplicationResult::REMOVED; + } + effect.mMinMagnitude *= magnitudeMult; + effect.mMaxMagnitude *= magnitudeMult; + } + return MWMechanics::MagicApplicationResult::APPLIED; + } + static const std::map sBoundItemsMap{ {ESM::MagicEffect::BoundBattleAxe, "sMagicBoundBattleAxeID"}, {ESM::MagicEffect::BoundBoots, "sMagicBoundBootsID"}, @@ -279,7 +371,7 @@ namespace namespace MWMechanics { -void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, bool& invalid, bool& receivedMagicDamage) +void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, bool& invalid, bool& receivedMagicDamage, bool& recalculateMagicka) { const auto world = MWBase::Environment::get().getWorld(); bool godmode = target == getPlayer() && world->getGodModeState(); @@ -539,7 +631,7 @@ void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, co if (!target.isInCell() || !target.getCell()->isExterior() || godmode) break; float time = world->getTimeStamp().getHour(); - float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13))); + float timeDiff = std::clamp(std::abs(time - 13.f), 0.f, 7.f); float damageScale = 1.f - timeDiff / 7.f; // When cloudy, the sun damage effect is halved static float fMagicSunBlockedMult = world->getStore().get().find("fMagicSunBlockedMult")->mValue.getFloat(); @@ -609,7 +701,7 @@ void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, co fortifySkill(target, effect, effect.mMagnitude); break; case ESM::MagicEffect::FortifyMaximumMagicka: - target.getClass().getCreatureStats(target).setNeedRecalcDynamicStats(true); + recalculateMagicka = true; break; case ESM::MagicEffect::AbsorbHealth: case ESM::MagicEffect::AbsorbMagicka: @@ -682,28 +774,29 @@ void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, co } } -bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt) +MagicApplicationResult applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt) { const auto world = MWBase::Environment::get().getWorld(); bool invalid = false; bool receivedMagicDamage = false; + bool recalculateMagicka = false; if(effect.mEffectId == ESM::MagicEffect::Corprus && spellParams.shouldWorsen()) { spellParams.worsen(); for(auto& otherEffect : spellParams.getEffects()) { if(isCorprusEffect(otherEffect)) - applyMagicEffect(target, caster, spellParams, otherEffect, invalid, receivedMagicDamage); + applyMagicEffect(target, caster, spellParams, otherEffect, invalid, receivedMagicDamage, recalculateMagicka); } if(target == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); - return false; + return MagicApplicationResult::APPLIED; } else if(effect.mEffectId == ESM::MagicEffect::Levitate && !world->isLevitationEnabled()) { if(target == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}"); - return true; + return MagicApplicationResult::REMOVED; } const auto* magicEffect = world->getStore().get().find(effect.mEffectId); if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) @@ -711,10 +804,10 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce) { effect.mTimeLeft -= dt; - return false; + return MagicApplicationResult::APPLIED; } else if(!dt) - return false; + return MagicApplicationResult::APPLIED; } if(effect.mEffectId == ESM::MagicEffect::Lock) { @@ -770,28 +863,19 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac } else { - auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); - if(spellParams.getType() != ESM::ActiveSpells::Type_Ability && !(effect.mFlags & (ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Ignore_Resistances))) + auto& stats = target.getClass().getCreatureStats(target); + auto& magnitudes = stats.getMagicEffects(); + if(spellParams.getType() != ESM::ActiveSpells::Type_Ability && !(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) { - const ESM::Spell* spell = nullptr; - if(spellParams.getType() == ESM::ActiveSpells::Type_Temporary) - spell = world->getStore().get().search(spellParams.getId()); - float magnitudeMult = getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes); - if (magnitudeMult == 0) - { - // Fully resisted, show message - if (target == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); - else if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); - return true; - } - effect.mMinMagnitude *= magnitudeMult; - effect.mMaxMagnitude *= magnitudeMult; + MagicApplicationResult result = applyProtections(target, caster, spellParams, effect, magicEffect); + if(result != MagicApplicationResult::APPLIED) + return result; } float oldMagnitude = 0.f; if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) oldMagnitude = effect.mMagnitude; + else if(spellParams.getType() == ESM::ActiveSpells::Type_Consumable || spellParams.getType() == ESM::ActiveSpells::Type_Temporary) + playEffects(target, *magicEffect); float magnitude = roll(effect); //Note that there's an early out for Flag_Applied AppliedOnce effects so we don't have to exclude them here effect.mMagnitude = magnitude; @@ -809,13 +893,13 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac effect.mMagnitude = oldMagnitude; effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; effect.mTimeLeft -= dt; - return false; + return MagicApplicationResult::APPLIED; } } if(effect.mEffectId == ESM::MagicEffect::Corprus) spellParams.worsen(); else - applyMagicEffect(target, caster, spellParams, effect, invalid, receivedMagicDamage); + applyMagicEffect(target, caster, spellParams, effect, invalid, receivedMagicDamage, recalculateMagicka); effect.mMagnitude = magnitude; magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(effect.mMagnitude - oldMagnitude)); } @@ -832,7 +916,9 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; if (receivedMagicDamage && target == getPlayer()) MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); - return false; + if(recalculateMagicka) + target.getClass().getCreatureStats(target).recalculateMagicka(); + return MagicApplicationResult::APPLIED; } void removeMagicEffect(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spellParams, const ESM::ActiveEffect& effect) @@ -981,7 +1067,7 @@ void removeMagicEffect(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellPara fortifySkill(target, effect, -effect.mMagnitude); break; case ESM::MagicEffect::FortifyMaximumMagicka: - target.getClass().getCreatureStats(target).setNeedRecalcDynamicStats(true); + target.getClass().getCreatureStats(target).recalculateMagicka(); break; case ESM::MagicEffect::AbsorbAttribute: { diff --git a/apps/openmw/mwmechanics/spelleffects.hpp b/apps/openmw/mwmechanics/spelleffects.hpp index a9e7b066f3..2861d0d64a 100644 --- a/apps/openmw/mwmechanics/spelleffects.hpp +++ b/apps/openmw/mwmechanics/spelleffects.hpp @@ -10,8 +10,13 @@ namespace MWMechanics { + enum class MagicApplicationResult + { + APPLIED, REMOVED, REFLECTED + }; + // Applies a tick of a single effect. Returns true if the effect should be removed immediately - bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt); + MagicApplicationResult applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt); // Undoes permanent effects created by ESM::MagicEffect::AppliedOnce void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spell, const ESM::ActiveEffect& effect); diff --git a/apps/openmw/mwmechanics/spellutil.cpp b/apps/openmw/mwmechanics/spellutil.cpp index b18ad288ad..70167abcd3 100644 --- a/apps/openmw/mwmechanics/spellutil.cpp +++ b/apps/openmw/mwmechanics/spellutil.cpp @@ -162,7 +162,10 @@ namespace MWMechanics float castChance = baseChance + castBonus; castChance *= stats.getFatigueTerm(); - return std::max(0.f, cap ? std::min(100.f, castChance) : castChance); + if (cap) + return std::clamp(castChance, 0.f, 100.f); + + return std::max(castChance, 0.f); } float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka) diff --git a/apps/openmw/mwmechanics/weaponpriority.cpp b/apps/openmw/mwmechanics/weaponpriority.cpp index 570e89a17d..1a17cc87e6 100644 --- a/apps/openmw/mwmechanics/weaponpriority.cpp +++ b/apps/openmw/mwmechanics/weaponpriority.cpp @@ -128,8 +128,7 @@ namespace MWMechanics } // Take hit chance in account, but do not allow rating become negative. - float chance = getHitChance(actor, enemy, value) / 100.f; - rating *= std::min(1.f, std::max(0.01f, chance)); + rating *= std::clamp(getHitChance(actor, enemy, value) / 100.f, 0.01f, 1.f); if (weapclass != ESM::WeaponType::Ammo) rating *= weapon->mData.mSpeed; diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index e140141e32..16501c432d 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -12,6 +12,7 @@ #include "collisiontype.hpp" #include "mtphysics.hpp" +#include "trace.h" #include @@ -21,8 +22,8 @@ namespace MWPhysics Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler, bool canWaterWalk) : mStandingOnPtr(nullptr), mCanWaterWalk(canWaterWalk), mWalkingOnWater(false) - , mMeshTranslation(shape->mCollisionBox.center), mOriginalHalfExtents(shape->mCollisionBox.extents) - , mVelocity(0,0,0), mStuckFrames(0), mLastStuckPosition{0, 0, 0} + , mMeshTranslation(shape->mCollisionBox.mCenter), mOriginalHalfExtents(shape->mCollisionBox.mExtents) + , mStuckFrames(0), mLastStuckPosition{0, 0, 0} , mForce(0.f, 0.f, 0.f), mOnGround(true), mOnSlope(false) , mInternalCollisionMode(true) , mExternalCollisionMode(true) @@ -123,7 +124,6 @@ void Actor::updatePosition() mSimulationPosition = worldPosition; mPositionOffset = osg::Vec3f(); mStandingOnPtr = nullptr; - mSkipCollisions = true; mSkipSimulation = true; } @@ -133,11 +133,6 @@ void Actor::setSimulationPosition(const osg::Vec3f& position) mSimulationPosition = position; } -osg::Vec3f Actor::getSimulationPosition() const -{ - return mSimulationPosition; -} - osg::Vec3f Actor::getScaledMeshTranslation() const { return mRotation * osg::componentMultiply(mMeshTranslation, mScale); @@ -167,17 +162,19 @@ bool Actor::setPosition(const osg::Vec3f& position) { std::scoped_lock lock(mPositionMutex); applyOffsetChange(); - bool hasChanged = mPosition != position || mWorldPositionChanged; - mPreviousPosition = mPosition; - mPosition = position; + bool hasChanged = (mPosition != position && !mSkipSimulation) || mWorldPositionChanged; + if (!mSkipSimulation) + { + mPreviousPosition = mPosition; + mPosition = position; + } return hasChanged; } -void Actor::adjustPosition(const osg::Vec3f& offset, bool ignoreCollisions) +void Actor::adjustPosition(const osg::Vec3f& offset) { std::scoped_lock lock(mPositionMutex); mPositionOffset += offset; - mSkipCollisions = mSkipCollisions || ignoreCollisions; } void Actor::applyOffsetChange() @@ -191,16 +188,6 @@ void Actor::applyOffsetChange() mWorldPositionChanged = true; } -osg::Vec3f Actor::getPosition() const -{ - return mPosition; -} - -osg::Vec3f Actor::getPreviousPosition() const -{ - return mPreviousPosition; -} - void Actor::setRotation(osg::Quat quat) { std::scoped_lock lock(mPositionMutex); @@ -288,19 +275,15 @@ void Actor::setStandingOnPtr(const MWWorld::Ptr& ptr) mStandingOnPtr = ptr; } -bool Actor::skipCollisions() +bool Actor::canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const { - return std::exchange(mSkipCollisions, false); -} - -void Actor::setVelocity(osg::Vec3f velocity) -{ - mVelocity = velocity; -} - -osg::Vec3f Actor::velocity() -{ - return std::exchange(mVelocity, osg::Vec3f()); + const float halfZ = getHalfExtents().z(); + const osg::Vec3f actorPosition = getPosition(); + const osg::Vec3f startingPosition(actorPosition.x(), actorPosition.y(), actorPosition.z() + halfZ); + const osg::Vec3f destinationPosition(actorPosition.x(), actorPosition.y(), waterlevel + halfZ); + MWPhysics::ActorTracer tracer; + tracer.doTrace(getCollisionObject(), startingPosition, destinationPosition, world); + return (tracer.mFraction >= 1.0f); } } diff --git a/apps/openmw/mwphysics/actor.hpp b/apps/openmw/mwphysics/actor.hpp index 0846401c1d..01d8037f6b 100644 --- a/apps/openmw/mwphysics/actor.hpp +++ b/apps/openmw/mwphysics/actor.hpp @@ -12,6 +12,7 @@ class btCollisionShape; class btCollisionObject; +class btCollisionWorld; class btConvexShape; namespace Resource @@ -59,7 +60,6 @@ namespace MWPhysics * to account for e.g. scripted movements */ void setSimulationPosition(const osg::Vec3f& position); - osg::Vec3f getSimulationPosition() const; void updateCollisionObjectPosition(); @@ -89,15 +89,11 @@ namespace MWPhysics void updatePosition(); // register a position offset that will be applied during simulation. - void adjustPosition(const osg::Vec3f& offset, bool ignoreCollisions); + void adjustPosition(const osg::Vec3f& offset); // apply position offset. Can't be called during simulation void applyOffsetChange(); - osg::Vec3f getPosition() const; - - osg::Vec3f getPreviousPosition() const; - /** * Returns the half extents of the collision body (scaled according to rendering scale) * @note The reason we need this extra method is because of an inconsistency in MW - NPC race scales aren't applied to the collision shape, @@ -160,10 +156,7 @@ namespace MWPhysics mLastStuckPosition = position; } - bool skipCollisions(); - - void setVelocity(osg::Vec3f velocity); - osg::Vec3f velocity(); + bool canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const; private: MWWorld::Ptr mStandingOnPtr; @@ -190,13 +183,8 @@ namespace MWPhysics osg::Quat mRotation; osg::Vec3f mScale; - osg::Vec3f mSimulationPosition; - osg::Vec3f mPosition; - osg::Vec3f mPreviousPosition; osg::Vec3f mPositionOffset; - osg::Vec3f mVelocity; bool mWorldPositionChanged; - bool mSkipCollisions; bool mSkipSimulation; mutable std::mutex mPositionMutex; diff --git a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp index fc8725f5f4..a01ab96301 100644 --- a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp +++ b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp @@ -15,9 +15,9 @@ namespace MWPhysics const btVector3& position, const btScalar radius) { const btVector3 nearest( - std::max(aabbMin.x(), std::min(aabbMax.x(), position.x())), - std::max(aabbMin.y(), std::min(aabbMax.y(), position.y())), - std::max(aabbMin.z(), std::min(aabbMax.z(), position.z())) + std::clamp(position.x(), aabbMin.x(), aabbMax.x()), + std::clamp(position.y(), aabbMin.y(), aabbMax.y()), + std::clamp(position.z(), aabbMin.z(), aabbMax.z()) ); return nearest.distance(position) < radius; } diff --git a/apps/openmw/mwphysics/movementsolver.cpp b/apps/openmw/mwphysics/movementsolver.cpp index 44a5391f0d..fc2ea57b40 100644 --- a/apps/openmw/mwphysics/movementsolver.cpp +++ b/apps/openmw/mwphysics/movementsolver.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,8 @@ #include "constants.hpp" #include "contacttestwrapper.h" #include "physicssystem.hpp" +#include "projectile.hpp" +#include "projectileconvexcallback.hpp" #include "stepper.hpp" #include "trace.h" @@ -116,7 +119,7 @@ namespace MWPhysics } void MovementSolver::move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, - WorldFrameData& worldData) + const WorldFrameData& worldData) { // Reset per-frame data actor.mWalkingOnWater = false; @@ -203,7 +206,7 @@ namespace MWPhysics if((newPosition - nextpos).length2() > 0.0001) { // trace to where character would go if there were no obstructions - tracer.doTrace(actor.mCollisionObject, newPosition, nextpos, collisionWorld); + tracer.doTrace(actor.mCollisionObject, newPosition, nextpos, collisionWorld, actor.mIsOnGround); // check for obstructions if(tracer.mFraction >= 1.0f) @@ -338,7 +341,7 @@ namespace MWPhysics osg::Vec3f from = newPosition; auto dropDistance = 2*sGroundOffset + (actor.mIsOnGround ? sStepSizeDown : 0); osg::Vec3f to = newPosition - osg::Vec3f(0,0,dropDistance); - tracer.doTrace(actor.mCollisionObject, from, to, collisionWorld); + tracer.doTrace(actor.mCollisionObject, from, to, collisionWorld, actor.mIsOnGround); if(tracer.mFraction < 1.0f) { if (!isActor(tracer.mHitObject)) @@ -398,6 +401,29 @@ namespace MWPhysics actor.mPosition.z() -= actor.mHalfExtentsZ; // vanilla-accurate } + void MovementSolver::move(ProjectileFrameData& projectile, float time, const btCollisionWorld* collisionWorld) + { + btVector3 btFrom = Misc::Convert::toBullet(projectile.mPosition); + btVector3 btTo = Misc::Convert::toBullet(projectile.mPosition + projectile.mMovement * time); + + if (btFrom == btTo) + return; + + ProjectileConvexCallback resultCallback(projectile.mCaster, projectile.mCollisionObject, btFrom, btTo, projectile.mProjectile); + resultCallback.m_collisionFilterMask = 0xff; + resultCallback.m_collisionFilterGroup = CollisionType_Projectile; + + const btQuaternion btrot = btQuaternion::getIdentity(); + btTransform from_ (btrot, btFrom); + btTransform to_ (btrot, btTo); + + const btCollisionShape* shape = projectile.mCollisionObject->getCollisionShape(); + assert(shape->isConvex()); + collisionWorld->convexSweepTest(static_cast(shape), from_, to_, resultCallback); + + projectile.mPosition = Misc::Convert::toOsg(projectile.mProjectile->isActive() ? btTo : resultCallback.m_hitPointWorld); + } + btVector3 addMarginToDelta(btVector3 delta) { if(delta.length2() == 0.0) diff --git a/apps/openmw/mwphysics/movementsolver.hpp b/apps/openmw/mwphysics/movementsolver.hpp index 30733eeec8..1bbe76cbec 100644 --- a/apps/openmw/mwphysics/movementsolver.hpp +++ b/apps/openmw/mwphysics/movementsolver.hpp @@ -37,13 +37,15 @@ namespace MWPhysics class Actor; struct ActorFrameData; + struct ProjectileFrameData; struct WorldFrameData; class MovementSolver { 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 move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, const WorldFrameData& worldData); + static void move(ProjectileFrameData& projectile, float time, const btCollisionWorld* collisionWorld); static void unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld); }; } diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index a0dde67c2e..5c49dee297 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -105,10 +105,134 @@ namespace return actorData.mPosition.z() < actorData.mSwimLevel; } - osg::Vec3f interpolateMovements(MWPhysics::Actor& actor, MWPhysics::ActorFrameData& actorData, float timeAccum, float physicsDt) + osg::Vec3f interpolateMovements(const MWPhysics::PtrHolder& ptr, float timeAccum, float physicsDt) { const float interpolationFactor = std::clamp(timeAccum / physicsDt, 0.0f, 1.0f); - return actorData.mPosition * interpolationFactor + actor.getPreviousPosition() * (1.f - interpolationFactor); + return ptr.getPosition() * interpolationFactor + ptr.getPreviousPosition() * (1.f - interpolationFactor); + } + + namespace Visitors + { + struct InitPosition + { + const btCollisionWorld* mCollisionWorld; + void operator()(MWPhysics::ActorSimulation& sim) const + { + auto& [actor, frameData] = sim; + actor->applyOffsetChange(); + frameData.mPosition = actor->getPosition(); + if (frameData.mWaterCollision && frameData.mPosition.z() < frameData.mWaterlevel && actor->canMoveToWaterSurface(frameData.mWaterlevel, mCollisionWorld)) + { + const auto offset = osg::Vec3f(0, 0, frameData.mWaterlevel - frameData.mPosition.z()); + MWBase::Environment::get().getWorld()->moveObjectBy(actor->getPtr(), offset); + actor->applyOffsetChange(); + frameData.mPosition = actor->getPosition(); + } + frameData.mOldHeight = frameData.mPosition.z(); + const auto rotation = actor->getPtr().getRefData().getPosition().asRotationVec3(); + frameData.mRotation = osg::Vec2f(rotation.x(), rotation.z()); + frameData.mInertia = actor->getInertialForce(); + frameData.mStuckFrames = actor->getStuckFrames(); + frameData.mLastStuckPosition = actor->getLastStuckPosition(); + } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + } + }; + + struct PreStep + { + btCollisionWorld* mCollisionWorld; + void operator()(MWPhysics::ActorSimulation& sim) const + { + MWPhysics::MovementSolver::unstuck(sim.second, mCollisionWorld); + } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + } + }; + + struct UpdatePosition + { + btCollisionWorld* mCollisionWorld; + void operator()(MWPhysics::ActorSimulation& sim) const + { + auto& [actor, frameData] = sim; + if (actor->setPosition(frameData.mPosition)) + { + frameData.mPosition = actor->getPosition(); // account for potential position change made by script + actor->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); + } + } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + auto& [proj, frameData] = sim; + proj->setPosition(frameData.mPosition); + proj->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(proj->getCollisionObject()); + } + }; + + struct Move + { + const float mPhysicsDt; + const btCollisionWorld* mCollisionWorld; + const MWPhysics::WorldFrameData& mWorldFrameData; + void operator()(MWPhysics::ActorSimulation& sim) const + { + MWPhysics::MovementSolver::move(sim.second, mPhysicsDt, mCollisionWorld, mWorldFrameData); + } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + MWPhysics::MovementSolver::move(sim.second, mPhysicsDt, mCollisionWorld); + } + }; + + struct Sync + { + const bool mAdvanceSimulation; + const float mTimeAccum; + const float mPhysicsDt; + const MWPhysics::PhysicsTaskScheduler* scheduler; + void operator()(MWPhysics::ActorSimulation& sim) const + { + auto& [actor, frameData] = sim; + auto ptr = actor->getPtr(); + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + const float heightDiff = frameData.mPosition.z() - frameData.mOldHeight; + const bool isStillOnGround = (mAdvanceSimulation && frameData.mWasOnGround && frameData.mIsOnGround); + + if (isStillOnGround || frameData.mFlying || isUnderWater(frameData) || frameData.mSlowFall < 1) + stats.land(ptr == MWMechanics::getPlayer() && (frameData.mFlying || isUnderWater(frameData))); + else if (heightDiff < 0) + stats.addToFallHeight(-heightDiff); + + actor->setSimulationPosition(::interpolateMovements(*actor, mTimeAccum, mPhysicsDt)); + actor->setLastStuckPosition(frameData.mLastStuckPosition); + actor->setStuckFrames(frameData.mStuckFrames); + if (mAdvanceSimulation) + { + MWWorld::Ptr standingOn; + auto* ptrHolder = static_cast(scheduler->getUserPointer(frameData.mStandingOn)); + if (ptrHolder) + standingOn = ptrHolder->getPtr(); + actor->setStandingOnPtr(standingOn); + // the "on ground" state of an actor might have been updated by a traceDown, don't overwrite the change + if (actor->getOnGround() == frameData.mWasOnGround) + actor->setOnGround(frameData.mIsOnGround); + actor->setOnSlope(frameData.mIsOnSlope); + actor->setWalkingOnWater(frameData.mWalkingOnWater); + actor->setInertialForce(frameData.mInertia); + } + } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + auto& [proj, frameData] = sim; + proj->setSimulationPosition(::interpolateMovements(*proj, mTimeAccum, mPhysicsDt)); + } + }; } namespace Config @@ -235,13 +359,12 @@ namespace MWPhysics return std::make_tuple(numSteps, actualDelta); } - void PhysicsTaskScheduler::applyQueuedMovements(float & timeAccum, std::vector>&& actors, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) + void PhysicsTaskScheduler::applyQueuedMovements(float & timeAccum, std::vector&& simulations, 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. MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); - assert(actors.size() == actorsData.size()); double timeStart = mTimer->tick(); @@ -259,19 +382,19 @@ namespace MWPhysics timeAccum -= numSteps*newDelta; // init - for (size_t i = 0; i < actors.size(); ++i) + const Visitors::InitPosition vis{mCollisionWorld}; + for (auto& sim : simulations) { - actorsData[i].updatePosition(*actors[i], mCollisionWorld); + std::visit(vis, sim); } mPrevStepCount = numSteps; mRemainingSteps = numSteps; mTimeAccum = timeAccum; mPhysicsDt = newDelta; - mActors = std::move(actors); - mActorsFrameData = std::move(actorsData); + mSimulations = std::move(simulations); mAdvanceSimulation = (mRemainingSteps != 0); mNewFrame = true; - mNumJobs = mActorsFrameData.size(); + mNumJobs = mSimulations.size(); mNextLOS.store(0, std::memory_order_relaxed); mNextJob.store(0, std::memory_order_release); @@ -301,8 +424,7 @@ namespace MWPhysics MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); mBudget.reset(mDefaultPhysicsDt); mAsyncBudget.reset(0.0f); - mActors.clear(); - mActorsFrameData.clear(); + mSimulations.clear(); for (const auto& [_, actor] : actors) { actor->updatePosition(); @@ -448,7 +570,7 @@ namespace MWPhysics } else if (const auto projectile = std::dynamic_pointer_cast(ptr)) { - projectile->commitPositionChange(); + projectile->updateCollisionObjectPosition(); mCollisionWorld->updateSingleAabb(projectile->getCollisionObject()); } } @@ -467,47 +589,11 @@ namespace MWPhysics void PhysicsTaskScheduler::updateActorsPositions() { - for (size_t i = 0; i < mActors.size(); ++i) + const Visitors::UpdatePosition vis{mCollisionWorld}; + for (auto& sim : mSimulations) { - if (mActors[i]->setPosition(mActorsFrameData[i].mPosition)) - { - MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); - mActorsFrameData[i].mPosition = mActors[i]->getPosition(); // account for potential position change made by script - mActors[i]->updateCollisionObjectPosition(); - mCollisionWorld->updateSingleAabb(mActors[i]->getCollisionObject()); - } - } - } - - void PhysicsTaskScheduler::updateActor(Actor& actor, ActorFrameData& actorData, bool simulationPerformed, float timeAccum, float dt) const - { - auto ptr = actor.getPtr(); - - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - const float heightDiff = actorData.mPosition.z() - actorData.mOldHeight; - const bool isStillOnGround = (simulationPerformed && actorData.mWasOnGround && actorData.mIsOnGround); - - if (isStillOnGround || actorData.mFlying || isUnderWater(actorData) || actorData.mSlowFall < 1) - stats.land(ptr == MWMechanics::getPlayer() && (actorData.mFlying || isUnderWater(actorData))); - else if (heightDiff < 0) - stats.addToFallHeight(-heightDiff); - - actor.setSimulationPosition(interpolateMovements(actor, actorData, timeAccum, dt)); - actor.setLastStuckPosition(actorData.mLastStuckPosition); - actor.setStuckFrames(actorData.mStuckFrames); - if (simulationPerformed) - { - MWWorld::Ptr standingOn; - auto* ptrHolder = static_cast(getUserPointer(actorData.mStandingOn)); - if (ptrHolder) - standingOn = ptrHolder->getPtr(); - actor.setStandingOnPtr(standingOn); - // the "on ground" state of an actor might have been updated by a traceDown, don't overwrite the change - if (actor.getOnGround() == actorData.mWasOnGround) - actor.setOnGround(actorData.mIsOnGround); - actor.setOnSlope(actorData.mIsOnSlope); - actor.setWalkingOnWater(actorData.mWalkingOnWater); - actor.setInertialForce(actorData.mInertia); + MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); + std::visit(vis, sim); } } @@ -532,10 +618,11 @@ namespace MWPhysics { mPreStepBarrier->wait([this] { afterPreStep(); }); int job = 0; + const Visitors::Move vis{mPhysicsDt, mCollisionWorld, *mWorldFrameData}; while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) { MaybeLock lockColWorld(mCollisionWorldMutex, mNumThreads); - MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld, *mWorldFrameData); + std::visit(vis, mSimulations[job]); } mPostStepBarrier->wait([this] { afterPostStep(); }); @@ -577,7 +664,7 @@ namespace MWPhysics void PhysicsTaskScheduler::releaseSharedStates() { std::scoped_lock lock(mSimulationMutex, mUpdateAabbMutex); - mActors.clear(); + mSimulations.clear(); mUpdateAabb.clear(); } @@ -586,10 +673,11 @@ namespace MWPhysics updateAabbs(); if (!mRemainingSteps) return; - for (size_t i = 0; i < mActors.size(); ++i) + const Visitors::PreStep vis{mCollisionWorld}; + for (auto& sim : mSimulations) { MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); - MovementSolver::unstuck(mActorsFrameData[i], mCollisionWorld); + std::visit(vis, sim); } } @@ -618,7 +706,8 @@ namespace MWPhysics void PhysicsTaskScheduler::syncWithMainThread() { - for (size_t i = 0; i < mActors.size(); ++i) - updateActor(*mActors[i], mActorsFrameData[i], mAdvanceSimulation, mTimeAccum, mPhysicsDt); + const Visitors::Sync vis{mAdvanceSimulation, mTimeAccum, mPhysicsDt, this}; + for (auto& sim : mSimulations) + std::visit(vis, sim); } } diff --git a/apps/openmw/mwphysics/mtphysics.hpp b/apps/openmw/mwphysics/mtphysics.hpp index 08997947e4..44330b2cc6 100644 --- a/apps/openmw/mwphysics/mtphysics.hpp +++ b/apps/openmw/mwphysics/mtphysics.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -39,7 +40,7 @@ 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 - void applyQueuedMovements(float & timeAccum, std::vector>&& actors, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + void applyQueuedMovements(float & timeAccum, std::vector&& simulations, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); void resetSimulation(const ActorMap& actors); @@ -57,14 +58,12 @@ namespace MWPhysics bool getLineOfSight(const std::shared_ptr& actor1, const std::shared_ptr& actor2); void debugDraw(); void* getUserPointer(const btCollisionObject* object) const; - void releaseSharedStates(); // destroy all objects whose destructor can't be safely called from ~PhysicsTaskScheduler() private: void doSimulation(); void worker(); void updateActorsPositions(); - void updateActor(Actor& actor, ActorFrameData& actorData, bool simulationPerformed, float timeAccum, float dt) const; bool hasLineOfSight(const Actor* actor1, const Actor* actor2); void refreshLOSCache(); void updateAabbs(); @@ -77,8 +76,7 @@ namespace MWPhysics void syncWithMainThread(); std::unique_ptr mWorldFrameData; - std::vector> mActors; - std::vector mActorsFrameData; + std::vector mSimulations; std::unordered_set mCollisionObjects; float mDefaultPhysicsDt; float mPhysicsDt; diff --git a/apps/openmw/mwphysics/object.cpp b/apps/openmw/mwphysics/object.cpp index 879c12124e..08fcc7e47d 100644 --- a/apps/openmw/mwphysics/object.cpp +++ b/apps/openmw/mwphysics/object.cpp @@ -24,7 +24,7 @@ namespace MWPhysics , mTaskScheduler(scheduler) { mPtr = ptr; - mCollisionObject = BulletHelpers::makeCollisionObject(mShapeInstance->getCollisionShape(), + mCollisionObject = BulletHelpers::makeCollisionObject(mShapeInstance->mCollisionShape.get(), Misc::Convert::toBullet(mPosition), Misc::Convert::toBullet(rotation)); mCollisionObject->setUserPointer(this); mShapeInstance->setLocalScaling(mScale); @@ -109,9 +109,9 @@ namespace MWPhysics if (mShapeInstance->mAnimatedShapes.empty()) return false; - assert (mShapeInstance->getCollisionShape()->isCompound()); + assert (mShapeInstance->mCollisionShape->isCompound()); - btCompoundShape* compound = static_cast(mShapeInstance->getCollisionShape()); + btCompoundShape* compound = static_cast(mShapeInstance->mCollisionShape.get()); for (const auto& [recIndex, shapeIndex] : mShapeInstance->mAnimatedShapes) { auto nodePathFound = mRecIndexToNodePath.find(recIndex); diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 6c46fd6d05..98e3bcf737 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -60,19 +60,6 @@ namespace { - bool canMoveToWaterSurface(const MWPhysics::Actor* physicActor, const float waterlevel, btCollisionWorld* world) - { - if (!physicActor) - return false; - const float halfZ = physicActor->getHalfExtents().z(); - const osg::Vec3f actorPosition = physicActor->getPosition(); - const osg::Vec3f startingPosition(actorPosition.x(), actorPosition.y(), actorPosition.z() + halfZ); - const osg::Vec3f destinationPosition(actorPosition.x(), actorPosition.y(), waterlevel + halfZ); - MWPhysics::ActorTracer tracer; - tracer.doTrace(physicActor->getCollisionObject(), startingPosition, destinationPosition, world); - return (tracer.mFraction >= 1.0f); - } - void handleJump(const MWWorld::Ptr &ptr) { if (!ptr.getClass().isActor()) @@ -370,6 +357,8 @@ namespace MWPhysics bool PhysicsSystem::getLineOfSight(const MWWorld::ConstPtr &actor1, const MWWorld::ConstPtr &actor2) const { + if (actor1 == actor2) return true; + const auto it1 = mActors.find(actor1.mRef); const auto it2 = mActors.find(actor2.mRef); if (it1 == mActors.end() || it2 == mActors.end()) @@ -386,7 +375,8 @@ namespace MWPhysics bool PhysicsSystem::canMoveToWaterSurface(const MWWorld::ConstPtr &actor, const float waterlevel) { - return ::canMoveToWaterSurface(getActor(actor), waterlevel, mCollisionWorld.get()); + const auto* physactor = getActor(actor); + return physactor && physactor->canMoveToWaterSurface(waterlevel, mCollisionWorld.get()); } osg::Vec3f PhysicsSystem::getHalfExtents(const MWWorld::ConstPtr &actor) const @@ -491,7 +481,7 @@ namespace MWPhysics if (ptr.mRef->mData.mPhysicsPostponed) return; osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); - if (!shapeInstance || !shapeInstance->getCollisionShape()) + if (!shapeInstance || !shapeInstance->mCollisionShape) return; assert(!getObject(ptr)); @@ -526,10 +516,10 @@ namespace MWPhysics void PhysicsSystem::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated) { - if (auto found = mObjects.find(old.mRef); found != mObjects.end()) - found->second->updatePtr(updated); - else if (auto found = mActors.find(old.mRef); found != mActors.end()) - found->second->updatePtr(updated); + if (auto foundObject = mObjects.find(old.mRef); foundObject != mObjects.end()) + foundObject->second->updatePtr(updated); + else if (auto foundActor = mActors.find(old.mRef); foundActor != mActors.end()) + foundActor->second->updatePtr(updated); for (auto& [_, actor] : mActors) { @@ -592,33 +582,6 @@ namespace MWPhysics } } - void PhysicsSystem::updateProjectile(const int projectileId, const osg::Vec3f &position) const - { - const auto foundProjectile = mProjectiles.find(projectileId); - assert(foundProjectile != mProjectiles.end()); - auto* projectile = foundProjectile->second.get(); - - btVector3 btFrom = Misc::Convert::toBullet(projectile->getPosition()); - btVector3 btTo = Misc::Convert::toBullet(position); - - if (btFrom == btTo) - return; - - ProjectileConvexCallback resultCallback(projectile->getCasterCollisionObject(), projectile->getCollisionObject(), btFrom, btTo, projectile); - resultCallback.m_collisionFilterMask = 0xff; - resultCallback.m_collisionFilterGroup = CollisionType_Projectile; - - const btQuaternion btrot = btQuaternion::getIdentity(); - btTransform from_ (btrot, btFrom); - btTransform to_ (btrot, btTo); - - mTaskScheduler->convexSweepTest(projectile->getConvexShape(), from_, to_, resultCallback); - - const auto newpos = projectile->isActive() ? position : Misc::Convert::toOsg(projectile->getHitPosition()); - projectile->setPosition(newpos); - mTaskScheduler->updateSingleAabb(foundProjectile->second); - } - void PhysicsSystem::updateRotation(const MWWorld::Ptr &ptr, osg::Quat rotate) { if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) @@ -655,7 +618,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->mCollisionBox.extents.length2() == 0) + if (!ptr.getClass().isNpc() && shape && shape->mCollisionBox.mExtents.length2() == 0) { const std::string fallbackModel = ptr.getClass().getModel(ptr); if (fallbackModel != mesh) @@ -680,7 +643,7 @@ namespace MWPhysics { osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); assert(shapeInstance); - float radius = computeRadius ? shapeInstance->mCollisionBox.extents.length() / 2.f : 1.f; + float radius = computeRadius ? shapeInstance->mCollisionBox.mExtents.length() / 2.f : 1.f; mProjectileId++; @@ -727,11 +690,10 @@ namespace MWPhysics actor->setVelocity(osg::Vec3f()); } - std::pair>, std::vector> PhysicsSystem::prepareFrameData(bool willSimulate) + std::vector PhysicsSystem::prepareSimulation(bool willSimulate) { - std::pair>, std::vector> framedata; - framedata.first.reserve(mActors.size()); - framedata.second.reserve(mActors.size()); + std::vector simulations; + simulations.reserve(mActors.size() + mProjectiles.size()); const MWBase::World *world = MWBase::Environment::get().getWorld(); for (const auto& [ref, physicActor] : mActors) { @@ -756,18 +718,23 @@ namespace MWPhysics physicActor->setCanWaterWalk(waterCollision); // Slow fall reduces fall speed by a factor of (effect magnitude / 200) - const float slowFall = 1.f - std::max(0.f, std::min(1.f, effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f)); + const float slowFall = 1.f - std::clamp(effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f, 0.f, 1.f); const bool godmode = ptr == world->getPlayerConstPtr() && world->getGodModeState(); const bool inert = stats.isDead() || (!godmode && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0); - framedata.first.emplace_back(physicActor); - framedata.second.emplace_back(*physicActor, inert, waterCollision, slowFall, waterlevel); + simulations.emplace_back(ActorSimulation{physicActor, ActorFrameData{*physicActor, inert, waterCollision, slowFall, waterlevel}}); // if the simulation will run, a jump request will be fulfilled. Update mechanics accordingly. if (willSimulate) handleJump(ptr); } - return framedata; + + for (const auto& [id, projectile] : mProjectiles) + { + simulations.emplace_back(ProjectileSimulation{projectile, ProjectileFrameData{*projectile}}); + } + + return simulations; } void PhysicsSystem::stepSimulation(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) @@ -793,9 +760,9 @@ namespace MWPhysics mTaskScheduler->resetSimulation(mActors); else { - auto [actors, framedata] = prepareFrameData(mTimeAccum >= mPhysicsDt); + auto simulations = prepareSimulation(mTimeAccum >= mPhysicsDt); // modifies mTimeAccum - mTaskScheduler->applyQueuedMovements(mTimeAccum, std::move(actors), std::move(framedata), frameStart, frameNumber, stats); + mTaskScheduler->applyQueuedMovements(mTimeAccum, std::move(simulations), frameStart, frameNumber, stats); } } @@ -974,25 +941,17 @@ namespace MWPhysics , mWasOnGround(actor.getOnGround()) , mIsAquatic(actor.getPtr().getClass().isPureWaterCreature(actor.getPtr())) , mWaterCollision(waterCollision) - , mSkipCollisionDetection(actor.skipCollisions() || !actor.getCollisionMode()) + , mSkipCollisionDetection(!actor.getCollisionMode()) { } - void ActorFrameData::updatePosition(Actor& actor, btCollisionWorld* world) + ProjectileFrameData::ProjectileFrameData(Projectile& projectile) + : mPosition(projectile.getPosition()) + , mMovement(projectile.velocity()) + , mCaster(projectile.getCasterCollisionObject()) + , mCollisionObject(projectile.getCollisionObject()) + , mProjectile(&projectile) { - actor.applyOffsetChange(); - mPosition = actor.getPosition(); - if (mWaterCollision && mPosition.z() < mWaterlevel && canMoveToWaterSurface(&actor, mWaterlevel, world)) - { - mPosition.z() = mWaterlevel; - MWBase::Environment::get().getWorld()->moveObject(actor.getPtr(), mPosition, false); - } - mOldHeight = mPosition.z(); - const auto rotation = actor.getPtr().getRefData().getPosition().asRotationVec3(); - mRotation = osg::Vec2f(rotation.x(), rotation.z()); - mInertia = actor.getInertialForce(); - mStuckFrames = actor.getStuckFrames(); - mLastStuckPosition = actor.getLastStuckPosition(); } WorldFrameData::WorldFrameData() diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 6ec4ebfda9..c31bbfbf65 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -75,7 +76,6 @@ namespace MWPhysics struct ActorFrameData { ActorFrameData(Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel); - void updatePosition(Actor& actor, btCollisionWorld* world); osg::Vec3f mPosition; osg::Vec3f mInertia; const btCollisionObject* mStandingOn; @@ -100,6 +100,16 @@ namespace MWPhysics const bool mSkipCollisionDetection; }; + struct ProjectileFrameData + { + explicit ProjectileFrameData(Projectile& projectile); + osg::Vec3f mPosition; + osg::Vec3f mMovement; + const btCollisionObject* mCaster; + const btCollisionObject* mCollisionObject; + Projectile* mProjectile; + }; + struct WorldFrameData { WorldFrameData(); @@ -107,6 +117,10 @@ namespace MWPhysics osg::Vec3f mStormDirection; }; + using ActorSimulation = std::pair, ActorFrameData>; + using ProjectileSimulation = std::pair, ProjectileFrameData>; + using Simulation = std::variant; + class PhysicsSystem : public RayCastingInterface { public: @@ -124,7 +138,6 @@ namespace MWPhysics int addProjectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius); void setCaster(int projectileId, const MWWorld::Ptr& caster); - void updateProjectile(const int projectileId, const osg::Vec3f &position) const; void removeProjectile(const int projectileId); void updatePtr (const MWWorld::Ptr& old, const MWWorld::Ptr& updated); @@ -253,7 +266,7 @@ namespace MWPhysics void updateWater(); - std::pair>, std::vector> prepareFrameData(bool willSimulate); + std::vector prepareSimulation(bool willSimulate); std::unique_ptr mBroadphase; std::unique_ptr mCollisionConfiguration; diff --git a/apps/openmw/mwphysics/projectile.cpp b/apps/openmw/mwphysics/projectile.cpp index 4efb245149..9f8962d5e6 100644 --- a/apps/openmw/mwphysics/projectile.cpp +++ b/apps/openmw/mwphysics/projectile.cpp @@ -31,14 +31,15 @@ Projectile::Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, f mCollisionObject->setCollisionShape(mShape.get()); mCollisionObject->setUserPointer(this); - setPosition(position); + mPosition = position; + mPreviousPosition = position; setCaster(caster); const int collisionMask = CollisionType_World | CollisionType_HeightMap | CollisionType_Actor | CollisionType_Door | CollisionType_Water | CollisionType_Projectile; mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Projectile, collisionMask); - commitPositionChange(); + updateCollisionObjectPosition(); } Projectile::~Projectile() @@ -48,29 +49,12 @@ Projectile::~Projectile() mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } -void Projectile::commitPositionChange() +void Projectile::updateCollisionObjectPosition() { std::scoped_lock lock(mMutex); - if (mTransformUpdatePending) - { - auto& trans = mCollisionObject->getWorldTransform(); - trans.setOrigin(Misc::Convert::toBullet(mPosition)); - mCollisionObject->setWorldTransform(trans); - mTransformUpdatePending = false; - } -} - -void Projectile::setPosition(const osg::Vec3f &position) -{ - std::scoped_lock lock(mMutex); - mPosition = position; - mTransformUpdatePending = true; -} - -osg::Vec3f Projectile::getPosition() const -{ - std::scoped_lock lock(mMutex); - return mPosition; + auto& trans = mCollisionObject->getWorldTransform(); + trans.setOrigin(Misc::Convert::toBullet(mPosition)); + mCollisionObject->setWorldTransform(trans); } void Projectile::hit(const btCollisionObject* target, btVector3 pos, btVector3 normal) diff --git a/apps/openmw/mwphysics/projectile.hpp b/apps/openmw/mwphysics/projectile.hpp index 5e4e487c03..10ed2c9582 100644 --- a/apps/openmw/mwphysics/projectile.hpp +++ b/apps/openmw/mwphysics/projectile.hpp @@ -36,10 +36,7 @@ namespace MWPhysics btConvexShape* getConvexShape() const { return mConvexShape; } - void commitPositionChange(); - - void setPosition(const osg::Vec3f& position); - osg::Vec3f getPosition() const; + void updateCollisionObjectPosition(); bool isActive() const { @@ -80,13 +77,11 @@ namespace MWPhysics std::unique_ptr mShape; btConvexShape* mConvexShape; - bool mTransformUpdatePending; bool mHitWater; std::atomic mActive; MWWorld::Ptr mCaster; const btCollisionObject* mCasterColObj; const btCollisionObject* mHitTarget; - osg::Vec3f mPosition; btVector3 mHitPosition; btVector3 mHitNormal; diff --git a/apps/openmw/mwphysics/ptrholder.hpp b/apps/openmw/mwphysics/ptrholder.hpp index e84f3d1cfe..fcd6ce203a 100644 --- a/apps/openmw/mwphysics/ptrholder.hpp +++ b/apps/openmw/mwphysics/ptrholder.hpp @@ -30,9 +30,49 @@ namespace MWPhysics return mCollisionObject.get(); } + void setVelocity(osg::Vec3f velocity) + { + mVelocity = velocity; + } + + osg::Vec3f velocity() + { + return std::exchange(mVelocity, osg::Vec3f()); + } + + void setSimulationPosition(const osg::Vec3f& position) + { + mSimulationPosition = position; + } + + osg::Vec3f getSimulationPosition() const + { + return mSimulationPosition; + } + + void setPosition(const osg::Vec3f& position) + { + mPreviousPosition = mPosition; + mPosition = position; + } + + osg::Vec3f getPosition() const + { + return mPosition; + } + + osg::Vec3f getPreviousPosition() const + { + return mPreviousPosition; + } + protected: MWWorld::Ptr mPtr; std::unique_ptr mCollisionObject; + osg::Vec3f mVelocity; + osg::Vec3f mSimulationPosition; + osg::Vec3f mPosition; + osg::Vec3f mPreviousPosition; }; } diff --git a/apps/openmw/mwphysics/stepper.cpp b/apps/openmw/mwphysics/stepper.cpp index af85658910..5ef6833701 100644 --- a/apps/openmw/mwphysics/stepper.cpp +++ b/apps/openmw/mwphysics/stepper.cpp @@ -36,7 +36,7 @@ namespace MWPhysics // 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, Constants::sStepSizeUp), mColWorld); + mUpStepper.doTrace(mColObj, position, position + osg::Vec3f(0.0f, 0.0f, Constants::sStepSizeUp), mColWorld, onGround); float upDistance = 0; if(!mUpStepper.mHitObject) @@ -117,7 +117,7 @@ namespace MWPhysics downStepSize = upDistance; else downStepSize = moveDistance + upDistance + sStepSizeDown; - mDownStepper.doTrace(mColObj, tracerDest, tracerDest + osg::Vec3f(0.0f, 0.0f, -downStepSize), mColWorld); + mDownStepper.doTrace(mColObj, tracerDest, tracerDest + osg::Vec3f(0.0f, 0.0f, -downStepSize), mColWorld, onGround); // 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 diff --git a/apps/openmw/mwphysics/trace.cpp b/apps/openmw/mwphysics/trace.cpp index 049d026e8e..b7930bfa53 100644 --- a/apps/openmw/mwphysics/trace.cpp +++ b/apps/openmw/mwphysics/trace.cpp @@ -12,38 +12,84 @@ namespace MWPhysics { -void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) +ActorConvexCallback sweepHelper(const btCollisionObject *actor, const btVector3& from, const btVector3& to, const btCollisionWorld* world, bool actorFilter) { - const btVector3 btstart = Misc::Convert::toBullet(start); - const btVector3 btend = Misc::Convert::toBullet(end); - const btTransform &trans = actor->getWorldTransform(); - btTransform from(trans); - btTransform to(trans); - from.setOrigin(btstart); - to.setOrigin(btend); - - const btVector3 motion = btstart-btend; - ActorConvexCallback 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; + btTransform transFrom(trans); + btTransform transTo(trans); + transFrom.setOrigin(from); + transTo.setOrigin(to); const btCollisionShape *shape = actor->getCollisionShape(); assert(shape->isConvex()); - world->convexSweepTest(static_cast(shape), from, to, newTraceCallback); + + const btVector3 motion = from - to; // FIXME: this is backwards; means ActorConvexCallback is doing dot product tests backwards too + ActorConvexCallback traceCallback(actor, motion, btScalar(0.0), world); + // Inherit the actor's collision group and mask + traceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; + traceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; + if(actorFilter) + traceCallback.m_collisionFilterMask &= ~CollisionType_Actor; + + world->convexSweepTest(static_cast(shape), transFrom, transTo, traceCallback); + return traceCallback; +} + +void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world, bool attempt_short_trace) +{ + const btVector3 btstart = Misc::Convert::toBullet(start); + btVector3 btend = Misc::Convert::toBullet(end); + + // Because Bullet's collision trace tests touch *all* geometry in its path, a lot of long collision tests + // will unnecessarily test against complex meshes that are dozens of units away. This wouldn't normally be + // a problem, but bullet isn't the fastest in the world when it comes to doing tests against triangle meshes. + // Therefore, we try out a short trace first, then only fall back to the full length trace if needed. + // This trace needs to be at least a couple units long, but there's no one particular ideal length. + // The length of 2.1 chosen here is a "works well in practice after testing a few random lengths" value. + // (Also, we only do this short test if the intended collision trace is long enough for it to make sense.) + const float fallback_length = 2.1f; + bool doing_short_trace = false; + // For some reason, typical scenes perform a little better if we increase the threshold length for the length test. + // (Multiplying by 2 in 'square distance' units gives us about 1.4x the threshold length. In benchmarks this was + // slightly better for the performance of normal scenes than 4.0, and just plain better than 1.0.) + if(attempt_short_trace && (btend-btstart).length2() > fallback_length*fallback_length*2.0) + { + btend = btstart + (btend-btstart).normalized()*fallback_length; + doing_short_trace = true; + } + + const auto traceCallback = sweepHelper(actor, btstart, btend, world, false); // Copy the hit data over to our trace results struct: - if(newTraceCallback.hasHit()) + if(traceCallback.hasHit()) { - mFraction = newTraceCallback.m_closestHitFraction; - mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); + mFraction = traceCallback.m_closestHitFraction; + // ensure fraction is correct (covers intended distance traveled instead of actual distance traveled) + if(doing_short_trace && (end-start).length2() > 0.0) + mFraction *= (btend-btstart).length() / (end-start).length(); + mPlaneNormal = Misc::Convert::toOsg(traceCallback.m_hitNormalWorld); mEndPos = (end-start)*mFraction + start; - mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld); - mHitObject = newTraceCallback.m_hitCollisionObject; + mHitPoint = Misc::Convert::toOsg(traceCallback.m_hitPointWorld); + mHitObject = traceCallback.m_hitCollisionObject; } else { + if(doing_short_trace) + { + btend = Misc::Convert::toBullet(end); + const auto newTraceCallback = sweepHelper(actor, btstart, btend, world, false); + + if(newTraceCallback.hasHit()) + { + mFraction = newTraceCallback.m_closestHitFraction; + mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); + mEndPos = (end-start)*mFraction + start; + mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld); + mHitObject = newTraceCallback.m_hitCollisionObject; + return; + } + } + // fallthrough mEndPos = end; mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f); mFraction = 1.0f; @@ -54,25 +100,11 @@ void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& star void ActorTracer::findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) { - const btVector3 btstart = Misc::Convert::toBullet(start); - const btVector3 btend = Misc::Convert::toBullet(end); - - const btTransform &trans = actor->getCollisionObject()->getWorldTransform(); - btTransform from(trans.getBasis(), btstart); - btTransform to(trans.getBasis(), btend); - - const btVector3 motion = btstart-btend; - ActorConvexCallback 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; - newTraceCallback.m_collisionFilterMask &= ~CollisionType_Actor; - - world->convexSweepTest(actor->getConvexShape(), from, to, newTraceCallback); - if(newTraceCallback.hasHit()) + const auto traceCallback = sweepHelper(actor->getCollisionObject(), Misc::Convert::toBullet(start), Misc::Convert::toBullet(end), world, true); + if(traceCallback.hasHit()) { - mFraction = newTraceCallback.m_closestHitFraction; - mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); + mFraction = traceCallback.m_closestHitFraction; + mPlaneNormal = Misc::Convert::toOsg(traceCallback.m_hitNormalWorld); mEndPos = (end-start)*mFraction + start; } else diff --git a/apps/openmw/mwphysics/trace.h b/apps/openmw/mwphysics/trace.h index 0297c9e076..af38756b3e 100644 --- a/apps/openmw/mwphysics/trace.h +++ b/apps/openmw/mwphysics/trace.h @@ -20,7 +20,7 @@ namespace MWPhysics float mFraction; - void doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world); + void doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world, bool attempt_short_trace = false); void findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world); }; } diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index b346d4ac6c..7706f7d7f1 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -74,7 +75,7 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st osg::ref_ptr instance = mResourceSystem->getSceneManager()->getInstance(model, parent); const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) return PartHolderPtr(); @@ -84,30 +85,58 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st return PartHolderPtr(new PartHolder(instance)); } -std::string ActorAnimation::getShieldMesh(const MWWorld::ConstPtr& shield) const +osg::ref_ptr ActorAnimation::attach(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool isLight) +{ + osg::ref_ptr templateNode = mResourceSystem->getSceneManager()->getTemplate(model); + + const NodeMap& nodeMap = getNodeMap(); + auto found = nodeMap.find(bonename); + if (found == nodeMap.end()) + throw std::runtime_error("Can't find attachment node " + bonename); + if(isLight) + { + osg::Quat rotation(osg::DegreesToRadians(-90.f), osg::Vec3f(1,0,0)); + return SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager(), &rotation); + } + return SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager()); +} + +std::string ActorAnimation::getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const { - std::string mesh = shield.getClass().getModel(shield); const ESM::Armor *armor = shield.get()->mBase; const std::vector& bodyparts = armor->mParts.mParts; + // Try to recover the body part model, use ground model as a fallback otherwise. if (!bodyparts.empty()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const MWWorld::Store &partStore = store.get(); - - // Try to get shield model from bodyparts first, with ground model as fallback for (const auto& part : bodyparts) { - // Assume all creatures use the male mesh. - if (part.mPart != ESM::PRT_Shield || part.mMale.empty()) + if (part.mPart != ESM::PRT_Shield) continue; - const ESM::BodyPart *bodypart = partStore.search(part.mMale); - if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty()) + + std::string bodypartName; + if (female && !part.mFemale.empty()) + bodypartName = part.mFemale; + else if (!part.mMale.empty()) + bodypartName = part.mMale; + + if (!bodypartName.empty()) { - mesh = "meshes\\" + bodypart->mModel; - break; + const ESM::BodyPart *bodypart = partStore.search(bodypartName); + if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor) + return std::string(); + if (!bodypart->mModel.empty()) + return "meshes\\" + bodypart->mModel; } } } + return shield.getClass().getModel(shield); +} + +std::string ActorAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const +{ + std::string mesh = getShieldMesh(shield, false); if (mesh.empty()) return mesh; @@ -143,7 +172,7 @@ bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (shield != inv.end() && shield->getType() == ESM::Armor::sRecordId && !getShieldMesh(*shield).empty()) + if (shield != inv.end() && shield->getType() == ESM::Armor::sRecordId && !getSheathedShieldMesh(*shield).empty()) { if(stats.getDrawState() != MWMechanics::DrawState_Weapon) return false; @@ -201,7 +230,7 @@ void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) return; } - std::string mesh = getShieldMesh(*shield); + std::string mesh = getSheathedShieldMesh(*shield); if (mesh.empty()) return; @@ -255,7 +284,7 @@ bool ActorAnimation::useShieldAnimations() const const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (weapon != inv.end() && shield != inv.end() && shield->getType() == ESM::Armor::sRecordId && - !getShieldMesh(*shield).empty()) + !getSheathedShieldMesh(*shield).empty()) { auto type = weapon->getType(); if(type == ESM::Weapon::sRecordId) diff --git a/apps/openmw/mwrender/actoranimation.hpp b/apps/openmw/mwrender/actoranimation.hpp index 61ad1ca235..1ece0c326d 100644 --- a/apps/openmw/mwrender/actoranimation.hpp +++ b/apps/openmw/mwrender/actoranimation.hpp @@ -45,7 +45,8 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener virtual void updateHolsteredWeapon(bool showHolsteredWeapons); virtual void updateHolsteredShield(bool showCarriedLeft); virtual void updateQuiver(); - virtual std::string getShieldMesh(const MWWorld::ConstPtr& shield) const; + std::string getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const; + virtual std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const; virtual std::string getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename, bool enchantedGlow, osg::Vec4f* glowColor); virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename) @@ -53,6 +54,7 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener osg::Vec4f stubColor = osg::Vec4f(0,0,0,0); return attachMesh(model, bonename, false, &stubColor); }; + osg::ref_ptr attach(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool isLight); PartHolderPtr mScabbard; PartHolderPtr mHolsteredShield; diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 10624c8bca..ecfe65c575 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -389,11 +389,6 @@ namespace MWRender mAlpha = alpha; } - void setLightSource(const osg::ref_ptr& lightSource) - { - mLightSource = lightSource; - } - protected: void setDefaults(osg::StateSet* stateset) override { @@ -416,13 +411,10 @@ namespace MWRender { osg::Material* material = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); material->setAlpha(osg::Material::FRONT_AND_BACK, mAlpha); - if (mLightSource) - mLightSource->setActorFade(mAlpha); } private: float mAlpha; - osg::ref_ptr mLightSource; }; struct Animation::AnimSource @@ -968,8 +960,9 @@ namespace MWRender { osg::ref_ptr node = getNodeMap().at(it->first); // this should not throw, we already checked for the node existing in addAnimSource - node->addUpdateCallback(it->second); - mActiveControllers.emplace_back(node, it->second); + osg::Callback* callback = it->second->getAsCallback(); + node->addUpdateCallback(callback); + mActiveControllers.emplace_back(node, callback); if (blendMask == 0 && node == mAccumRoot) { @@ -1319,10 +1312,10 @@ namespace MWRender cache.insert(std::make_pair(model, created)); - return sceneMgr->createInstance(created); + return sceneMgr->getInstance(created); } else - return sceneMgr->createInstance(found->second); + return sceneMgr->getInstance(found->second); } else { @@ -1484,6 +1477,7 @@ namespace MWRender bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); mExtraLightSource = SceneUtil::addLight(parent, esmLight, Mask_ParticleSystem, Mask_Lighting, exterior); + mExtraLightSource->setActorFade(mAlpha); } void Animation::addEffect (const std::string& model, int effectId, bool loop, const std::string& bonename, const std::string& texture) @@ -1510,7 +1504,7 @@ namespace MWRender parentNode = mInsert; else { - NodeMap::const_iterator found = getNodeMap().find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = getNodeMap().find(bonename); if (found == getNodeMap().end()) throw std::runtime_error("Can't find bone " + bonename); @@ -1619,8 +1613,7 @@ namespace MWRender const osg::Node* Animation::getNode(const std::string &name) const { - std::string lowerName = Misc::StringUtils::lowerCase(name); - NodeMap::const_iterator found = getNodeMap().find(lowerName); + NodeMap::const_iterator found = getNodeMap().find(name); if (found == getNodeMap().end()) return nullptr; else @@ -1639,7 +1632,6 @@ namespace MWRender if (mTransparencyUpdater == nullptr) { mTransparencyUpdater = new TransparencyUpdater(alpha); - mTransparencyUpdater->setLightSource(mExtraLightSource); mObjectRoot->addCullCallback(mTransparencyUpdater); } else @@ -1650,6 +1642,8 @@ namespace MWRender mObjectRoot->removeCullCallback(mTransparencyUpdater); mTransparencyUpdater = nullptr; } + if (mExtraLightSource) + mExtraLightSource->setActorFade(alpha); } void Animation::setLightEffect(float effect) diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 84788c1e2d..d37d548dd3 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -7,8 +7,10 @@ #include #include #include +#include #include +#include namespace ESM { @@ -157,6 +159,8 @@ public: virtual bool updateCarriedLeftVisible(const int weaptype) const { return false; }; + typedef std::unordered_map, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> NodeMap; + protected: class AnimationTime : public SceneUtil::ControllerSource { @@ -250,8 +254,6 @@ protected: std::shared_ptr mAnimationTimePtr[sNumBlendMasks]; - // Stored in all lowercase for a case-insensitive lookup - typedef std::map > NodeMap; mutable NodeMap mNodeMap; mutable bool mNodeMapCreated; diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index e750fcad46..d1e842790b 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -236,7 +236,7 @@ namespace MWRender mTotalMovement += speed * duration; speed /= (1.f + speed / 500.f); float maxDelta = 300.f * duration; - mSmoothedSpeed += osg::clampBetween(speed - mSmoothedSpeed, -maxDelta, maxDelta); + mSmoothedSpeed += std::clamp(speed - mSmoothedSpeed, -maxDelta, maxDelta); mMaxNextCameraDistance = mCameraDistance + duration * (100.f + mBaseCameraDistance); updateStandingPreviewMode(); @@ -434,7 +434,7 @@ namespace MWRender { const float epsilon = 0.000001f; float limit = static_cast(osg::PI_2) - epsilon; - mPitch = osg::clampBetween(angle, -limit, limit); + mPitch = std::clamp(angle, -limit, limit); } float Camera::getCameraDistance() const @@ -460,7 +460,7 @@ namespace MWRender } mIsNearest = mBaseCameraDistance <= mNearest; - mBaseCameraDistance = osg::clampBetween(mBaseCameraDistance, mNearest, mFurthest); + mBaseCameraDistance = std::clamp(mBaseCameraDistance, mNearest, mFurthest); Settings::Manager::setFloat("third person camera distance", "Camera", mBaseCameraDistance); } diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 4a84bf4f23..c717806e35 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -190,7 +190,9 @@ namespace MWRender mTexture->setInternalFormat(GL_RGBA); mTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - mTexture->setUserValue("premultiplied alpha", true); + + mTextureStateSet = new osg::StateSet; + mTextureStateSet->setAttribute(new osg::BlendFunc(osg::BlendFunc::ONE, osg::BlendFunc::ONE_MINUS_SRC_ALPHA)); mCamera = new osg::Camera; // hints that the camera is not relative to the master camera diff --git a/apps/openmw/mwrender/characterpreview.hpp b/apps/openmw/mwrender/characterpreview.hpp index 3eb9688465..808ff0801d 100644 --- a/apps/openmw/mwrender/characterpreview.hpp +++ b/apps/openmw/mwrender/characterpreview.hpp @@ -18,6 +18,7 @@ namespace osg class Camera; class Group; class Viewport; + class StateSet; } namespace MWRender @@ -41,6 +42,8 @@ namespace MWRender void rebuild(); osg::ref_ptr getTexture(); + /// Get the osg::StateSet required to render the texture correctly, if any. + osg::StateSet* getTextureStateSet() { return mTextureStateSet; } private: CharacterPreview(const CharacterPreview&); @@ -54,6 +57,7 @@ namespace MWRender osg::ref_ptr mParent; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mTexture; + osg::ref_ptr mTextureStateSet; osg::ref_ptr mCamera; osg::ref_ptr mDrawOnceCallback; diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index 502340d4b9..50dfb68008 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -126,7 +126,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) if (bonename != "Weapon Bone") { const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) bonename = "Weapon Bone"; } @@ -139,32 +139,13 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) bonename = "Shield Bone"; if (item.getType() == ESM::Armor::sRecordId) { - // Shield body part model should be used if possible. - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - for (const auto& part : item.get()->mBase->mParts.mParts) - { - // Assume all creatures use the male mesh. - if (part.mPart != ESM::PRT_Shield || part.mMale.empty()) - continue; - const ESM::BodyPart *bodypart = store.get().search(part.mMale); - if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty()) - { - itemModel = "meshes\\" + bodypart->mModel; - break; - } - } + itemModel = getShieldMesh(item, false); } } try { - osg::ref_ptr node = mResourceSystem->getSceneManager()->getTemplate(itemModel); - - const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); - if (found == nodeMap.end()) - throw std::runtime_error("Can't find attachment node " + bonename); - osg::ref_ptr attached = SceneUtil::attach(node, mObjectRoot, bonename, found->second.get()); + osg::ref_ptr attached = attach(itemModel, bonename, bonename, item.getType() == ESM::Light::sRecordId); scene.reset(new PartHolder(attached)); diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 70f0ff02bb..0eb0e7738a 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -562,8 +562,8 @@ bool LocalMap::isPositionExplored (float nX, float nY, int x, int y) if (!segment.mFogOfWarImage) return false; - nX = std::max(0.f, std::min(1.f, nX)); - nY = std::max(0.f, std::min(1.f, nY)); + nX = std::clamp(nX, 0.f, 1.f); + nY = std::clamp(nY, 0.f, 1.f); int texU = static_cast((sFogOfWarResolution - 1) * nX); int texV = static_cast((sFogOfWarResolution - 1) * nY); @@ -648,7 +648,7 @@ void LocalMap::updatePlayer (const osg::Vec3f& position, const osg::Quat& orient uint32_t clr = *(uint32_t*)data; uint8_t alpha = (clr >> 24); - alpha = std::min( alpha, (uint8_t) (std::max(0.f, std::min(1.f, (sqrDist/sqrExploreRadius)))*255) ); + alpha = std::min( alpha, (uint8_t) (std::clamp((sqrDist/sqrExploreRadius)*255, 0.f, 1.f))); uint32_t val = (uint32_t) (alpha << 24); if ( *data != val) { diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 0e100326dd..0d6c21c308 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -82,34 +82,6 @@ std::string getVampireHead(const std::string& race, bool female) return "meshes\\" + bodyPart->mModel; } -std::string getShieldBodypartMesh(const std::vector& bodyparts, bool female) -{ - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const MWWorld::Store &partStore = store.get(); - for (const auto& part : bodyparts) - { - if (part.mPart != ESM::PRT_Shield) - continue; - - std::string bodypartName; - if (female && !part.mFemale.empty()) - bodypartName = part.mFemale; - else if (!part.mMale.empty()) - bodypartName = part.mMale; - - if (!bodypartName.empty()) - { - const ESM::BodyPart *bodypart = partStore.search(bodypartName); - if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor) - return std::string(); - if (!bodypart->mModel.empty()) - return "meshes\\" + bodypart->mModel; - } - } - - return std::string(); -} - } @@ -547,14 +519,9 @@ void NpcAnimation::updateNpcBase() mWeaponAnimationTime->updateStartTime(); } -std::string NpcAnimation::getShieldMesh(const MWWorld::ConstPtr& shield) const +std::string NpcAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const { - std::string mesh = shield.getClass().getModel(shield); - const ESM::Armor *armor = shield.get()->mBase; - const std::vector& bodyparts = armor->mParts.mParts; - // Try to recover the body part model, use ground model as a fallback otherwise. - if (!bodyparts.empty()) - mesh = getShieldBodypartMesh(bodyparts, !mNpc->isMale()); + std::string mesh = getShieldMesh(shield, !mNpc->isMale()); if (mesh.empty()) return std::string(); @@ -678,7 +645,7 @@ void NpcAnimation::updateParts() { const ESM::Light *light = part.get()->mBase; addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, - 1, "meshes\\"+light->mModel); + 1, "meshes\\"+light->mModel, false, nullptr, true); if (mObjectParts[ESM::PRT_Shield]) addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), light); } @@ -708,16 +675,9 @@ void NpcAnimation::updateParts() -PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool enchantedGlow, osg::Vec4f* glowColor) +PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) { - osg::ref_ptr templateNode = mResourceSystem->getSceneManager()->getTemplate(model); - - const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); - if (found == nodeMap.end()) - throw std::runtime_error("Can't find attachment node " + bonename); - - osg::ref_ptr attached = SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second); + osg::ref_ptr attached = attach(model, bonename, bonefilter, isLight); if (enchantedGlow) mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor); @@ -790,7 +750,7 @@ bool NpcAnimation::isFemalePart(const ESM::BodyPart* bodypart) return bodypart->mData.mFlags & ESM::BodyPart::BPF_Female; } -bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, bool enchantedGlow, osg::Vec4f* glowColor) +bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) { if(priority <= mPartPriorities[type]) return false; @@ -813,7 +773,7 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g if (weaponBonename != bonename) { const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(weaponBonename)); + NodeMap::const_iterator found = nodeMap.find(weaponBonename); if (found != nodeMap.end()) bonename = weaponBonename; } @@ -822,7 +782,7 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g // PRT_Hair seems to be the only type that breaks consistency and uses a filter that's different from the attachment bone const std::string bonefilter = (type == ESM::PRT_Hair) ? "hair" : bonename; - mObjectParts[type] = insertBoundedPart(mesh, bonename, bonefilter, enchantedGlow, glowColor); + mObjectParts[type] = insertBoundedPart(mesh, bonename, bonefilter, enchantedGlow, glowColor, isLight); } catch (std::exception& e) { @@ -1011,13 +971,10 @@ void NpcAnimation::showCarriedLeft(bool show) // For shields we must try to use the body part model if (iter->getType() == ESM::Armor::sRecordId) { - const ESM::Armor *armor = iter->get()->mBase; - const std::vector& bodyparts = armor->mParts.mParts; - if (!bodyparts.empty()) - mesh = getShieldBodypartMesh(bodyparts, !mNpc->isMale()); + mesh = getShieldMesh(*iter, !mNpc->isMale()); } if (mesh.empty() || addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1, - mesh, !iter->getClass().getEnchantment(*iter).empty(), &glowColor)) + mesh, !iter->getClass().getEnchantment(*iter).empty(), &glowColor, iter->getType() == ESM::Light::sRecordId)) { if (mesh.empty()) reserveIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1); diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index b511a52f37..2dcfac3036 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -83,13 +83,13 @@ private: NpcType getNpcType() const; PartHolderPtr insertBoundedPart(const std::string &model, const std::string &bonename, - const std::string &bonefilter, bool enchantedGlow, osg::Vec4f* glowColor=nullptr); + const std::string &bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight); void removeIndividualPart(ESM::PartReferenceType type); void reserveIndividualPart(ESM::PartReferenceType type, int group, int priority); bool addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, - bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr); + bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr, bool isLight = false); void removePartGroup(int group); void addPartGroup(int group, int priority, const std::vector &parts, bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr); @@ -105,7 +105,7 @@ private: protected: void addControllers() override; bool isArrowAttached() const override; - std::string getShieldMesh(const MWWorld::ConstPtr& shield) const override; + std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const override; public: /** diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index 5c22c3fc8d..4e21d33475 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -428,7 +428,6 @@ namespace MWRender continue; if (std::find(cell->mMovedRefs.begin(), cell->mMovedRefs.end(), ref.mRefNum) != cell->mMovedRefs.end()) continue; - Misc::StringUtils::lowerCaseInPlace(ref.mRefID); int type = store.findStatic(ref.mRefID); if (!typeFilter(type,size>=2)) continue; if (deleted) { refs.erase(ref.mRefNum); continue; } @@ -444,7 +443,6 @@ namespace MWRender for (auto [ref, deleted] : cell->mLeasedRefs) { if (deleted) { refs.erase(ref.mRefNum); continue; } - Misc::StringUtils::lowerCaseInPlace(ref.mRefID); int type = store.findStatic(ref.mRefID); if (!typeFilter(type,size>=2)) continue; refs[ref.mRefNum] = std::move(ref); @@ -617,6 +615,10 @@ namespace MWRender pat->setAttitude(nodeAttitude); } + // DO NOT COPY AND PASTE THIS CODE. Cloning osg::Geometry without also cloning its contained Arrays is generally unsafe. + // In this specific case the operation is safe under the following two assumptions: + // - When Arrays are removed or replaced in the cloned geometry, the original Arrays in their place must outlive the cloned geometry regardless. (ensured by TemplateMultiRef) + // - Arrays that we add or replace in the cloned geometry must be explicitely forbidden from reusing BufferObjects of the original geometry. (ensured by needvbo() in optimizer.cpp) copyop.setCopyFlags(merge ? osg::CopyOp::DEEP_COPY_NODES|osg::CopyOp::DEEP_COPY_DRAWABLES : osg::CopyOp::DEEP_COPY_NODES); copyop.mOptimizeBillboards = (size > 1/4.f); copyop.mNodePath.push_back(trans); @@ -645,7 +647,8 @@ namespace MWRender } if (numinstances > 0) { - // add a ref to the original template, to hint to the cache that it's still being used and should be kept in cache + // add a ref to the original template to help verify the safety of shallow cloning operations + // in addition, we hint to the cache that it's still being used and should be kept in cache templateRefs->addRef(cnode); if (pair.second.mNeedCompile) @@ -730,12 +733,8 @@ namespace MWRender } void clampToCell(osg::Vec3f& cellPos) { - osg::Vec2i min (mCell.x(), mCell.y()); - osg::Vec2i max (mCell.x()+1, mCell.y()+1); - if (cellPos.x() < min.x()) cellPos.x() = min.x(); - if (cellPos.x() > max.x()) cellPos.x() = max.x(); - if (cellPos.y() < min.y()) cellPos.y() = min.y(); - if (cellPos.y() > max.y()) cellPos.y() = max.y(); + cellPos.x() = std::clamp(cellPos.x(), mCell.x(), mCell.x() + 1); + cellPos.y() = std::clamp(cellPos.y(), mCell.y(), mCell.y() + 1); } osg::Vec3f mPosition; osg::Vec2i mCell; diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 1f29c45182..4e2da15107 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -297,7 +297,8 @@ namespace MWRender , mViewDistance(Settings::Manager::getFloat("viewing distance", "Camera")) , mFieldOfViewOverridden(false) , mFieldOfViewOverride(0.f) - , mFieldOfView(std::min(std::max(1.f, Settings::Manager::getFloat("field of view", "Camera")), 179.f)) + , mFieldOfView(std::clamp(Settings::Manager::getFloat("field of view", "Camera"), 1.f, 179.f)) + , mFirstPersonFieldOfView(std::clamp(Settings::Manager::getFloat("first person field of view", "Camera"), 1.f, 179.f)) { bool reverseZ = SceneUtil::getReverseZ(); @@ -517,8 +518,6 @@ namespace MWRender NifOsg::Loader::setIntersectionDisabledNodeMask(Mask_Effect); Nif::NIFFile::setLoadUnsupportedFiles(Settings::Manager::getBool("load unsupported nif files", "Models")); - float firstPersonFov = Settings::Manager::getFloat("first person field of view", "Camera"); - mFirstPersonFieldOfView = std::min(std::max(1.f, firstPersonFov), 179.f); mStateUpdater->setFogEnd(mViewDistance); mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("simpleWater", false)); @@ -753,12 +752,12 @@ namespace MWRender else if (mode == Render_Scene) { unsigned int mask = mViewer->getCamera()->getCullMask(); - bool enabled = mask&Mask_Scene; - enabled = !enabled; + bool enabled = !(mask&sToggleWorldMask); if (enabled) - mask |= Mask_Scene; + mask |= sToggleWorldMask; else - mask &= ~Mask_Scene; + mask &= ~sToggleWorldMask; + mWater->showWorld(enabled); mViewer->getCamera()->setCullMask(mask); return enabled; } @@ -902,15 +901,8 @@ namespace MWRender return false; } - unsigned int maskBackup = mPlayerAnimation->getObjectRoot()->getNodeMask(); - - if (mCamera->isFirstPerson()) - mPlayerAnimation->getObjectRoot()->setNodeMask(0); - mScreenshotManager->screenshot360(image); - mPlayerAnimation->getObjectRoot()->setNodeMask(maskBackup); - return true; } diff --git a/apps/openmw/mwrender/screenshotmanager.cpp b/apps/openmw/mwrender/screenshotmanager.cpp index 5a047a1566..ab7d0d93f0 100644 --- a/apps/openmw/mwrender/screenshotmanager.cpp +++ b/apps/openmw/mwrender/screenshotmanager.cpp @@ -251,14 +251,13 @@ namespace MWRender osg::ref_ptr screenshotCamera(new osg::Camera); osg::ref_ptr quad(new osg::ShapeDrawable(new osg::Box(osg::Vec3(0,0,0), 2.0))); - quad->getOrCreateStateSet()->setRenderBinDetails(100, "RenderBin", osg::StateSet::USE_RENDERBIN_DETAILS); std::map defineMap; Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); osg::ref_ptr fragmentShader(shaderMgr.getShader("s360_fragment.glsl", defineMap,osg::Shader::FRAGMENT)); osg::ref_ptr vertexShader(shaderMgr.getShader("s360_vertex.glsl", defineMap, osg::Shader::VERTEX)); - osg::ref_ptr stateset = new osg::StateSet; + osg::ref_ptr stateset = quad->getOrCreateStateSet(); osg::ref_ptr program(new osg::Program); program->addShader(fragmentShader); @@ -269,9 +268,6 @@ namespace MWRender stateset->addUniform(new osg::Uniform("mapping", screenshotMapping)); stateset->setTextureAttributeAndModes(0, cubeTexture, osg::StateAttribute::ON); - quad->setStateSet(stateset); - quad->setUpdateCallback(nullptr); - screenshotCamera->addChild(quad); renderCameraToImage(screenshotCamera, image, screenshotW, screenshotH); @@ -347,7 +343,7 @@ namespace MWRender rttCamera->addChild(mWater->getReflectionNode()); rttCamera->addChild(mWater->getRefractionNode()); - rttCamera->setCullMask(mViewer->getCamera()->getCullMask() & (~Mask_GUI)); + rttCamera->setCullMask(mViewer->getCamera()->getCullMask() & ~(Mask_GUI|Mask_FirstPerson)); rttCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index ebf901b5ab..b6b0a3669b 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -1,1306 +1,185 @@ #include "sky.hpp" -#include - -#include -#include -#include #include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include #include +#include +#include #include +#include -#include +#include -#include +#include +#include +#include +#include #include #include #include -#include -#include -#include -#include -#include -#include -#include - -#include +#include +#include #include +#include "../mwworld/weather.hpp" + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "vismask.hpp" #include "renderbin.hpp" #include "util.hpp" +#include "skyutil.hpp" namespace { - osg::ref_ptr createAlphaTrackingUnlitMaterial() - { - osg::ref_ptr mat = new osg::Material; - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); - mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); - mat->setColorMode(osg::Material::DIFFUSE); - return mat; - } - - osg::ref_ptr createUnlitMaterial() - { - osg::ref_ptr mat = new osg::Material; - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); - mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); - mat->setColorMode(osg::Material::OFF); - return mat; - } - - osg::ref_ptr createTexturedQuad(int numUvSets=1, float scale=1.f) - { - osg::ref_ptr geom = new osg::Geometry; - - osg::ref_ptr verts = new osg::Vec3Array; - verts->push_back(osg::Vec3f(-0.5*scale, -0.5*scale, 0)); - verts->push_back(osg::Vec3f(-0.5*scale, 0.5*scale, 0)); - verts->push_back(osg::Vec3f(0.5*scale, 0.5*scale, 0)); - verts->push_back(osg::Vec3f(0.5*scale, -0.5*scale, 0)); - - geom->setVertexArray(verts); - - osg::ref_ptr texcoords = new osg::Vec2Array; - texcoords->push_back(osg::Vec2f(0, 0)); - texcoords->push_back(osg::Vec2f(0, 1)); - texcoords->push_back(osg::Vec2f(1, 1)); - texcoords->push_back(osg::Vec2f(1, 0)); - - osg::ref_ptr colors = new osg::Vec4Array; - colors->push_back(osg::Vec4(1.f, 1.f, 1.f, 1.f)); - geom->setColorArray(colors, osg::Array::BIND_OVERALL); - - for (int i=0; isetTexCoordArray(i, texcoords, osg::Array::BIND_PER_VERTEX); - - geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4)); - - return geom; - } - -} - -namespace MWRender -{ - -class AtmosphereUpdater : public SceneUtil::StateSetUpdater -{ -public: - void setEmissionColor(const osg::Vec4f& emissionColor) - { - mEmissionColor = emissionColor; - } - -protected: - void setDefaults(osg::StateSet* stateset) override - { - stateset->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override - { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); - } - -private: - osg::Vec4f mEmissionColor; -}; - -class AtmosphereNightUpdater : public SceneUtil::StateSetUpdater -{ -public: - AtmosphereNightUpdater(Resource::ImageManager* imageManager) - { - // we just need a texture, its contents don't really matter - mTexture = new osg::Texture2D(imageManager->getWarningImage()); - } - - void setFade(const float fade) - { - mColor.a() = fade; - } - -protected: - void setDefaults(osg::StateSet* stateset) override - { - osg::ref_ptr texEnv (new osg::TexEnvCombine); - texEnv->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnv->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnv->setCombine_RGB(osg::TexEnvCombine::REPLACE); - texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - - stateset->setTextureAttributeAndModes(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureAttributeAndModes(1, texEnv, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override - { - osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnv->setConstantColor(mColor); - } - - osg::ref_ptr mTexture; - - osg::Vec4f mColor; -}; - -class CloudUpdater : public SceneUtil::StateSetUpdater -{ -public: - CloudUpdater() - : mAnimationTimer(0.f) - , mOpacity(0.f) - { - } - - void setAnimationTimer(float timer) - { - mAnimationTimer = timer; - } - - void setTexture(osg::ref_ptr texture) - { - mTexture = texture; - } - void setEmissionColor(const osg::Vec4f& emissionColor) - { - mEmissionColor = emissionColor; - } - void setOpacity(float opacity) - { - mOpacity = opacity; - } - -protected: - void setDefaults(osg::StateSet *stateset) override - { - osg::ref_ptr texmat (new osg::TexMat); - stateset->setTextureAttributeAndModes(0, texmat, osg::StateAttribute::ON); - stateset->setTextureAttributeAndModes(1, texmat, osg::StateAttribute::ON); - stateset->setAttribute(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - - // need to set opacity on a separate texture unit, diffuse alpha is used by the vertex colors already - osg::ref_ptr texEnvCombine (new osg::TexEnvCombine); - texEnvCombine->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - texEnvCombine->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnvCombine->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,1)); - texEnvCombine->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnvCombine->setCombine_RGB(osg::TexEnvCombine::REPLACE); - - stateset->setTextureAttributeAndModes(1, texEnvCombine, osg::StateAttribute::ON); - - stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override - { - osg::TexMat* texMat = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXMAT)); - texMat->setMatrix(osg::Matrix::translate(osg::Vec3f(0, -mAnimationTimer, 0.f))); - - stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureAttribute(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); - - osg::TexEnvCombine* texEnvCombine = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,mOpacity)); - } - -private: - float mAnimationTimer; - osg::ref_ptr mTexture; - osg::Vec4f mEmissionColor; - float mOpacity; -}; - -/// Transform that removes the eyepoint of the modelview matrix, -/// i.e. its children are positioned relative to the camera. -class CameraRelativeTransform : public osg::Transform -{ -public: - CameraRelativeTransform() - { - // Culling works in node-local space, not in camera space, so we can't cull this node correctly - // That's not a problem though, children of this node can be culled just fine - // Just make sure you do not place a CameraRelativeTransform deep in the scene graph - setCullingActive(false); - - addCullCallback(new CullCallback); - } - - CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop) - : osg::Transform(copy, copyop) - { - } - - META_Node(MWRender, CameraRelativeTransform) - - const osg::Vec3f& getLastViewPoint() const - { - return mViewPoint; - } - - bool computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const override - { - if (nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR) - { - mViewPoint = static_cast(nv)->getViewPoint(); - } - - if (_referenceFrame==RELATIVE_RF) - { - matrix.setTrans(osg::Vec3f(0.f,0.f,0.f)); - return false; - } - else // absolute - { - matrix.makeIdentity(); - return true; - } - } - - osg::BoundingSphere computeBound() const override - { - return osg::BoundingSphere(); - } - - class CullCallback : public SceneUtil::NodeCallback + class WrapAroundOperator : public osgParticle::Operator { public: - void operator() (osg::Node* node, osgUtil::CullVisitor* cv) + WrapAroundOperator(osg::Camera *camera, const osg::Vec3 &wrapRange) + : osgParticle::Operator() + , mCamera(camera) + , mWrapRange(wrapRange) + , mHalfWrapRange(mWrapRange / 2.0) { - // XXX have to remove unwanted culling plane of the water reflection camera + mPreviousCameraPosition = getCameraPosition(); + } - // Remove all planes that aren't from the standard frustum - unsigned int numPlanes = 4; - if (cv->getCullingMode() & osg::CullSettings::NEAR_PLANE_CULLING) - ++numPlanes; - if (cv->getCullingMode() & osg::CullSettings::FAR_PLANE_CULLING) - ++numPlanes; + osg::Object *cloneType() const override + { + return nullptr; + } - unsigned int mask = 0x1; - unsigned int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); - for (unsigned int i=0; igetProjectionCullingStack().back().getFrustum().getPlaneList().size(); ++i) + osg::Object *clone(const osg::CopyOp &op) const override + { + return nullptr; + } + + void operate(osgParticle::Particle *P, double dt) override + { + } + + void operateParticles(osgParticle::ParticleSystem *ps, double dt) override + { + osg::Vec3 position = getCameraPosition(); + osg::Vec3 positionDifference = position - mPreviousCameraPosition; + + osg::Matrix toWorld, toLocal; + + std::vector worldMatrices = ps->getWorldMatrices(); + + + if (!worldMatrices.empty()) { - if (i >= numPlanes) + toWorld = worldMatrices[0]; + toLocal.invert(toWorld); + } + + for (int i = 0; i < ps->numParticles(); ++i) + { + osgParticle::Particle *p = ps->getParticle(i); + p->setPosition(toWorld.preMult(p->getPosition())); + p->setPosition(p->getPosition() - positionDifference); + + for (int j = 0; j < 3; ++j) // wrap-around in all 3 dimensions { - // turn off this culling plane - resultMask &= (~mask); + osg::Vec3 pos = p->getPosition(); + + if (pos[j] < -mHalfWrapRange[j]) + pos[j] = mHalfWrapRange[j] + fmod(pos[j] - mHalfWrapRange[j],mWrapRange[j]); + else if (pos[j] > mHalfWrapRange[j]) + pos[j] = fmod(pos[j] + mHalfWrapRange[j],mWrapRange[j]) - mHalfWrapRange[j]; + + p->setPosition(pos); } - mask <<= 1; + p->setPosition(toLocal.preMult(p->getPosition())); } - cv->getProjectionCullingStack().back().getFrustum().setResultMask(resultMask); - cv->getCurrentCullingSet().getFrustum().setResultMask(resultMask); - - cv->getProjectionCullingStack().back().pushCurrentMask(); - cv->getCurrentCullingSet().pushCurrentMask(); - - traverse(node, cv); - - cv->getProjectionCullingStack().back().popCurrentMask(); - cv->getCurrentCullingSet().popCurrentMask(); - } - }; -private: - // viewPoint for the current frame - mutable osg::Vec3f mViewPoint; -}; - -class ModVertexAlphaVisitor : public osg::NodeVisitor -{ -public: - ModVertexAlphaVisitor(int meshType) - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mMeshType(meshType) - { - } - - void apply(osg::Drawable& drw) override - { - osg::Geometry* geom = drw.asGeometry(); - if (!geom) - return; - - osg::ref_ptr colors = new osg::Vec4Array(geom->getVertexArray()->getNumElements()); - for (unsigned int i=0; isize(); ++i) - { - float alpha = 1.f; - if (mMeshType == 0) alpha = (i%2) ? 0.f : 1.f; // this is a cylinder, so every second vertex belongs to the bottom-most row - else if (mMeshType == 1) - { - if (i>= 49 && i <= 64) alpha = 0.f; // bottom-most row - else if (i>= 33 && i <= 48) alpha = 0.25098; // second row - else alpha = 1.f; - } - else if (mMeshType == 2) - { - if (geom->getColorArray()) - { - osg::Vec4Array* origColors = static_cast(geom->getColorArray()); - alpha = ((*origColors)[i].x() == 1.f) ? 1.f : 0.f; - } - else - alpha = 1.f; - } - - (*colors)[i] = osg::Vec4f(0.f, 0.f, 0.f, alpha); - } - - geom->setColorArray(colors, osg::Array::BIND_PER_VERTEX); - } - -private: - int mMeshType; -}; - -/// @brief Hides the node subgraph if the eye point is below water. -/// @note Must be added as cull callback. -/// @note Meant to be used on a node that is child of a CameraRelativeTransform. -/// The current view point must be retrieved by the CameraRelativeTransform since we can't get it anymore once we are in camera-relative space. -class UnderwaterSwitchCallback : public SceneUtil::NodeCallback -{ -public: - UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform) - : mCameraRelativeTransform(cameraRelativeTransform) - , mEnabled(true) - , mWaterLevel(0.f) - { - } - - bool isUnderwater() - { - osg::Vec3f viewPoint = mCameraRelativeTransform->getLastViewPoint(); - return mEnabled && viewPoint.z() < mWaterLevel; - } - - void operator()(osg::Node* node, osg::NodeVisitor* nv) - { - if (isUnderwater()) - return; - - traverse(node, nv); - } - - void setEnabled(bool enabled) - { - mEnabled = enabled; - } - void setWaterLevel(float waterLevel) - { - mWaterLevel = waterLevel; - } - -private: - osg::ref_ptr mCameraRelativeTransform; - bool mEnabled; - float mWaterLevel; -}; - -/// A base class for the sun and moons. -class CelestialBody -{ -public: - CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask=~0u) - : mVisibleMask(visibleMask) - { - mGeom = createTexturedQuad(numUvSets); - mTransform = new osg::PositionAttitudeTransform; - mTransform->setNodeMask(mVisibleMask); - mTransform->setScale(osg::Vec3f(450,450,450) * scaleFactor); - mTransform->addChild(mGeom); - - parentNode->addChild(mTransform); - } - - virtual ~CelestialBody() {} - - virtual void adjustTransparency(const float ratio) = 0; - - void setVisible(bool visible) - { - mTransform->setNodeMask(visible ? mVisibleMask : 0); - } - -protected: - unsigned int mVisibleMask; - static const float mDistance; - osg::ref_ptr mTransform; - osg::ref_ptr mGeom; -}; - -const float CelestialBody::mDistance = 1000.0f; - -class Sun : public CelestialBody -{ -public: - Sun(osg::Group* parentNode, Resource::ImageManager& imageManager) - : CelestialBody(parentNode, 1.0f, 1, Mask_Sun) - , mUpdater(new Updater) - { - mTransform->addUpdateCallback(mUpdater); - - osg::ref_ptr sunTex (new osg::Texture2D(imageManager.getImage("textures/tx_sun_05.dds"))); - sunTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - sunTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - mGeom->getOrCreateStateSet()->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); - - osg::ref_ptr queryNode (new osg::Group); - // Need to render after the world geometry so we can correctly test for occlusions - osg::StateSet* stateset = queryNode->getOrCreateStateSet(); - stateset->setRenderBinDetails(RenderBin_OcclusionQuery, "RenderBin"); - stateset->setNestRenderBins(false); - // Set up alpha testing on the occlusion testing subgraph, that way we can get the occlusion tested fragments to match the circular shape of the sun - osg::ref_ptr alphaFunc (new osg::AlphaFunc); - alphaFunc->setFunction(osg::AlphaFunc::GREATER, 0.8); - stateset->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); - stateset->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON); - // Disable writing to the color buffer. We are using this geometry for visibility tests only. - osg::ref_ptr colormask (new osg::ColorMask(0, 0, 0, 0)); - stateset->setAttributeAndModes(colormask, osg::StateAttribute::ON); - - mTransform->addChild(queryNode); - - mOcclusionQueryVisiblePixels = createOcclusionQueryNode(queryNode, true); - mOcclusionQueryTotalPixels = createOcclusionQueryNode(queryNode, false); - - createSunFlash(imageManager); - createSunGlare(); - } - - ~Sun() - { - mTransform->removeUpdateCallback(mUpdater); - destroySunFlash(); - destroySunGlare(); - } - - void setColor(const osg::Vec4f& color) - { - mUpdater->mColor.r() = color.r(); - mUpdater->mColor.g() = color.g(); - mUpdater->mColor.b() = color.b(); - } - - void adjustTransparency(const float ratio) override - { - mUpdater->mColor.a() = ratio; - if (mSunGlareCallback) - mSunGlareCallback->setGlareView(ratio); - if (mSunFlashCallback) - mSunFlashCallback->setGlareView(ratio); - } - - void setDirection(const osg::Vec3f& direction) - { - osg::Vec3f normalizedDirection = direction / direction.length(); - mTransform->setPosition(normalizedDirection * mDistance); - - osg::Quat quat; - quat.makeRotate(osg::Vec3f(0.0f, 0.0f, 1.0f), normalizedDirection); - mTransform->setAttitude(quat); - } - - void setGlareTimeOfDayFade(float val) - { - if (mSunGlareCallback) - mSunGlareCallback->setTimeOfDayFade(val); - } - -private: - class DummyComputeBoundCallback : public osg::Node::ComputeBoundingSphereCallback - { - public: - osg::BoundingSphere computeBound(const osg::Node& node) const override { return osg::BoundingSphere(); } - }; - - /// @param queryVisible If true, queries the amount of visible pixels. If false, queries the total amount of pixels. - osg::ref_ptr createOcclusionQueryNode(osg::Group* parent, bool queryVisible) - { - osg::ref_ptr oqn = new osg::OcclusionQueryNode; - oqn->setQueriesEnabled(true); - -#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) - // With OSG 3.6.5, the method of providing user defined query geometry has been completely replaced - osg::ref_ptr queryGeom = new osg::QueryGeometry(oqn->getName()); -#else - osg::ref_ptr queryGeom = oqn->getQueryGeometry(); -#endif - - // Make it fast! A DYNAMIC query geometry means we can't break frame until the flare is rendered (which is rendered after all the other geometry, - // so that would be pretty bad). STATIC should be safe, since our node's local bounds are static, thus computeBounds() which modifies the queryGeometry - // is only called once. - // Note the debug geometry setDebugDisplay(true) is always DYNAMIC and that can't be changed, not a big deal. - queryGeom->setDataVariance(osg::Object::STATIC); - - // Set up the query geometry to match the actual sun's rendering shape. osg::OcclusionQueryNode wasn't originally intended to allow this, - // normally it would automatically adjust the query geometry to match the sub graph's bounding box. The below hack is needed to - // circumvent this. - queryGeom->setVertexArray(mGeom->getVertexArray()); - queryGeom->setTexCoordArray(0, mGeom->getTexCoordArray(0), osg::Array::BIND_PER_VERTEX); - queryGeom->removePrimitiveSet(0, queryGeom->getNumPrimitiveSets()); - queryGeom->addPrimitiveSet(mGeom->getPrimitiveSet(0)); - - // Hack to disable unwanted awful code inside OcclusionQueryNode::computeBound. - oqn->setComputeBoundingSphereCallback(new DummyComputeBoundCallback); - // Still need a proper bounding sphere. - oqn->setInitialBound(queryGeom->getBound()); - -#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) - oqn->setQueryGeometry(queryGeom.release()); -#endif - - osg::StateSet* queryStateSet = new osg::StateSet; - if (queryVisible) - { - auto depth = SceneUtil::createDepth(); - // This is a trick to make fragments written by the query always use the maximum depth value, - // without having to retrieve the current far clipping distance. - // We want the sun glare to be "infinitely" far away. - double far = SceneUtil::getReverseZ() ? 0.0 : 1.0; - depth->setZNear(far); - depth->setZFar(far); - depth->setWriteMask(false); - queryStateSet->setAttributeAndModes(depth, osg::StateAttribute::ON); - } - else - { - queryStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - } - oqn->setQueryStateSet(queryStateSet); - - parent->addChild(oqn); - - return oqn; - } - - void createSunFlash(Resource::ImageManager& imageManager) - { - osg::ref_ptr tex (new osg::Texture2D(imageManager.getImage("textures/tx_sun_flash_grey_05.dds"))); - tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - osg::ref_ptr group (new osg::Group); - - mTransform->addChild(group); - - const float scale = 2.6f; - osg::ref_ptr geom = createTexturedQuad(1, scale); - group->addChild(geom); - - osg::StateSet* stateset = geom->getOrCreateStateSet(); - - stateset->setTextureAttributeAndModes(0, tex, osg::StateAttribute::ON); - stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); - stateset->setNestRenderBins(false); - - mSunFlashNode = group; - - mSunFlashCallback = new SunFlashCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels); - mSunFlashNode->addCullCallback(mSunFlashCallback); - } - void destroySunFlash() - { - if (mSunFlashNode) - { - mSunFlashNode->removeCullCallback(mSunFlashCallback); - mSunFlashCallback = nullptr; - } - } - - void createSunGlare() - { - osg::ref_ptr camera (new osg::Camera); - camera->setProjectionMatrix(osg::Matrix::identity()); - camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); // add to skyRoot instead? - camera->setViewMatrix(osg::Matrix::identity()); - camera->setClearMask(0); - camera->setRenderOrder(osg::Camera::NESTED_RENDER); - camera->setAllowEventFocus(false); - - osg::ref_ptr geom = osg::createTexturedQuadGeometry(osg::Vec3f(-1,-1,0), osg::Vec3f(2,0,0), osg::Vec3f(0,2,0)); - - camera->addChild(geom); - - osg::StateSet* stateset = geom->getOrCreateStateSet(); - - stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); - stateset->setNestRenderBins(false); - stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - - // set up additive blending - osg::ref_ptr blendFunc (new osg::BlendFunc); - blendFunc->setSource(osg::BlendFunc::SRC_ALPHA); - blendFunc->setDestination(osg::BlendFunc::ONE); - stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); - - mSunGlareCallback = new SunGlareCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels, mTransform); - mSunGlareNode = camera; - - mSunGlareNode->addCullCallback(mSunGlareCallback); - - mTransform->addChild(camera); - } - void destroySunGlare() - { - if (mSunGlareNode) - { - mSunGlareNode->removeCullCallback(mSunGlareCallback); - mSunGlareCallback = nullptr; - } - } - - class Updater : public SceneUtil::StateSetUpdater - { - public: - osg::Vec4f mColor; - - Updater() - : mColor(1.f, 1.f, 1.f, 1.f) - { - } - - void setDefaults(osg::StateSet* stateset) override - { - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor*) override - { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mColor.a())); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(mColor.r(), mColor.g(), mColor.b(), 1)); - } - }; - - class OcclusionCallback - { - public: - OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) - : mOcclusionQueryVisiblePixels(oqnVisible) - , mOcclusionQueryTotalPixels(oqnTotal) - { + mPreviousCameraPosition = position; } protected: - float getVisibleRatio (osg::Camera* camera) + osg::Camera *mCamera; + osg::Vec3 mPreviousCameraPosition; + osg::Vec3 mWrapRange; + osg::Vec3 mHalfWrapRange; + + osg::Vec3 getCameraPosition() { - int visible = mOcclusionQueryVisiblePixels->getQueryGeometry()->getNumPixels(camera); - int total = mOcclusionQueryTotalPixels->getQueryGeometry()->getNumPixels(camera); - - float visibleRatio = 0.f; - if (total > 0) - visibleRatio = static_cast(visible) / static_cast(total); - - float dt = MWBase::Environment::get().getFrameDuration(); - - float lastRatio = mLastRatio[osg::observer_ptr(camera)]; - - float change = dt*10; - - if (visibleRatio > lastRatio) - visibleRatio = std::min(visibleRatio, lastRatio + change); - else - visibleRatio = std::max(visibleRatio, lastRatio - change); - - mLastRatio[osg::observer_ptr(camera)] = visibleRatio; - - return visibleRatio; + return mCamera->getInverseViewMatrix().getTrans(); } - - private: - osg::ref_ptr mOcclusionQueryVisiblePixels; - osg::ref_ptr mOcclusionQueryTotalPixels; - - std::map, float> mLastRatio; }; - /// SunFlashCallback handles fading/scaling of a node depending on occlusion query result. Must be attached as a cull callback. - class SunFlashCallback : public OcclusionCallback, public SceneUtil::NodeCallback + class WeatherAlphaOperator : public osgParticle::Operator { public: - SunFlashCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) - : OcclusionCallback(oqnVisible, oqnTotal) - , mGlareView(1.f) + WeatherAlphaOperator(float& alpha, bool rain) + : mAlpha(alpha) + , mIsRain(rain) + { } + + osg::Object *cloneType() const override { + return nullptr; } - void operator()(osg::Node* node, osgUtil::CullVisitor* cv) + osg::Object *clone(const osg::CopyOp &op) const override { - float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); - - osg::ref_ptr stateset; - - if (visibleRatio > 0.f) - { - const float fadeThreshold = 0.1; - if (visibleRatio < fadeThreshold) - { - float fade = 1.f - (fadeThreshold - visibleRatio) / fadeThreshold; - osg::ref_ptr mat (createUnlitMaterial()); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade*mGlareView)); - stateset = new osg::StateSet; - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - else if (visibleRatio < 1.f) - { - const float threshold = 0.6; - visibleRatio = visibleRatio * (1.f - threshold) + threshold; - } - } - - float scale = visibleRatio; - - if (scale == 0.f) - { - // no traverse - return; - } - else if (scale == 1.f) - traverse(node, cv); - else - { - osg::Matrix modelView = *cv->getModelViewMatrix(); - - modelView.preMultScale(osg::Vec3f(scale, scale, scale)); - - if (stateset) - cv->pushStateSet(stateset); - - cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); - - traverse(node, cv); - - cv->popModelViewMatrix(); - - if (stateset) - cv->popStateSet(); - } + return nullptr; } - void setGlareView(float value) + void operate(osgParticle::Particle *particle, double dt) override { - mGlareView = value; + constexpr float rainThreshold = 0.6f; // Rain_Threshold? + float alpha = mIsRain ? mAlpha * rainThreshold : mAlpha; + particle->setAlphaRange(osgParticle::rangef(alpha, alpha)); } private: - float mGlareView; + float &mAlpha; + bool mIsRain; }; - - /// SunGlareCallback controls a full-screen glare effect depending on occlusion query result and the angle between sun and camera. - /// Must be attached as a cull callback to the node above the glare node. - class SunGlareCallback : public OcclusionCallback, public SceneUtil::NodeCallback + // Updater for alpha value on a node's StateSet. Assumes the node has an existing Material StateAttribute. + class AlphaFader : public SceneUtil::StateSetUpdater { public: - SunGlareCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal, - osg::ref_ptr sunTransform) - : OcclusionCallback(oqnVisible, oqnTotal) - , mSunTransform(sunTransform) - , mTimeOfDayFade(1.f) - , mGlareView(1.f) - { - mColor = Fallback::Map::getColour("Weather_Sun_Glare_Fader_Color"); - mSunGlareFaderMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Max"); - mSunGlareFaderAngleMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Angle_Max"); - - // Replicating a design flaw in MW. The color was being set on both ambient and emissive properties, which multiplies the result by two, - // then finally gets clamped by the fixed function pipeline. With the default INI settings, only the red component gets clamped, - // so the resulting color looks more orange than red. - mColor *= 2; - for (int i=0; i<3; ++i) - mColor[i] = std::min(1.f, mColor[i]); - } - - void operator ()(osg::Node* node, osgUtil::CullVisitor* cv) - { - float angleRadians = getAngleToSunInRadians(*cv->getCurrentRenderStage()->getInitialViewMatrix()); - float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); - - const float angleMaxRadians = osg::DegreesToRadians(mSunGlareFaderAngleMax); - - float value = 1.f - std::min(1.f, angleRadians / angleMaxRadians); - float fade = value * mSunGlareFaderMax; - - fade *= mTimeOfDayFade * mGlareView * visibleRatio; - - if (fade == 0.f) - { - // no traverse - return; - } - else - { - osg::ref_ptr stateset (new osg::StateSet); - - osg::ref_ptr mat (createUnlitMaterial()); - - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mColor); - - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); - - cv->pushStateSet(stateset); - traverse(node, cv); - cv->popStateSet(); - } - } - - void setTimeOfDayFade(float val) - { - mTimeOfDayFade = val; - } - - void setGlareView(float glareView) - { - mGlareView = glareView; - } - - private: - float getAngleToSunInRadians(const osg::Matrix& viewMatrix) const - { - osg::Vec3d eye, center, up; - viewMatrix.getLookAt(eye, center, up); - - osg::Vec3d forward = center - eye; - osg::Vec3d sun = mSunTransform->getPosition(); - - forward.normalize(); - sun.normalize(); - float angleRadians = std::acos(forward * sun); - return angleRadians; - } - - osg::ref_ptr mSunTransform; - float mTimeOfDayFade; - float mGlareView; - osg::Vec4f mColor; - float mSunGlareFaderMax; - float mSunGlareFaderAngleMax; - }; - - osg::ref_ptr mUpdater; - osg::ref_ptr mSunFlashCallback; - osg::ref_ptr mSunFlashNode; - osg::ref_ptr mSunGlareCallback; - osg::ref_ptr mSunGlareNode; - osg::ref_ptr mOcclusionQueryVisiblePixels; - osg::ref_ptr mOcclusionQueryTotalPixels; -}; - -class Moon : public CelestialBody -{ -public: - enum Type - { - Type_Masser = 0, - Type_Secunda - }; - - Moon(osg::Group* parentNode, Resource::ImageManager& imageManager, float scaleFactor, Type type) - : CelestialBody(parentNode, scaleFactor, 2) - , mType(type) - , mPhase(MoonState::Phase::Unspecified) - , mUpdater(new Updater(imageManager)) - { - setPhase(MoonState::Phase::Full); - setVisible(true); - - mGeom->addUpdateCallback(mUpdater); - } - - ~Moon() - { - mGeom->removeUpdateCallback(mUpdater); - } - - void adjustTransparency(const float ratio) override - { - mUpdater->mTransparency *= ratio; - } - - void setState(const MoonState& state) - { - float radsX = ((state.mRotationFromHorizon) * static_cast(osg::PI)) / 180.0f; - float radsZ = ((state.mRotationFromNorth) * static_cast(osg::PI)) / 180.0f; - - osg::Quat rotX(radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); - osg::Quat rotZ(radsZ, osg::Vec3f(0.0f, 0.0f, 1.0f)); - - osg::Vec3f direction = rotX * rotZ * osg::Vec3f(0.0f, 1.0f, 0.0f); - mTransform->setPosition(direction * mDistance); - - // The moon quad is initially oriented facing down, so we need to offset its X-axis - // rotation to rotate it to face the camera when sitting at the horizon. - osg::Quat attX((-static_cast(osg::PI) / 2.0f) + radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); - mTransform->setAttitude(attX * rotZ); - - setPhase(state.mPhase); - mUpdater->mTransparency = state.mMoonAlpha; - mUpdater->mShadowBlend = state.mShadowBlend; - } - - void setAtmosphereColor(const osg::Vec4f& color) - { - mUpdater->mAtmosphereColor = color; - } - - void setColor(const osg::Vec4f& color) - { - mUpdater->mMoonColor = color; - } - - unsigned int getPhaseInt() const - { - if (mPhase == MoonState::Phase::New) return 0; - else if (mPhase == MoonState::Phase::WaxingCrescent) return 1; - else if (mPhase == MoonState::Phase::WaningCrescent) return 1; - else if (mPhase == MoonState::Phase::FirstQuarter) return 2; - else if (mPhase == MoonState::Phase::ThirdQuarter) return 2; - else if (mPhase == MoonState::Phase::WaxingGibbous) return 3; - else if (mPhase == MoonState::Phase::WaningGibbous) return 3; - else if (mPhase == MoonState::Phase::Full) return 4; - return 0; - } - -private: - struct Updater : public SceneUtil::StateSetUpdater - { - Resource::ImageManager& mImageManager; - osg::ref_ptr mPhaseTex; - osg::ref_ptr mCircleTex; - float mTransparency; - float mShadowBlend; - osg::Vec4f mAtmosphereColor; - osg::Vec4f mMoonColor; - - Updater(Resource::ImageManager& imageManager) - : mImageManager(imageManager) - , mPhaseTex() - , mCircleTex() - , mTransparency(1.0f) - , mShadowBlend(1.0f) - , mAtmosphereColor(1.0f, 1.0f, 1.0f, 1.0f) - , mMoonColor(1.0f, 1.0f, 1.0f, 1.0f) - { - } + /// @param alpha the variable alpha value is recovered from + AlphaFader(const float& alpha) + : mAlpha(alpha) + { } void setDefaults(osg::StateSet* stateset) override { - stateset->setTextureAttributeAndModes(0, mPhaseTex, osg::StateAttribute::ON); - osg::ref_ptr texEnv = new osg::TexEnvCombine; - texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); - texEnv->setSource0_RGB(osg::TexEnvCombine::CONSTANT); - texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); - texEnv->setConstantColor(osg::Vec4f(1.f, 0.f, 0.f, 1.f)); // mShadowBlend * mMoonColor - stateset->setTextureAttributeAndModes(0, texEnv, osg::StateAttribute::ON); - - stateset->setTextureAttributeAndModes(1, mCircleTex, osg::StateAttribute::ON); - osg::ref_ptr texEnv2 = new osg::TexEnvCombine; - texEnv2->setCombine_RGB(osg::TexEnvCombine::ADD); - texEnv2->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnv2->setSource0_Alpha(osg::TexEnvCombine::TEXTURE); - texEnv2->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnv2->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - texEnv2->setSource1_RGB(osg::TexEnvCombine::CONSTANT); - texEnv2->setConstantColor(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); // mAtmosphereColor.rgb, mTransparency - stateset->setTextureAttributeAndModes(1, texEnv2, osg::StateAttribute::ON); - - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + // need to create a deep copy of StateAttributes we will modify + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + stateset->setAttribute(osg::clone(mat, osg::CopyOp::DEEP_COPY_ALL), osg::StateAttribute::ON); } - void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override { - osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXENV)); - texEnv->setConstantColor(mMoonColor * mShadowBlend); - - osg::TexEnvCombine* texEnv2 = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnv2->setConstantColor(osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, mAlpha)); } - void setTextures(const std::string& phaseTex, const std::string& circleTex) - { - mPhaseTex = new osg::Texture2D(mImageManager.getImage(phaseTex)); - mPhaseTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mPhaseTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - mCircleTex = new osg::Texture2D(mImageManager.getImage(circleTex)); - mCircleTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mCircleTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - reset(); - } + protected: + const float &mAlpha; }; - Type mType; - MoonState::Phase mPhase; - osg::ref_ptr mUpdater; - - void setPhase(const MoonState::Phase& phase) - { - if(mPhase == phase) - return; - - mPhase = phase; - - std::string textureName = "textures/tx_"; - - if (mType == Moon::Type_Secunda) - textureName += "secunda_"; - else - textureName += "masser_"; - - if (phase == MoonState::Phase::New) textureName += "new"; - else if(phase == MoonState::Phase::WaxingCrescent) textureName += "one_wax"; - else if(phase == MoonState::Phase::FirstQuarter) textureName += "half_wax"; - else if(phase == MoonState::Phase::WaxingGibbous) textureName += "three_wax"; - else if(phase == MoonState::Phase::WaningCrescent) textureName += "one_wan"; - else if(phase == MoonState::Phase::ThirdQuarter) textureName += "half_wan"; - else if(phase == MoonState::Phase::WaningGibbous) textureName += "three_wan"; - else if(phase == MoonState::Phase::Full) textureName += "full"; - - textureName += ".dds"; - - if (mType == Moon::Type_Secunda) - mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_s.dds"); - else - mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_m.dds"); - } -}; - -SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager) - : mSceneManager(sceneManager) - , mCamera(nullptr) - , mAtmosphereNightRoll(0.f) - , mCreated(false) - , mIsStorm(false) - , mDay(0) - , mMonth(0) - , mCloudAnimationTimer(0.f) - , mRainTimer(0.f) - , mStormDirection(0,1,0) - , mClouds() - , mNextClouds() - , mCloudBlendFactor(0.0f) - , mCloudSpeed(0.0f) - , mStarsOpacity(0.0f) - , mRemainingTransitionTime(0.0f) - , mRainEnabled(false) - , mRainSpeed(0) - , mRainDiameter(0) - , mRainMinHeight(0) - , mRainMaxHeight(0) - , mRainEntranceSpeed(1) - , mRainMaxRaindrops(0) - , mWindSpeed(0.f) - , mBaseWindSpeed(0.f) - , mEnabled(true) - , mSunEnabled(true) - , mPrecipitationAlpha(0.f) -{ - osg::ref_ptr skyroot (new CameraRelativeTransform); - skyroot->setName("Sky Root"); - // Assign empty program to specify we don't want shaders - // The shaders generated by the SceneManager can't handle everything we need - skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), osg::StateAttribute::OVERRIDE|osg::StateAttribute::PROTECTED|osg::StateAttribute::ON); - SceneUtil::ShadowManager::disableShadowsForStateSet(skyroot->getOrCreateStateSet()); - - skyroot->setNodeMask(Mask_Sky); - parentNode->addChild(skyroot); - - mRootNode = skyroot; - - mEarlyRenderBinRoot = new osg::Group; - // render before the world is rendered - mEarlyRenderBinRoot->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Sky, "RenderBin"); - // Prevent unwanted clipping by water reflection camera's clipping plane - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_CLIP_PLANE0, osg::StateAttribute::OFF); - mRootNode->addChild(mEarlyRenderBinRoot); - - mUnderwaterSwitch = new UnderwaterSwitchCallback(skyroot); -} - -void SkyManager::create() -{ - assert(!mCreated); - - mAtmosphereDay = mSceneManager->getInstance(Settings::Manager::getString("skyatmosphere", "Models"), mEarlyRenderBinRoot); - ModVertexAlphaVisitor modAtmosphere(0); - mAtmosphereDay->accept(modAtmosphere); - - mAtmosphereUpdater = new AtmosphereUpdater; - mAtmosphereDay->addUpdateCallback(mAtmosphereUpdater); - - mAtmosphereNightNode = new osg::PositionAttitudeTransform; - mAtmosphereNightNode->setNodeMask(0); - mEarlyRenderBinRoot->addChild(mAtmosphereNightNode); - - osg::ref_ptr atmosphereNight; - if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) - atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight02", "Models"), mAtmosphereNightNode); - else - atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight01", "Models"), mAtmosphereNightNode); - atmosphereNight->getOrCreateStateSet()->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - ModVertexAlphaVisitor modStars(2); - atmosphereNight->accept(modStars); - mAtmosphereNightUpdater = new AtmosphereNightUpdater(mSceneManager->getImageManager()); - atmosphereNight->addUpdateCallback(mAtmosphereNightUpdater); - - mSun.reset(new Sun(mEarlyRenderBinRoot, *mSceneManager->getImageManager())); - - mMasser.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager->getImageManager(), Fallback::Map::getFloat("Moons_Masser_Size")/125, Moon::Type_Masser)); - mSecunda.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager->getImageManager(), Fallback::Map::getFloat("Moons_Secunda_Size")/125, Moon::Type_Secunda)); - - mCloudNode = new osg::PositionAttitudeTransform; - mEarlyRenderBinRoot->addChild(mCloudNode); - mCloudMesh = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mCloudNode); - ModVertexAlphaVisitor modClouds(1); - mCloudMesh->accept(modClouds); - mCloudUpdater = new CloudUpdater; - mCloudUpdater->setOpacity(1.f); - mCloudMesh->addUpdateCallback(mCloudUpdater); - - mCloudMesh2 = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mCloudNode); - mCloudMesh2->accept(modClouds); - mCloudUpdater2 = new CloudUpdater; - mCloudUpdater2->setOpacity(0.f); - mCloudMesh2->addUpdateCallback(mCloudUpdater2); - mCloudMesh2->setNodeMask(0); - - auto depth = SceneUtil::createDepth(); - depth->setWriteMask(false); - mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth, osg::StateAttribute::ON); - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_FOG, osg::StateAttribute::OFF); - - mMoonScriptColor = Fallback::Map::getColour("Moons_Script_Color"); - - mCreated = true; -} - -class RainCounter : public osgParticle::ConstantRateCounter -{ -public: - int numParticlesToCreate(double dt) const override - { - // limit dt to avoid large particle emissions if there are jumps in the simulation time - // 0.2 seconds is the same cap as used in Engine's frame loop - dt = std::min(dt, 0.2); - return ConstantRateCounter::numParticlesToCreate(dt); - } -}; - -class RainShooter : public osgParticle::Shooter -{ -public: - RainShooter() - : mAngle(0.f) - { - } - - void shoot(osgParticle::Particle* particle) const override - { - particle->setVelocity(mVelocity); - particle->setAngle(osg::Vec3f(-mAngle, 0, (Misc::Rng::rollProbability() * 2 - 1) * osg::PI)); - } - - void setVelocity(const osg::Vec3f& velocity) - { - mVelocity = velocity; - } - - void setAngle(float angle) - { - mAngle = angle; - } - - osg::Object* cloneType() const override - { - return new RainShooter; - } - osg::Object* clone(const osg::CopyOp &) const override - { - return new RainShooter(*this); - } - -private: - osg::Vec3f mVelocity; - float mAngle; -}; - -// Updater for alpha value on a node's StateSet. Assumes the node has an existing Material StateAttribute. -class AlphaFader : public SceneUtil::StateSetUpdater -{ -public: - /// @param alpha the variable alpha value is recovered from - AlphaFader(float& alpha) - : mAlpha(alpha) - { - } - - void setDefaults(osg::StateSet* stateset) override - { - // need to create a deep copy of StateAttributes we will modify - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - stateset->setAttribute(osg::clone(mat, osg::CopyOp::DEEP_COPY_ALL), osg::StateAttribute::ON); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override - { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mAlpha)); - } - // Helper for adding AlphaFaders to a subgraph class SetupVisitor : public osg::NodeVisitor { public: - SetupVisitor(float &alpha) + SetupVisitor(const float &alpha) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mAlpha(alpha) - { - } + { } void apply(osg::Node &node) override { @@ -1320,7 +199,7 @@ public: callback = callback->getNestedCallback(); } - osg::ref_ptr alphaFader (new AlphaFader(mAlpha)); + osg::ref_ptr alphaFader = new AlphaFader(mAlpha); if (composite) composite->addController(alphaFader); @@ -1333,608 +212,666 @@ public: } private: - float &mAlpha; + const float &mAlpha; }; - -protected: - float &mAlpha; -}; - -void SkyManager::setCamera(osg::Camera *camera) -{ - mCamera = camera; } -class WrapAroundOperator : public osgParticle::Operator +namespace MWRender { -public: - WrapAroundOperator(osg::Camera *camera, const osg::Vec3 &wrapRange): osgParticle::Operator(), - mCamera(camera), mWrapRange(wrapRange), mHalfWrapRange(mWrapRange / 2.0) + SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager) + : mSceneManager(sceneManager) + , mCamera(nullptr) + , mAtmosphereNightRoll(0.f) + , mCreated(false) + , mIsStorm(false) + , mDay(0) + , mMonth(0) + , mCloudAnimationTimer(0.f) + , mRainTimer(0.f) + , mStormParticleDirection(MWWorld::Weather::defaultDirection()) + , mStormDirection(MWWorld::Weather::defaultDirection()) + , mClouds() + , mNextClouds() + , mCloudBlendFactor(0.f) + , mCloudSpeed(0.f) + , mStarsOpacity(0.f) + , mRemainingTransitionTime(0.f) + , mRainEnabled(false) + , mRainSpeed(0.f) + , mRainDiameter(0.f) + , mRainMinHeight(0.f) + , mRainMaxHeight(0.f) + , mRainEntranceSpeed(1.f) + , mRainMaxRaindrops(0) + , mWindSpeed(0.f) + , mBaseWindSpeed(0.f) + , mEnabled(true) + , mSunEnabled(true) + , mPrecipitationAlpha(0.f) { - mPreviousCameraPosition = getCameraPosition(); + osg::ref_ptr skyroot = new CameraRelativeTransform; + skyroot->setName("Sky Root"); + // Assign empty program to specify we don't want shaders when we are rendering in FFP pipeline + if (!mSceneManager->getForceShaders()) + skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), osg::StateAttribute::OVERRIDE|osg::StateAttribute::PROTECTED|osg::StateAttribute::ON); + SceneUtil::ShadowManager::disableShadowsForStateSet(skyroot->getOrCreateStateSet()); + + skyroot->setNodeMask(Mask_Sky); + parentNode->addChild(skyroot); + + mRootNode = skyroot; + + mEarlyRenderBinRoot = new osg::Group; + // render before the world is rendered + mEarlyRenderBinRoot->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Sky, "RenderBin"); + // Prevent unwanted clipping by water reflection camera's clipping plane + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_CLIP_PLANE0, osg::StateAttribute::OFF); + mRootNode->addChild(mEarlyRenderBinRoot); + + mUnderwaterSwitch = new UnderwaterSwitchCallback(skyroot); } - osg::Object *cloneType() const override + void SkyManager::create() { - return nullptr; - } + assert(!mCreated); - osg::Object *clone(const osg::CopyOp &op) const override - { - return nullptr; - } + bool forceShaders = mSceneManager->getForceShaders(); - void operate(osgParticle::Particle *P, double dt) override - { - } + mAtmosphereDay = mSceneManager->getInstance(Settings::Manager::getString("skyatmosphere", "Models"), mEarlyRenderBinRoot); + ModVertexAlphaVisitor modAtmosphere(ModVertexAlphaVisitor::Atmosphere); + mAtmosphereDay->accept(modAtmosphere); - void operateParticles(osgParticle::ParticleSystem *ps, double dt) override - { - osg::Vec3 position = getCameraPosition(); - osg::Vec3 positionDifference = position - mPreviousCameraPosition; + mAtmosphereUpdater = new AtmosphereUpdater; + mAtmosphereDay->addUpdateCallback(mAtmosphereUpdater); - osg::Matrix toWorld, toLocal; + mAtmosphereNightNode = new osg::PositionAttitudeTransform; + mAtmosphereNightNode->setNodeMask(0); + mEarlyRenderBinRoot->addChild(mAtmosphereNightNode); - std::vector worldMatrices = ps->getWorldMatrices(); - - if (!worldMatrices.empty()) + osg::ref_ptr atmosphereNight; + if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) + atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight02", "Models"), mAtmosphereNightNode); + else + atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight01", "Models"), mAtmosphereNightNode); + atmosphereNight->getOrCreateStateSet()->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + ModVertexAlphaVisitor modStars(ModVertexAlphaVisitor::Stars); + atmosphereNight->accept(modStars); + mAtmosphereNightUpdater = new AtmosphereNightUpdater(mSceneManager->getImageManager(), forceShaders); + atmosphereNight->addUpdateCallback(mAtmosphereNightUpdater); + + mSun.reset(new Sun(mEarlyRenderBinRoot, *mSceneManager->getImageManager())); + mMasser.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager, Fallback::Map::getFloat("Moons_Masser_Size")/125, Moon::Type_Masser)); + mSecunda.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager, Fallback::Map::getFloat("Moons_Secunda_Size")/125, Moon::Type_Secunda)); + + mCloudNode = new osg::Group; + mEarlyRenderBinRoot->addChild(mCloudNode); + + mCloudMesh = new osg::PositionAttitudeTransform; + osg::ref_ptr cloudMeshChild = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mCloudMesh); + mCloudUpdater = new CloudUpdater(forceShaders); + mCloudUpdater->setOpacity(1.f); + cloudMeshChild->addUpdateCallback(mCloudUpdater); + mCloudMesh->addChild(cloudMeshChild); + + mNextCloudMesh = new osg::PositionAttitudeTransform; + osg::ref_ptr nextCloudMeshChild = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mNextCloudMesh); + mNextCloudUpdater = new CloudUpdater(forceShaders); + mNextCloudUpdater->setOpacity(0.f); + nextCloudMeshChild->addUpdateCallback(mNextCloudUpdater); + mNextCloudMesh->setNodeMask(0); + mNextCloudMesh->addChild(nextCloudMeshChild); + + mCloudNode->addChild(mCloudMesh); + mCloudNode->addChild(mNextCloudMesh); + + ModVertexAlphaVisitor modClouds(ModVertexAlphaVisitor::Clouds); + mCloudMesh->accept(modClouds); + mNextCloudMesh->accept(modClouds); + + if (mSceneManager->getForceShaders()) { - toWorld = worldMatrices[0]; - toLocal.invert(toWorld); + auto vertex = mSceneManager->getShaderManager().getShader("sky_vertex.glsl", {}, osg::Shader::VERTEX); + auto fragment = mSceneManager->getShaderManager().getShader("sky_fragment.glsl", {}, osg::Shader::FRAGMENT); + auto program = mSceneManager->getShaderManager().getProgram(vertex, fragment); + mEarlyRenderBinRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("pass", -1)); + mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(program, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); } - for (int i = 0; i < ps->numParticles(); ++i) - { - osgParticle::Particle *p = ps->getParticle(i); - p->setPosition(toWorld.preMult(p->getPosition())); - p->setPosition(p->getPosition() - positionDifference); + auto depth = SceneUtil::createDepth(); + depth->setWriteMask(false); + mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth); + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_FOG, osg::StateAttribute::OFF); - for (int j = 0; j < 3; ++j) // wrap-around in all 3 dimensions - { - osg::Vec3 pos = p->getPosition(); + mMoonScriptColor = Fallback::Map::getColour("Moons_Script_Color"); - if (pos[j] < -mHalfWrapRange[j]) - pos[j] = mHalfWrapRange[j] + fmod(pos[j] - mHalfWrapRange[j],mWrapRange[j]); - else if (pos[j] > mHalfWrapRange[j]) - pos[j] = fmod(pos[j] + mHalfWrapRange[j],mWrapRange[j]) - mHalfWrapRange[j]; - - p->setPosition(pos); - } - - p->setPosition(toLocal.preMult(p->getPosition())); - } - - mPreviousCameraPosition = position; + mCreated = true; } -protected: - osg::Camera *mCamera; - osg::Vec3 mPreviousCameraPosition; - osg::Vec3 mWrapRange; - osg::Vec3 mHalfWrapRange; - - osg::Vec3 getCameraPosition() - { - return mCamera->getInverseViewMatrix().getTrans(); - } -}; - -class WeatherAlphaOperator : public osgParticle::Operator -{ -public: - WeatherAlphaOperator(float& alpha, bool rain) - : mAlpha(alpha) - , mIsRain(rain) + void SkyManager::setCamera(osg::Camera *camera) { + mCamera = camera; } - osg::Object *cloneType() const override + void SkyManager::createRain() { - return nullptr; - } + if (mRainNode) + return; - osg::Object *clone(const osg::CopyOp &op) const override - { - return nullptr; - } - - void operate(osgParticle::Particle *particle, double dt) override - { - constexpr float rainThreshold = 0.6f; // Rain_Threshold? - const float alpha = mIsRain ? mAlpha * rainThreshold : mAlpha; - particle->setAlphaRange(osgParticle::rangef(alpha, alpha)); - } - -private: - float &mAlpha; - bool mIsRain; -}; - -void SkyManager::createRain() -{ - if (mRainNode) - return; - - mRainNode = new osg::Group; - - mRainParticleSystem = new NifOsg::ParticleSystem; - osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); - - mRainParticleSystem->setParticleAlignment(osgParticle::ParticleSystem::FIXED); - mRainParticleSystem->setAlignVectorX(osg::Vec3f(0.1,0,0)); - mRainParticleSystem->setAlignVectorY(osg::Vec3f(0,0,1)); - - osg::ref_ptr stateset (mRainParticleSystem->getOrCreateStateSet()); - - osg::ref_ptr raindropTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage("textures/tx_raindrop_01.dds"))); - raindropTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - raindropTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - stateset->setTextureAttributeAndModes(0, raindropTex, osg::StateAttribute::ON); - stateset->setNestRenderBins(false); - stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); - stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); - stateset->setMode(GL_BLEND, osg::StateAttribute::ON); - - osg::ref_ptr mat (new osg::Material); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); - mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); - - osgParticle::Particle& particleTemplate = mRainParticleSystem->getDefaultParticleTemplate(); - particleTemplate.setSizeRange(osgParticle::rangef(5.f, 15.f)); - particleTemplate.setAlphaRange(osgParticle::rangef(1.f, 1.f)); - particleTemplate.setLifeTime(1); - - osg::ref_ptr emitter (new osgParticle::ModularEmitter); - emitter->setParticleSystem(mRainParticleSystem); - - osg::ref_ptr placer (new osgParticle::BoxPlacer); - placer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); - placer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); - placer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); - emitter->setPlacer(placer); - mPlacer = placer; - - // FIXME: vanilla engine does not use a particle system to handle rain, it uses a NIF-file with 20 raindrops in it. - // It spawns the (maxRaindrops-getParticleSystem()->numParticles())*dt/rainEntranceSpeed batches every frame (near 1-2). - // Since the rain is a regular geometry, it produces water ripples, also in theory it can be removed if collides with something. - osg::ref_ptr counter (new RainCounter); - counter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); - emitter->setCounter(counter); - mCounter = counter; - - osg::ref_ptr shooter (new RainShooter); - mRainShooter = shooter; - emitter->setShooter(shooter); - - osg::ref_ptr updater (new osgParticle::ParticleSystemUpdater); - updater->addParticleSystem(mRainParticleSystem); - - osg::ref_ptr program (new osgParticle::ModularProgram); - program->addOperator(new WrapAroundOperator(mCamera,rainRange)); - program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, true)); - program->setParticleSystem(mRainParticleSystem); - mRainNode->addChild(program); - - mRainNode->addChild(emitter); - mRainNode->addChild(mRainParticleSystem); - mRainNode->addChild(updater); - - // Note: if we ever switch to regular geometry rain, it'll need to use an AlphaFader. - mRainNode->addCullCallback(mUnderwaterSwitch); - mRainNode->setNodeMask(Mask_WeatherParticles); - - mRootNode->addChild(mRainNode); -} - -void SkyManager::destroyRain() -{ - if (!mRainNode) - return; - - mRootNode->removeChild(mRainNode); - mRainNode = nullptr; - mPlacer = nullptr; - mCounter = nullptr; - mRainParticleSystem = nullptr; - mRainShooter = nullptr; -} - -SkyManager::~SkyManager() -{ - if (mRootNode) - { - mRootNode->getParent(0)->removeChild(mRootNode); - mRootNode = nullptr; - } -} - -int SkyManager::getMasserPhase() const -{ - if (!mCreated) return 0; - return mMasser->getPhaseInt(); -} - -int SkyManager::getSecundaPhase() const -{ - if (!mCreated) return 0; - return mSecunda->getPhaseInt(); -} - -bool SkyManager::isEnabled() -{ - return mEnabled; -} - -bool SkyManager::hasRain() const -{ - return mRainNode != nullptr; -} - -float SkyManager::getPrecipitationAlpha() const -{ - if (mEnabled && !mIsStorm && (hasRain() || mParticleNode)) - return mPrecipitationAlpha; - - return 0.f; -} - -void SkyManager::update(float duration) -{ - if (!mEnabled) - return; - - switchUnderwaterRain(); - - if (mIsStorm) - { - osg::Quat quat; - quat.makeRotate(osg::Vec3f(0,1,0), mStormDirection); - - mCloudNode->setAttitude(quat); - if (mParticleNode) - { - // Morrowind deliberately rotates the blizzard mesh, so so should we. - if (mCurrentParticleEffect == Settings::Manager::getString("weatherblizzard", "Models")) - quat.makeRotate(osg::Vec3f(-1,0,0), mStormDirection); - mParticleNode->setAttitude(quat); - } - } - else - mCloudNode->setAttitude(osg::Quat()); - - // UV Scroll the clouds - mCloudAnimationTimer += duration * mCloudSpeed * 0.003; - mCloudUpdater->setAnimationTimer(mCloudAnimationTimer); - mCloudUpdater2->setAnimationTimer(mCloudAnimationTimer); - - // rotate the stars by 360 degrees every 4 days - mAtmosphereNightRoll += MWBase::Environment::get().getWorld()->getTimeScaleFactor()*duration*osg::DegreesToRadians(360.f) / (3600*96.f); - if (mAtmosphereNightNode->getNodeMask() != 0) - mAtmosphereNightNode->setAttitude(osg::Quat(mAtmosphereNightRoll, osg::Vec3f(0,0,1))); -} - -void SkyManager::setEnabled(bool enabled) -{ - if (enabled && !mCreated) - create(); - - mRootNode->setNodeMask(enabled ? Mask_Sky : 0u); - - mEnabled = enabled; -} - -void SkyManager::setMoonColour (bool red) -{ - if (!mCreated) return; - mSecunda->setColor(red ? mMoonScriptColor : osg::Vec4f(1,1,1,1)); -} - -void SkyManager::updateRainParameters() -{ - if (mRainShooter) - { - float angle = -std::atan(mWindSpeed/50.f); - mRainShooter->setVelocity(osg::Vec3f(0, mRainSpeed*std::sin(angle), -mRainSpeed/std::cos(angle))); - mRainShooter->setAngle(angle); + mRainNode = new osg::Group; + mRainParticleSystem = new NifOsg::ParticleSystem; osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); - mPlacer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); - mPlacer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); - mPlacer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); + mRainParticleSystem->setParticleAlignment(osgParticle::ParticleSystem::FIXED); + mRainParticleSystem->setAlignVectorX(osg::Vec3f(0.1,0,0)); + mRainParticleSystem->setAlignVectorY(osg::Vec3f(0,0,1)); - mCounter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); + osg::ref_ptr stateset = mRainParticleSystem->getOrCreateStateSet(); + + osg::ref_ptr raindropTex = new osg::Texture2D(mSceneManager->getImageManager()->getImage("textures/tx_raindrop_01.dds")); + raindropTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + raindropTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + + stateset->setTextureAttributeAndModes(0, raindropTex); + stateset->setNestRenderBins(false); + stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + stateset->setMode(GL_BLEND, osg::StateAttribute::ON); + + osg::ref_ptr mat = new osg::Material; + mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); + mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + stateset->setAttributeAndModes(mat); + + osgParticle::Particle& particleTemplate = mRainParticleSystem->getDefaultParticleTemplate(); + particleTemplate.setSizeRange(osgParticle::rangef(5.f, 15.f)); + particleTemplate.setAlphaRange(osgParticle::rangef(1.f, 1.f)); + particleTemplate.setLifeTime(1); + + osg::ref_ptr emitter = new osgParticle::ModularEmitter; + emitter->setParticleSystem(mRainParticleSystem); + + osg::ref_ptr placer = new osgParticle::BoxPlacer; + placer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); + placer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); + placer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); + emitter->setPlacer(placer); + mPlacer = placer; + + // FIXME: vanilla engine does not use a particle system to handle rain, it uses a NIF-file with 20 raindrops in it. + // It spawns the (maxRaindrops-getParticleSystem()->numParticles())*dt/rainEntranceSpeed batches every frame (near 1-2). + // Since the rain is a regular geometry, it produces water ripples, also in theory it can be removed if collides with something. + osg::ref_ptr counter = new RainCounter; + counter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); + emitter->setCounter(counter); + mCounter = counter; + + osg::ref_ptr shooter = new RainShooter; + mRainShooter = shooter; + emitter->setShooter(shooter); + + osg::ref_ptr updater = new osgParticle::ParticleSystemUpdater; + updater->addParticleSystem(mRainParticleSystem); + + osg::ref_ptr program = new osgParticle::ModularProgram; + program->addOperator(new WrapAroundOperator(mCamera,rainRange)); + program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, true)); + program->setParticleSystem(mRainParticleSystem); + mRainNode->addChild(program); + + mRainNode->addChild(emitter); + mRainNode->addChild(mRainParticleSystem); + mRainNode->addChild(updater); + + // Note: if we ever switch to regular geometry rain, it'll need to use an AlphaFader. + mRainNode->addCullCallback(mUnderwaterSwitch); + mRainNode->setNodeMask(Mask_WeatherParticles); + + mRainParticleSystem->setUserValue("simpleLighting", true); + mSceneManager->recreateShaders(mRainNode); + + mRootNode->addChild(mRainNode); } -} -void SkyManager::switchUnderwaterRain() -{ - if (!mRainParticleSystem) - return; - - bool freeze = mUnderwaterSwitch->isUnderwater(); - mRainParticleSystem->setFrozen(freeze); -} - -void SkyManager::setWeather(const WeatherResult& weather) -{ - if (!mCreated) return; - - mRainEntranceSpeed = weather.mRainEntranceSpeed; - mRainMaxRaindrops = weather.mRainMaxRaindrops; - mRainDiameter = weather.mRainDiameter; - mRainMinHeight = weather.mRainMinHeight; - mRainMaxHeight = weather.mRainMaxHeight; - mRainSpeed = weather.mRainSpeed; - mWindSpeed = weather.mWindSpeed; - mBaseWindSpeed = weather.mBaseWindSpeed; - - if (mRainEffect != weather.mRainEffect) + void SkyManager::destroyRain() { - mRainEffect = weather.mRainEffect; - if (!mRainEffect.empty()) + if (!mRainNode) + return; + + mRootNode->removeChild(mRainNode); + mRainNode = nullptr; + mPlacer = nullptr; + mCounter = nullptr; + mRainParticleSystem = nullptr; + mRainShooter = nullptr; + } + + SkyManager::~SkyManager() + { + if (mRootNode) { - createRain(); - } - else - { - destroyRain(); + mRootNode->getParent(0)->removeChild(mRootNode); + mRootNode = nullptr; } } - updateRainParameters(); - - mIsStorm = weather.mIsStorm; - - if (mCurrentParticleEffect != weather.mParticleEffect) + int SkyManager::getMasserPhase() const { - mCurrentParticleEffect = weather.mParticleEffect; + if (!mCreated) return 0; + return mMasser->getPhaseInt(); + } - // cleanup old particles - if (mParticleEffect) + int SkyManager::getSecundaPhase() const + { + if (!mCreated) return 0; + return mSecunda->getPhaseInt(); + } + + bool SkyManager::isEnabled() + { + return mEnabled; + } + + bool SkyManager::hasRain() const + { + return mRainNode != nullptr; + } + + float SkyManager::getPrecipitationAlpha() const + { + if (mEnabled && !mIsStorm && (hasRain() || mParticleNode)) + return mPrecipitationAlpha; + + return 0.f; + } + + void SkyManager::update(float duration) + { + if (!mEnabled) + return; + + switchUnderwaterRain(); + + if (mIsStorm && mParticleNode) { + osg::Quat quat; + quat.makeRotate(MWWorld::Weather::defaultDirection(), mStormParticleDirection); + // Morrowind deliberately rotates the blizzard mesh, so so should we. + if (mCurrentParticleEffect == Settings::Manager::getString("weatherblizzard", "Models")) + quat.makeRotate(osg::Vec3f(-1,0,0), mStormParticleDirection); + mParticleNode->setAttitude(quat); + } + + // UV Scroll the clouds + mCloudAnimationTimer += duration * mCloudSpeed * 0.003; + mNextCloudUpdater->setTextureCoord(mCloudAnimationTimer); + mCloudUpdater->setTextureCoord(mCloudAnimationTimer); + + // morrowind rotates each cloud mesh independently + osg::Quat rotation; + rotation.makeRotate(MWWorld::Weather::defaultDirection(), mStormDirection); + mCloudMesh->setAttitude(rotation); + + if (mNextCloudMesh->getNodeMask()) + { + rotation.makeRotate(MWWorld::Weather::defaultDirection(), mNextStormDirection); + mNextCloudMesh->setAttitude(rotation); + } + + // rotate the stars by 360 degrees every 4 days + mAtmosphereNightRoll += MWBase::Environment::get().getWorld()->getTimeScaleFactor()*duration*osg::DegreesToRadians(360.f) / (3600*96.f); + if (mAtmosphereNightNode->getNodeMask() != 0) + mAtmosphereNightNode->setAttitude(osg::Quat(mAtmosphereNightRoll, osg::Vec3f(0,0,1))); + } + + void SkyManager::setEnabled(bool enabled) + { + if (enabled && !mCreated) + create(); + + mRootNode->setNodeMask(enabled ? Mask_Sky : 0u); + + if (!enabled && mParticleNode && mParticleEffect) + { + mCurrentParticleEffect.clear(); mParticleNode->removeChild(mParticleEffect); mParticleEffect = nullptr; } - if (mCurrentParticleEffect.empty()) + mEnabled = enabled; + } + + void SkyManager::setMoonColour (bool red) + { + if (!mCreated) return; + mSecunda->setColor(red ? mMoonScriptColor : osg::Vec4f(1,1,1,1)); + } + + void SkyManager::updateRainParameters() + { + if (mRainShooter) { - if (mParticleNode) - { - mRootNode->removeChild(mParticleNode); - mParticleNode = nullptr; - } - } - else - { - if (!mParticleNode) - { - mParticleNode = new osg::PositionAttitudeTransform; - mParticleNode->addCullCallback(mUnderwaterSwitch); - mParticleNode->setNodeMask(Mask_WeatherParticles); - mRootNode->addChild(mParticleNode); - } + float angle = -std::atan(mWindSpeed/50.f); + mRainShooter->setVelocity(osg::Vec3f(0, mRainSpeed*std::sin(angle), -mRainSpeed/std::cos(angle))); + mRainShooter->setAngle(angle); - mParticleEffect = mSceneManager->getInstance(mCurrentParticleEffect, mParticleNode); + osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); - SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::shared_ptr(new SceneUtil::FrameTimeSource)); - mParticleEffect->accept(assignVisitor); + mPlacer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); + mPlacer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); + mPlacer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); - AlphaFader::SetupVisitor alphaFaderSetupVisitor(mPrecipitationAlpha); - - mParticleEffect->accept(alphaFaderSetupVisitor); - - SceneUtil::FindByClassVisitor findPSVisitor(std::string("ParticleSystem")); - mParticleEffect->accept(findPSVisitor); - - for (unsigned int i = 0; i < findPSVisitor.mFoundNodes.size(); ++i) - { - osgParticle::ParticleSystem *ps = static_cast(findPSVisitor.mFoundNodes[i]); - - osg::ref_ptr program (new osgParticle::ModularProgram); - if (!mIsStorm) - program->addOperator(new WrapAroundOperator(mCamera,osg::Vec3(1024,1024,800))); - program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, false)); - program->setParticleSystem(ps); - mParticleNode->addChild(program); - } + mCounter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); } } - if (mClouds != weather.mCloudTexture) + void SkyManager::switchUnderwaterRain() { - mClouds = weather.mCloudTexture; + if (!mRainParticleSystem) + return; - std::string texture = Misc::ResourceHelpers::correctTexturePath(mClouds, mSceneManager->getVFS()); - - osg::ref_ptr cloudTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture))); - cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); - cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - - mCloudUpdater->setTexture(cloudTex); + bool freeze = mUnderwaterSwitch->isUnderwater(); + mRainParticleSystem->setFrozen(freeze); } - if (mNextClouds != weather.mNextCloudTexture) + void SkyManager::setWeather(const WeatherResult& weather) { - mNextClouds = weather.mNextCloudTexture; + if (!mCreated) return; - if (!mNextClouds.empty()) + mRainEntranceSpeed = weather.mRainEntranceSpeed; + mRainMaxRaindrops = weather.mRainMaxRaindrops; + mRainDiameter = weather.mRainDiameter; + mRainMinHeight = weather.mRainMinHeight; + mRainMaxHeight = weather.mRainMaxHeight; + mRainSpeed = weather.mRainSpeed; + mWindSpeed = weather.mWindSpeed; + mBaseWindSpeed = weather.mBaseWindSpeed; + + if (mRainEffect != weather.mRainEffect) { - std::string texture = Misc::ResourceHelpers::correctTexturePath(mNextClouds, mSceneManager->getVFS()); + mRainEffect = weather.mRainEffect; + if (!mRainEffect.empty()) + { + createRain(); + } + else + { + destroyRain(); + } + } - osg::ref_ptr cloudTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture))); + updateRainParameters(); + + mIsStorm = weather.mIsStorm; + + if (mIsStorm) + mStormDirection = weather.mStormDirection; + + if (mCurrentParticleEffect != weather.mParticleEffect) + { + mCurrentParticleEffect = weather.mParticleEffect; + + // cleanup old particles + if (mParticleEffect) + { + mParticleNode->removeChild(mParticleEffect); + mParticleEffect = nullptr; + } + + if (mCurrentParticleEffect.empty()) + { + if (mParticleNode) + { + mRootNode->removeChild(mParticleNode); + mParticleNode = nullptr; + } + } + else + { + if (!mParticleNode) + { + mParticleNode = new osg::PositionAttitudeTransform; + mParticleNode->addCullCallback(mUnderwaterSwitch); + mParticleNode->setNodeMask(Mask_WeatherParticles); + mParticleNode->getOrCreateStateSet(); + mRootNode->addChild(mParticleNode); + } + + mParticleEffect = mSceneManager->getInstance(mCurrentParticleEffect, mParticleNode); + + SceneUtil::AssignControllerSourcesVisitor assignVisitor = std::shared_ptr(new SceneUtil::FrameTimeSource); + mParticleEffect->accept(assignVisitor); + + SetupVisitor alphaFaderSetupVisitor(mPrecipitationAlpha); + mParticleEffect->accept(alphaFaderSetupVisitor); + + SceneUtil::FindByClassVisitor findPSVisitor(std::string("ParticleSystem")); + mParticleEffect->accept(findPSVisitor); + + for (unsigned int i = 0; i < findPSVisitor.mFoundNodes.size(); ++i) + { + osgParticle::ParticleSystem *ps = static_cast(findPSVisitor.mFoundNodes[i]); + + osg::ref_ptr program = new osgParticle::ModularProgram; + if (!mIsStorm) + program->addOperator(new WrapAroundOperator(mCamera,osg::Vec3(1024,1024,800))); + program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, false)); + program->setParticleSystem(ps); + mParticleNode->addChild(program); + + for (int particleIndex = 0; particleIndex < ps->numParticles(); ++particleIndex) + { + ps->getParticle(particleIndex)->setAlphaRange(osgParticle::rangef(mPrecipitationAlpha, mPrecipitationAlpha)); + ps->getParticle(particleIndex)->update(0, true); + } + + ps->getOrCreateStateSet(); + ps->setUserValue("simpleLighting", true); + } + + mSceneManager->recreateShaders(mParticleNode); + } + } + + if (mClouds != weather.mCloudTexture) + { + mClouds = weather.mCloudTexture; + + std::string texture = Misc::ResourceHelpers::correctTexturePath(mClouds, mSceneManager->getVFS()); + + osg::ref_ptr cloudTex = new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture)); cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - mCloudUpdater2->setTexture(cloudTex); + mCloudUpdater->setTexture(cloudTex); } + + if (mStormDirection != weather.mStormDirection) + mStormDirection = weather.mStormDirection; + + if (mNextStormDirection != weather.mNextStormDirection) + mNextStormDirection = weather.mNextStormDirection; + + if (mNextClouds != weather.mNextCloudTexture) + { + mNextClouds = weather.mNextCloudTexture; + + if (!mNextClouds.empty()) + { + std::string texture = Misc::ResourceHelpers::correctTexturePath(mNextClouds, mSceneManager->getVFS()); + + osg::ref_ptr cloudTex = new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture)); + cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); + cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); + + mNextCloudUpdater->setTexture(cloudTex); + mNextStormDirection = weather.mStormDirection; + } + } + + if (mCloudBlendFactor != weather.mCloudBlendFactor) + { + mCloudBlendFactor = std::clamp(weather.mCloudBlendFactor, 0.f, 1.f); + + mCloudUpdater->setOpacity(1.f - mCloudBlendFactor); + mNextCloudUpdater->setOpacity(mCloudBlendFactor); + mNextCloudMesh->setNodeMask(mCloudBlendFactor > 0.f ? ~0u : 0); + } + + if (mCloudColour != weather.mFogColor) + { + osg::Vec4f clr (weather.mFogColor); + clr += osg::Vec4f(0.13f, 0.13f, 0.13f, 0.f); + + mCloudUpdater->setEmissionColor(clr); + mNextCloudUpdater->setEmissionColor(clr); + + mCloudColour = weather.mFogColor; + } + + if (mSkyColour != weather.mSkyColor) + { + mSkyColour = weather.mSkyColor; + + mAtmosphereUpdater->setEmissionColor(mSkyColour); + mMasser->setAtmosphereColor(mSkyColour); + mSecunda->setAtmosphereColor(mSkyColour); + } + + if (mFogColour != weather.mFogColor) + { + mFogColour = weather.mFogColor; + } + + mCloudSpeed = weather.mCloudSpeed; + + mMasser->adjustTransparency(weather.mGlareView); + mSecunda->adjustTransparency(weather.mGlareView); + + mSun->setColor(weather.mSunDiscColor); + mSun->adjustTransparency(weather.mGlareView * weather.mSunDiscColor.a()); + + float nextStarsOpacity = weather.mNightFade * weather.mGlareView; + + if (weather.mNight && mStarsOpacity != nextStarsOpacity) + { + mStarsOpacity = nextStarsOpacity; + + mAtmosphereNightUpdater->setFade(mStarsOpacity); + } + + mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0u : 0); + mPrecipitationAlpha = weather.mPrecipitationAlpha; } - if (mCloudBlendFactor != weather.mCloudBlendFactor) + float SkyManager::getBaseWindSpeed() const { - mCloudBlendFactor = weather.mCloudBlendFactor; + if (!mCreated) return 0.f; - mCloudUpdater->setOpacity((1.f-mCloudBlendFactor)); - mCloudUpdater2->setOpacity(mCloudBlendFactor); - mCloudMesh2->setNodeMask(mCloudBlendFactor > 0.f ? ~0u : 0); + return mBaseWindSpeed; } - if (mCloudColour != weather.mFogColor) + void SkyManager::sunEnable() { - osg::Vec4f clr (weather.mFogColor); - clr += osg::Vec4f(0.13f, 0.13f, 0.13f, 0.f); + if (!mCreated) return; - mCloudUpdater->setEmissionColor(clr); - mCloudUpdater2->setEmissionColor(clr); - - mCloudColour = weather.mFogColor; + mSun->setVisible(true); } - if (mSkyColour != weather.mSkyColor) + void SkyManager::sunDisable() { - mSkyColour = weather.mSkyColor; + if (!mCreated) return; - mAtmosphereUpdater->setEmissionColor(mSkyColour); - mMasser->setAtmosphereColor(mSkyColour); - mSecunda->setAtmosphereColor(mSkyColour); + mSun->setVisible(false); } - if (mFogColour != weather.mFogColor) + void SkyManager::setStormParticleDirection(const osg::Vec3f &direction) { - mFogColour = weather.mFogColor; + mStormParticleDirection = direction; } - mCloudSpeed = weather.mCloudSpeed; - - mMasser->adjustTransparency(weather.mGlareView); - mSecunda->adjustTransparency(weather.mGlareView); - - mSun->setColor(weather.mSunDiscColor); - mSun->adjustTransparency(weather.mGlareView * weather.mSunDiscColor.a()); - - float nextStarsOpacity = weather.mNightFade * weather.mGlareView; - - if (weather.mNight && mStarsOpacity != nextStarsOpacity) + void SkyManager::setSunDirection(const osg::Vec3f& direction) { - mStarsOpacity = nextStarsOpacity; + if (!mCreated) return; - mAtmosphereNightUpdater->setFade(mStarsOpacity); + mSun->setDirection(direction); } - mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0u : 0); - - mPrecipitationAlpha = weather.mPrecipitationAlpha; -} - -float SkyManager::getBaseWindSpeed() const -{ - if (!mCreated) return 0.f; - - return mBaseWindSpeed; -} - -void SkyManager::sunEnable() -{ - if (!mCreated) return; - - mSun->setVisible(true); -} - -void SkyManager::sunDisable() -{ - if (!mCreated) return; - - mSun->setVisible(false); -} - -void SkyManager::setStormDirection(const osg::Vec3f &direction) -{ - mStormDirection = direction; -} - -void SkyManager::setSunDirection(const osg::Vec3f& direction) -{ - if (!mCreated) return; - - mSun->setDirection(direction); -} - -void SkyManager::setMasserState(const MoonState& state) -{ - if(!mCreated) return; - - mMasser->setState(state); -} - -void SkyManager::setSecundaState(const MoonState& state) -{ - if(!mCreated) return; - - mSecunda->setState(state); -} - -void SkyManager::setDate(int day, int month) -{ - mDay = day; - mMonth = month; -} - -void SkyManager::setGlareTimeOfDayFade(float val) -{ - mSun->setGlareTimeOfDayFade(val); -} - -void SkyManager::setWaterHeight(float height) -{ - mUnderwaterSwitch->setWaterLevel(height); -} - -void SkyManager::listAssetsToPreload(std::vector& models, std::vector& textures) -{ - models.emplace_back(Settings::Manager::getString("skyatmosphere", "Models")); - if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) - models.emplace_back(Settings::Manager::getString("skynight02", "Models")); - models.emplace_back(Settings::Manager::getString("skynight01", "Models")); - models.emplace_back(Settings::Manager::getString("skyclouds", "Models")); - - models.emplace_back(Settings::Manager::getString("weatherashcloud", "Models")); - models.emplace_back(Settings::Manager::getString("weatherblightcloud", "Models")); - models.emplace_back(Settings::Manager::getString("weathersnow", "Models")); - models.emplace_back(Settings::Manager::getString("weatherblizzard", "Models")); - - textures.emplace_back("textures/tx_mooncircle_full_s.dds"); - textures.emplace_back("textures/tx_mooncircle_full_m.dds"); - - textures.emplace_back("textures/tx_masser_new.dds"); - textures.emplace_back("textures/tx_masser_one_wax.dds"); - textures.emplace_back("textures/tx_masser_half_wax.dds"); - textures.emplace_back("textures/tx_masser_three_wax.dds"); - textures.emplace_back("textures/tx_masser_one_wan.dds"); - textures.emplace_back("textures/tx_masser_half_wan.dds"); - textures.emplace_back("textures/tx_masser_three_wan.dds"); - textures.emplace_back("textures/tx_masser_full.dds"); - - textures.emplace_back("textures/tx_secunda_new.dds"); - textures.emplace_back("textures/tx_secunda_one_wax.dds"); - textures.emplace_back("textures/tx_secunda_half_wax.dds"); - textures.emplace_back("textures/tx_secunda_three_wax.dds"); - textures.emplace_back("textures/tx_secunda_one_wan.dds"); - textures.emplace_back("textures/tx_secunda_half_wan.dds"); - textures.emplace_back("textures/tx_secunda_three_wan.dds"); - textures.emplace_back("textures/tx_secunda_full.dds"); - - textures.emplace_back("textures/tx_sun_05.dds"); - textures.emplace_back("textures/tx_sun_flash_grey_05.dds"); - - textures.emplace_back("textures/tx_raindrop_01.dds"); -} - -void SkyManager::setWaterEnabled(bool enabled) -{ - mUnderwaterSwitch->setEnabled(enabled); -} + void SkyManager::setMasserState(const MoonState& state) + { + if(!mCreated) return; + mMasser->setState(state); + } + + void SkyManager::setSecundaState(const MoonState& state) + { + if(!mCreated) return; + + mSecunda->setState(state); + } + + void SkyManager::setDate(int day, int month) + { + mDay = day; + mMonth = month; + } + + void SkyManager::setGlareTimeOfDayFade(float val) + { + mSun->setGlareTimeOfDayFade(val); + } + + void SkyManager::setWaterHeight(float height) + { + mUnderwaterSwitch->setWaterLevel(height); + } + + void SkyManager::listAssetsToPreload(std::vector& models, std::vector& textures) + { + models.emplace_back(Settings::Manager::getString("skyatmosphere", "Models")); + if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) + models.emplace_back(Settings::Manager::getString("skynight02", "Models")); + models.emplace_back(Settings::Manager::getString("skynight01", "Models")); + models.emplace_back(Settings::Manager::getString("skyclouds", "Models")); + + models.emplace_back(Settings::Manager::getString("weatherashcloud", "Models")); + models.emplace_back(Settings::Manager::getString("weatherblightcloud", "Models")); + models.emplace_back(Settings::Manager::getString("weathersnow", "Models")); + models.emplace_back(Settings::Manager::getString("weatherblizzard", "Models")); + + textures.emplace_back("textures/tx_mooncircle_full_s.dds"); + textures.emplace_back("textures/tx_mooncircle_full_m.dds"); + + textures.emplace_back("textures/tx_masser_new.dds"); + textures.emplace_back("textures/tx_masser_one_wax.dds"); + textures.emplace_back("textures/tx_masser_half_wax.dds"); + textures.emplace_back("textures/tx_masser_three_wax.dds"); + textures.emplace_back("textures/tx_masser_one_wan.dds"); + textures.emplace_back("textures/tx_masser_half_wan.dds"); + textures.emplace_back("textures/tx_masser_three_wan.dds"); + textures.emplace_back("textures/tx_masser_full.dds"); + + textures.emplace_back("textures/tx_secunda_new.dds"); + textures.emplace_back("textures/tx_secunda_one_wax.dds"); + textures.emplace_back("textures/tx_secunda_half_wax.dds"); + textures.emplace_back("textures/tx_secunda_three_wax.dds"); + textures.emplace_back("textures/tx_secunda_one_wan.dds"); + textures.emplace_back("textures/tx_secunda_half_wan.dds"); + textures.emplace_back("textures/tx_secunda_three_wan.dds"); + textures.emplace_back("textures/tx_secunda_full.dds"); + + textures.emplace_back("textures/tx_sun_05.dds"); + textures.emplace_back("textures/tx_sun_flash_grey_05.dds"); + + textures.emplace_back("textures/tx_raindrop_01.dds"); + } + + void SkyManager::setWaterEnabled(bool enabled) + { + mUnderwaterSwitch->setEnabled(enabled); + } } diff --git a/apps/openmw/mwrender/sky.hpp b/apps/openmw/mwrender/sky.hpp index f8c501dda6..1a30633886 100644 --- a/apps/openmw/mwrender/sky.hpp +++ b/apps/openmw/mwrender/sky.hpp @@ -8,10 +8,7 @@ #include #include -namespace osg -{ - class Camera; -} +#include "skyutil.hpp" namespace osg { @@ -19,6 +16,7 @@ namespace osg class Node; class Material; class PositionAttitudeTransform; + class Camera; } namespace osgParticle @@ -34,91 +32,6 @@ namespace Resource namespace MWRender { - class AtmosphereUpdater; - class AtmosphereNightUpdater; - class CloudUpdater; - class Sun; - class Moon; - class RainCounter; - class RainShooter; - class RainFader; - class AlphaFader; - class UnderwaterSwitchCallback; - - struct WeatherResult - { - std::string mCloudTexture; - std::string mNextCloudTexture; - float mCloudBlendFactor; - - osg::Vec4f mFogColor; - - osg::Vec4f mAmbientColor; - - osg::Vec4f mSkyColor; - - // sun light color - osg::Vec4f mSunColor; - - // alpha is the sun transparency - osg::Vec4f mSunDiscColor; - - float mFogDepth; - - float mDLFogFactor; - float mDLFogOffset; - - float mWindSpeed; - float mBaseWindSpeed; - float mCurrentWindSpeed; - float mNextWindSpeed; - - float mCloudSpeed; - - float mGlareView; - - bool mNight; // use night skybox - float mNightFade; // fading factor for night skybox - - bool mIsStorm; - - std::string mAmbientLoopSoundID; - float mAmbientSoundVolume; - - std::string mParticleEffect; - std::string mRainEffect; - float mPrecipitationAlpha; - - float mRainDiameter; - float mRainMinHeight; - float mRainMaxHeight; - float mRainSpeed; - float mRainEntranceSpeed; - int mRainMaxRaindrops; - }; - - struct MoonState - { - enum class Phase - { - Full = 0, - WaningGibbous, - ThirdQuarter, - WaningCrescent, - New, - WaxingCrescent, - FirstQuarter, - WaxingGibbous, - Unspecified - }; - - float mRotationFromHorizon; - float mRotationFromNorth; - Phase mPhase; - float mShadowBlend; - float mMoonAlpha; - }; - ///@brief The SkyManager handles rendering of the sky domes, celestial bodies as well as other objects that need to be rendered /// relative to the camera (e.g. weather particle effects) class SkyManager @@ -162,7 +75,7 @@ namespace MWRender void setRainSpeed(float speed); - void setStormDirection(const osg::Vec3f& direction); + void setStormParticleDirection(const osg::Vec3f& direction); void setSunDirection(const osg::Vec3f& direction); @@ -203,12 +116,12 @@ namespace MWRender osg::ref_ptr mParticleEffect; osg::ref_ptr mUnderwaterSwitch; - osg::ref_ptr mCloudNode; + osg::ref_ptr mCloudNode; osg::ref_ptr mCloudUpdater; - osg::ref_ptr mCloudUpdater2; - osg::ref_ptr mCloudMesh; - osg::ref_ptr mCloudMesh2; + osg::ref_ptr mNextCloudUpdater; + osg::ref_ptr mCloudMesh; + osg::ref_ptr mNextCloudMesh; osg::ref_ptr mAtmosphereDay; @@ -239,7 +152,10 @@ namespace MWRender float mRainTimer; + // particle system rotation is independent of cloud rotation internally + osg::Vec3f mStormParticleDirection; osg::Vec3f mStormDirection; + osg::Vec3f mNextStormDirection; // remember some settings so we don't have to apply them again if they didn't change std::string mClouds; @@ -275,4 +191,4 @@ namespace MWRender }; } -#endif // GAME_RENDER_SKY_H +#endif diff --git a/apps/openmw/mwrender/skyutil.cpp b/apps/openmw/mwrender/skyutil.cpp new file mode 100644 index 0000000000..0e2955333f --- /dev/null +++ b/apps/openmw/mwrender/skyutil.cpp @@ -0,0 +1,1132 @@ +#include "skyutil.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include +#include + +#include + +#include + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/weather.hpp" + +#include "vismask.hpp" +#include "renderbin.hpp" + +namespace +{ + enum class Pass + { + Atmosphere, + Atmosphere_Night, + Clouds, + Moon, + Sun, + Sunflash_Query, + Sunglare, + }; + + osg::ref_ptr createTexturedQuad(int numUvSets = 1, float scale = 1.f) + { + osg::ref_ptr geom = new osg::Geometry; + + osg::ref_ptr verts = new osg::Vec3Array; + verts->push_back(osg::Vec3f(-0.5 * scale, -0.5 * scale, 0)); + verts->push_back(osg::Vec3f(-0.5 * scale, 0.5 * scale, 0)); + verts->push_back(osg::Vec3f(0.5 * scale, 0.5 * scale, 0)); + verts->push_back(osg::Vec3f(0.5 * scale, -0.5 * scale, 0)); + + geom->setVertexArray(verts); + + osg::ref_ptr texcoords = new osg::Vec2Array; + texcoords->push_back(osg::Vec2f(0, 1)); + texcoords->push_back(osg::Vec2f(0, 0)); + texcoords->push_back(osg::Vec2f(1, 0)); + texcoords->push_back(osg::Vec2f(1, 1)); + + osg::ref_ptr colors = new osg::Vec4Array; + colors->push_back(osg::Vec4(1.f, 1.f, 1.f, 1.f)); + geom->setColorArray(colors, osg::Array::BIND_OVERALL); + + for (int i=0; isetTexCoordArray(i, texcoords, osg::Array::BIND_PER_VERTEX); + + geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4)); + + return geom; + } + + struct DummyComputeBoundCallback : osg::Node::ComputeBoundingSphereCallback + { + osg::BoundingSphere computeBound(const osg::Node& node) const override + { + return osg::BoundingSphere(); + } + }; +} + +namespace MWRender +{ + osg::ref_ptr createUnlitMaterial(osg::Material::ColorMode colorMode) + { + osg::ref_ptr mat = new osg::Material; + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); + mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); + mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); + mat->setColorMode(colorMode); + return mat; + } + + osg::ref_ptr createAlphaTrackingUnlitMaterial() + { + return createUnlitMaterial(osg::Material::DIFFUSE); + } + + class SunUpdater : public SceneUtil::StateSetUpdater + { + public: + osg::Vec4f mColor; + + SunUpdater() + : mColor(1.f, 1.f, 1.f, 1.f) + { } + + void setDefaults(osg::StateSet* stateset) override + { + stateset->setAttributeAndModes(createUnlitMaterial()); + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + { + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mColor.a())); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(mColor.r(), mColor.g(), mColor.b(), 1)); + } + }; + + OcclusionCallback::OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) + : mOcclusionQueryVisiblePixels(oqnVisible) + , mOcclusionQueryTotalPixels(oqnTotal) + { } + + float OcclusionCallback::getVisibleRatio (osg::Camera* camera) + { + int visible = mOcclusionQueryVisiblePixels->getQueryGeometry()->getNumPixels(camera); + int total = mOcclusionQueryTotalPixels->getQueryGeometry()->getNumPixels(camera); + + float visibleRatio = 0.f; + if (total > 0) + visibleRatio = static_cast(visible) / static_cast(total); + + float dt = MWBase::Environment::get().getFrameDuration(); + + float lastRatio = mLastRatio[osg::observer_ptr(camera)]; + + float change = dt*10; + + if (visibleRatio > lastRatio) + visibleRatio = std::min(visibleRatio, lastRatio + change); + else + visibleRatio = std::max(visibleRatio, lastRatio - change); + + mLastRatio[osg::observer_ptr(camera)] = visibleRatio; + + return visibleRatio; + } + + /// SunFlashCallback handles fading/scaling of a node depending on occlusion query result. Must be attached as a cull callback. + class SunFlashCallback : public OcclusionCallback, public SceneUtil::NodeCallback + { + public: + SunFlashCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) + : OcclusionCallback(oqnVisible, oqnTotal) + , mGlareView(1.f) + { } + + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) + { + float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); + + osg::ref_ptr stateset; + + if (visibleRatio > 0.f) + { + const float fadeThreshold = 0.1; + if (visibleRatio < fadeThreshold) + { + float fade = 1.f - (fadeThreshold - visibleRatio) / fadeThreshold; + osg::ref_ptr mat (createUnlitMaterial()); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade*mGlareView)); + stateset = new osg::StateSet; + stateset->setAttributeAndModes(mat, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + else if (visibleRatio < 1.f) + { + const float threshold = 0.6; + visibleRatio = visibleRatio * (1.f - threshold) + threshold; + } + } + + float scale = visibleRatio; + + if (scale == 0.f) + { + // no traverse + return; + } + else if (scale == 1.f) + traverse(node, cv); + else + { + osg::Matrix modelView = *cv->getModelViewMatrix(); + + modelView.preMultScale(osg::Vec3f(scale, scale, scale)); + + if (stateset) + cv->pushStateSet(stateset); + + cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); + + traverse(node, cv); + + cv->popModelViewMatrix(); + + if (stateset) + cv->popStateSet(); + } + } + + void setGlareView(float value) + { + mGlareView = value; + } + + private: + float mGlareView; + }; + + /// SunGlareCallback controls a full-screen glare effect depending on occlusion query result and the angle between sun and camera. + /// Must be attached as a cull callback to the node above the glare node. + class SunGlareCallback : public OcclusionCallback, public SceneUtil::NodeCallback + { + public: + SunGlareCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal, + osg::ref_ptr sunTransform) + : OcclusionCallback(oqnVisible, oqnTotal) + , mSunTransform(sunTransform) + , mTimeOfDayFade(1.f) + , mGlareView(1.f) + { + mColor = Fallback::Map::getColour("Weather_Sun_Glare_Fader_Color"); + mSunGlareFaderMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Max"); + mSunGlareFaderAngleMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Angle_Max"); + + // Replicating a design flaw in MW. The color was being set on both ambient and emissive properties, which multiplies the result by two, + // then finally gets clamped by the fixed function pipeline. With the default INI settings, only the red component gets clamped, + // so the resulting color looks more orange than red. + mColor *= 2; + for (int i=0; i<3; ++i) + mColor[i] = std::min(1.f, mColor[i]); + } + + void operator ()(osg::Node* node, osgUtil::CullVisitor* cv) + { + float angleRadians = getAngleToSunInRadians(*cv->getCurrentRenderStage()->getInitialViewMatrix()); + float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); + + const float angleMaxRadians = osg::DegreesToRadians(mSunGlareFaderAngleMax); + + float value = 1.f - std::min(1.f, angleRadians / angleMaxRadians); + float fade = value * mSunGlareFaderMax; + + fade *= mTimeOfDayFade * mGlareView * visibleRatio; + + if (fade == 0.f) + { + // no traverse + return; + } + else + { + osg::ref_ptr stateset = new osg::StateSet; + + osg::ref_ptr mat = createUnlitMaterial(); + + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mColor); + + stateset->setAttributeAndModes(mat); + + cv->pushStateSet(stateset); + traverse(node, cv); + cv->popStateSet(); + } + } + + void setTimeOfDayFade(float val) + { + mTimeOfDayFade = val; + } + + void setGlareView(float glareView) + { + mGlareView = glareView; + } + + private: + float getAngleToSunInRadians(const osg::Matrix& viewMatrix) const + { + osg::Vec3d eye, center, up; + viewMatrix.getLookAt(eye, center, up); + + osg::Vec3d forward = center - eye; + osg::Vec3d sun = mSunTransform->getPosition(); + + forward.normalize(); + sun.normalize(); + float angleRadians = std::acos(forward * sun); + return angleRadians; + } + + osg::ref_ptr mSunTransform; + float mTimeOfDayFade; + float mGlareView; + osg::Vec4f mColor; + float mSunGlareFaderMax; + float mSunGlareFaderAngleMax; + }; + + struct MoonUpdater : SceneUtil::StateSetUpdater + { + Resource::ImageManager& mImageManager; + osg::ref_ptr mPhaseTex; + osg::ref_ptr mCircleTex; + float mTransparency; + float mShadowBlend; + osg::Vec4f mAtmosphereColor; + osg::Vec4f mMoonColor; + bool mForceShaders; + + MoonUpdater(Resource::ImageManager& imageManager, bool forceShaders) + : mImageManager(imageManager) + , mPhaseTex() + , mCircleTex() + , mTransparency(1.0f) + , mShadowBlend(1.0f) + , mAtmosphereColor(1.0f, 1.0f, 1.0f, 1.0f) + , mMoonColor(1.0f, 1.0f, 1.0f, 1.0f) + , mForceShaders(forceShaders) + { } + + void setDefaults(osg::StateSet* stateset) override + { + if (mForceShaders) + { + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Moon))); + stateset->setTextureAttributeAndModes(0, mPhaseTex); + stateset->setTextureAttributeAndModes(1, mCircleTex); + stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("moonBlend", osg::Vec4f{})); + stateset->addUniform(new osg::Uniform("atmosphereFade", osg::Vec4f{})); + stateset->addUniform(new osg::Uniform("diffuseMap", 0)); + stateset->addUniform(new osg::Uniform("maskMap", 1)); + stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + else + { + stateset->setTextureAttributeAndModes(0, mPhaseTex); + osg::ref_ptr texEnv = new osg::TexEnvCombine; + texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); + texEnv->setSource0_RGB(osg::TexEnvCombine::CONSTANT); + texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); + texEnv->setConstantColor(osg::Vec4f(1.f, 0.f, 0.f, 1.f)); // mShadowBlend * mMoonColor + stateset->setTextureAttributeAndModes(0, texEnv); + + stateset->setTextureAttributeAndModes(1, mCircleTex); + osg::ref_ptr texEnv2 = new osg::TexEnvCombine; + texEnv2->setCombine_RGB(osg::TexEnvCombine::ADD); + texEnv2->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnv2->setSource0_Alpha(osg::TexEnvCombine::TEXTURE); + texEnv2->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnv2->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + texEnv2->setSource1_RGB(osg::TexEnvCombine::CONSTANT); + texEnv2->setConstantColor(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); // mAtmosphereColor.rgb, mTransparency + stateset->setTextureAttributeAndModes(1, texEnv2); + stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + { + if (mForceShaders) + { + stateset->setTextureAttribute(0, mPhaseTex, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureAttribute(1, mCircleTex, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + if (auto* uMoonBlend = stateset->getUniform("moonBlend")) + uMoonBlend->set(mMoonColor * mShadowBlend); + if (auto* uAtmosphereFade = stateset->getUniform("atmosphereFade")) + uAtmosphereFade->set(osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); + } + else + { + osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(mMoonColor * mShadowBlend); + + osg::TexEnvCombine* texEnv2 = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv2->setConstantColor(osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); + } + } + + void setTextures(const std::string& phaseTex, const std::string& circleTex) + { + mPhaseTex = new osg::Texture2D(mImageManager.getImage(phaseTex)); + mPhaseTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mPhaseTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + mCircleTex = new osg::Texture2D(mImageManager.getImage(circleTex)); + mCircleTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mCircleTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + + reset(); + } + }; + + class CameraRelativeTransformCullCallback : public SceneUtil::NodeCallback + { + public: + void operator() (osg::Node* node, osgUtil::CullVisitor* cv) + { + // XXX have to remove unwanted culling plane of the water reflection camera + + // Remove all planes that aren't from the standard frustum + unsigned int numPlanes = 4; + if (cv->getCullingMode() & osg::CullSettings::NEAR_PLANE_CULLING) + ++numPlanes; + if (cv->getCullingMode() & osg::CullSettings::FAR_PLANE_CULLING) + ++numPlanes; + + unsigned int mask = 0x1; + unsigned int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); + for (unsigned int i=0; igetProjectionCullingStack().back().getFrustum().getPlaneList().size(); ++i) + { + if (i >= numPlanes) + { + // turn off this culling plane + resultMask &= (~mask); + } + + mask <<= 1; + } + + cv->getProjectionCullingStack().back().getFrustum().setResultMask(resultMask); + cv->getCurrentCullingSet().getFrustum().setResultMask(resultMask); + + cv->getProjectionCullingStack().back().pushCurrentMask(); + cv->getCurrentCullingSet().pushCurrentMask(); + + traverse(node, cv); + + cv->getProjectionCullingStack().back().popCurrentMask(); + cv->getCurrentCullingSet().popCurrentMask(); + } + }; + + void AtmosphereUpdater::setEmissionColor(const osg::Vec4f& emissionColor) + { + mEmissionColor = emissionColor; + } + + void AtmosphereUpdater::setDefaults(osg::StateSet* stateset) + { + stateset->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Atmosphere))); + } + + void AtmosphereUpdater::apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) + { + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); + } + + AtmosphereNightUpdater::AtmosphereNightUpdater(Resource::ImageManager* imageManager, bool forceShaders) + : mColor(osg::Vec4f(0,0,0,0)) + , mTexture(new osg::Texture2D(imageManager->getWarningImage())) + , mForceShaders(forceShaders) + { } + + void AtmosphereNightUpdater::setFade(float fade) + { + mColor.a() = fade; + } + + void AtmosphereNightUpdater::setDefaults(osg::StateSet* stateset) + { + if (mForceShaders) + { + stateset->addUniform(new osg::Uniform("opacity", 0.f)); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Atmosphere_Night))); + } + else + { + osg::ref_ptr texEnv = new osg::TexEnvCombine; + texEnv->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnv->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnv->setCombine_RGB(osg::TexEnvCombine::REPLACE); + texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + + stateset->setTextureAttributeAndModes(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureAttributeAndModes(1, texEnv, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + } + + void AtmosphereNightUpdater::apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) + { + if (mForceShaders) + { + stateset->getUniform("opacity")->set(mColor.a()); + } + else + { + osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(mColor); + } + } + + CloudUpdater::CloudUpdater(bool forceShaders) + : mOpacity(0.f) + , mForceShaders(forceShaders) + { } + + void CloudUpdater::setTexture(osg::ref_ptr texture) + { + mTexture = texture; + } + + void CloudUpdater::setEmissionColor(const osg::Vec4f& emissionColor) + { + mEmissionColor = emissionColor; + } + + void CloudUpdater::setOpacity(float opacity) + { + mOpacity = opacity; + } + + void CloudUpdater::setTextureCoord(float timer) + { + mTexMat = osg::Matrixf::translate(osg::Vec3f(0.f, -timer, 0.f)); + } + + void CloudUpdater::setDefaults(osg::StateSet *stateset) + { + stateset->setAttribute(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + osg::ref_ptr texmat = new osg::TexMat; + stateset->setTextureAttributeAndModes(0, texmat); + + if (mForceShaders) + { + stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + stateset->addUniform(new osg::Uniform("opacity", 1.f)); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Clouds))); + } + else + { + stateset->setTextureAttributeAndModes(1, texmat); + // need to set opacity on a separate texture unit, diffuse alpha is used by the vertex colors already + osg::ref_ptr texEnvCombine = new osg::TexEnvCombine; + texEnvCombine->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + texEnvCombine->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnvCombine->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,1)); + texEnvCombine->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnvCombine->setCombine_RGB(osg::TexEnvCombine::REPLACE); + + stateset->setTextureAttributeAndModes(1, texEnvCombine); + + stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + } + + void CloudUpdater::apply(osg::StateSet *stateset, osg::NodeVisitor *nv) + { + stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); + + osg::TexMat* texMat = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXMAT)); + texMat->setMatrix(mTexMat); + + if (mForceShaders) + { + stateset->getUniform("opacity")->set(mOpacity); + } + else + { + stateset->setTextureAttribute(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(osg::Vec4f(1,1,1,mOpacity)); + } + } + + CameraRelativeTransform::CameraRelativeTransform() + { + // Culling works in node-local space, not in camera space, so we can't cull this node correctly + // That's not a problem though, children of this node can be culled just fine + // Just make sure you do not place a CameraRelativeTransform deep in the scene graph + setCullingActive(false); + + addCullCallback(new CameraRelativeTransformCullCallback); + } + + CameraRelativeTransform::CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop) + : osg::Transform(copy, copyop) + { } + + const osg::Vec3f& CameraRelativeTransform::getLastViewPoint() const + { + return mViewPoint; + } + + bool CameraRelativeTransform::computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const + { + if (nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR) + { + mViewPoint = static_cast(nv)->getViewPoint(); + } + + if (_referenceFrame==RELATIVE_RF) + { + matrix.setTrans(osg::Vec3f(0.f,0.f,0.f)); + return false; + } + else // absolute + { + matrix.makeIdentity(); + return true; + } + } + + osg::BoundingSphere CameraRelativeTransform::computeBound() const + { + return osg::BoundingSphere(); + } + + UnderwaterSwitchCallback::UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform) + : mCameraRelativeTransform(cameraRelativeTransform) + , mEnabled(true) + , mWaterLevel(0.f) + { } + + bool UnderwaterSwitchCallback::isUnderwater() + { + osg::Vec3f viewPoint = mCameraRelativeTransform->getLastViewPoint(); + return mEnabled && viewPoint.z() < mWaterLevel; + } + + void UnderwaterSwitchCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) + { + if (isUnderwater()) + return; + + traverse(node, nv); + } + + void UnderwaterSwitchCallback::setEnabled(bool enabled) + { + mEnabled = enabled; + } + void UnderwaterSwitchCallback::setWaterLevel(float waterLevel) + { + mWaterLevel = waterLevel; + } + + const float CelestialBody::mDistance = 1000.0f; + + CelestialBody::CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask) + : mVisibleMask(visibleMask) + { + mGeom = createTexturedQuad(numUvSets); + mGeom->getOrCreateStateSet(); + mTransform = new osg::PositionAttitudeTransform; + mTransform->setNodeMask(mVisibleMask); + mTransform->setScale(osg::Vec3f(450,450,450) * scaleFactor); + mTransform->addChild(mGeom); + + parentNode->addChild(mTransform); + } + + void CelestialBody::setVisible(bool visible) + { + mTransform->setNodeMask(visible ? mVisibleMask : 0); + } + + Sun::Sun(osg::Group* parentNode, Resource::ImageManager& imageManager) + : CelestialBody(parentNode, 1.0f, 1, Mask_Sun) + , mUpdater(new SunUpdater) + { + mTransform->addUpdateCallback(mUpdater); + + osg::ref_ptr sunTex = new osg::Texture2D(imageManager.getImage("textures/tx_sun_05.dds")); + sunTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + sunTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + sunTex->setName("diffuseMap"); + + mGeom->getOrCreateStateSet()->setTextureAttributeAndModes(0, sunTex); + mGeom->getOrCreateStateSet()->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun))); + + osg::ref_ptr queryNode = new osg::Group; + // Need to render after the world geometry so we can correctly test for occlusions + osg::StateSet* stateset = queryNode->getOrCreateStateSet(); + stateset->setRenderBinDetails(RenderBin_OcclusionQuery, "RenderBin"); + stateset->setNestRenderBins(false); + // Set up alpha testing on the occlusion testing subgraph, that way we can get the occlusion tested fragments to match the circular shape of the sun + osg::ref_ptr alphaFunc = new osg::AlphaFunc; + alphaFunc->setFunction(osg::AlphaFunc::GREATER, 0.8); + stateset->setAttributeAndModes(alphaFunc); + stateset->setTextureAttributeAndModes(0, sunTex); + stateset->setAttributeAndModes(createUnlitMaterial()); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sunflash_Query))); + + // Disable writing to the color buffer. We are using this geometry for visibility tests only. + osg::ref_ptr colormask = new osg::ColorMask(0, 0, 0, 0); + stateset->setAttributeAndModes(colormask); + + mTransform->addChild(queryNode); + + mOcclusionQueryVisiblePixels = createOcclusionQueryNode(queryNode, true); + mOcclusionQueryTotalPixels = createOcclusionQueryNode(queryNode, false); + + createSunFlash(imageManager); + createSunGlare(); + } + + Sun::~Sun() + { + mTransform->removeUpdateCallback(mUpdater); + destroySunFlash(); + destroySunGlare(); + } + + void Sun::setColor(const osg::Vec4f& color) + { + mUpdater->mColor.r() = color.r(); + mUpdater->mColor.g() = color.g(); + mUpdater->mColor.b() = color.b(); + } + + void Sun::adjustTransparency(const float ratio) + { + mUpdater->mColor.a() = ratio; + if (mSunGlareCallback) + mSunGlareCallback->setGlareView(ratio); + if (mSunFlashCallback) + mSunFlashCallback->setGlareView(ratio); + } + + void Sun::setDirection(const osg::Vec3f& direction) + { + osg::Vec3f normalizedDirection = direction / direction.length(); + mTransform->setPosition(normalizedDirection * mDistance); + + osg::Quat quat; + quat.makeRotate(osg::Vec3f(0.0f, 0.0f, 1.0f), normalizedDirection); + mTransform->setAttitude(quat); + } + + void Sun::setGlareTimeOfDayFade(float val) + { + if (mSunGlareCallback) + mSunGlareCallback->setTimeOfDayFade(val); + } + + osg::ref_ptr Sun::createOcclusionQueryNode(osg::Group* parent, bool queryVisible) + { + osg::ref_ptr oqn = new osg::OcclusionQueryNode; + oqn->setQueriesEnabled(true); + +#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) + // With OSG 3.6.5, the method of providing user defined query geometry has been completely replaced + osg::ref_ptr queryGeom = new osg::QueryGeometry(oqn->getName()); +#else + osg::ref_ptr queryGeom = oqn->getQueryGeometry(); +#endif + + // Make it fast! A DYNAMIC query geometry means we can't break frame until the flare is rendered (which is rendered after all the other geometry, + // so that would be pretty bad). STATIC should be safe, since our node's local bounds are static, thus computeBounds() which modifies the queryGeometry + // is only called once. + // Note the debug geometry setDebugDisplay(true) is always DYNAMIC and that can't be changed, not a big deal. + queryGeom->setDataVariance(osg::Object::STATIC); + + // Set up the query geometry to match the actual sun's rendering shape. osg::OcclusionQueryNode wasn't originally intended to allow this, + // normally it would automatically adjust the query geometry to match the sub graph's bounding box. The below hack is needed to + // circumvent this. + queryGeom->setVertexArray(mGeom->getVertexArray()); + queryGeom->setTexCoordArray(0, mGeom->getTexCoordArray(0), osg::Array::BIND_PER_VERTEX); + queryGeom->removePrimitiveSet(0, queryGeom->getNumPrimitiveSets()); + queryGeom->addPrimitiveSet(mGeom->getPrimitiveSet(0)); + + // Hack to disable unwanted awful code inside OcclusionQueryNode::computeBound. + oqn->setComputeBoundingSphereCallback(new DummyComputeBoundCallback); + // Still need a proper bounding sphere. + oqn->setInitialBound(queryGeom->getBound()); + +#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) + oqn->setQueryGeometry(queryGeom.release()); +#endif + + osg::StateSet* queryStateSet = new osg::StateSet; + if (queryVisible) + { + auto depth = SceneUtil::createDepth(); + // This is a trick to make fragments written by the query always use the maximum depth value, + // without having to retrieve the current far clipping distance. + // We want the sun glare to be "infinitely" far away. + double far = SceneUtil::getReverseZ() ? 0.0 : 1.0; + depth->setZNear(far); + depth->setZFar(far); + depth->setWriteMask(false); + queryStateSet->setAttributeAndModes(depth); + } + else + { + queryStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + } + oqn->setQueryStateSet(queryStateSet); + + parent->addChild(oqn); + + return oqn; + } + + void Sun::createSunFlash(Resource::ImageManager& imageManager) + { + osg::ref_ptr tex = new osg::Texture2D(imageManager.getImage("textures/tx_sun_flash_grey_05.dds")); + tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + tex->setName("diffuseMap"); + + osg::ref_ptr group (new osg::Group); + + mTransform->addChild(group); + + const float scale = 2.6f; + osg::ref_ptr geom = createTexturedQuad(1, scale); + group->addChild(geom); + + osg::StateSet* stateset = geom->getOrCreateStateSet(); + + stateset->setTextureAttributeAndModes(0, tex); + stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); + stateset->setNestRenderBins(false); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun))); + + mSunFlashNode = group; + + mSunFlashCallback = new SunFlashCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels); + mSunFlashNode->addCullCallback(mSunFlashCallback); + } + + void Sun::destroySunFlash() + { + if (mSunFlashNode) + { + mSunFlashNode->removeCullCallback(mSunFlashCallback); + mSunFlashCallback = nullptr; + } + } + + void Sun::createSunGlare() + { + osg::ref_ptr camera = new osg::Camera; + camera->setProjectionMatrix(osg::Matrix::identity()); + camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); // add to skyRoot instead? + camera->setViewMatrix(osg::Matrix::identity()); + camera->setClearMask(0); + camera->setRenderOrder(osg::Camera::NESTED_RENDER); + camera->setAllowEventFocus(false); + + osg::ref_ptr geom = osg::createTexturedQuadGeometry(osg::Vec3f(-1,-1,0), osg::Vec3f(2,0,0), osg::Vec3f(0,2,0)); + camera->addChild(geom); + + osg::StateSet* stateset = geom->getOrCreateStateSet(); + + stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); + stateset->setNestRenderBins(false); + stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sunglare))); + + // set up additive blending + osg::ref_ptr blendFunc = new osg::BlendFunc; + blendFunc->setSource(osg::BlendFunc::SRC_ALPHA); + blendFunc->setDestination(osg::BlendFunc::ONE); + stateset->setAttributeAndModes(blendFunc); + + mSunGlareCallback = new SunGlareCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels, mTransform); + mSunGlareNode = camera; + + mSunGlareNode->addCullCallback(mSunGlareCallback); + + mTransform->addChild(camera); + } + + void Sun::destroySunGlare() + { + if (mSunGlareNode) + { + mSunGlareNode->removeCullCallback(mSunGlareCallback); + mSunGlareCallback = nullptr; + } + } + + Moon::Moon(osg::Group* parentNode, Resource::SceneManager& sceneManager, float scaleFactor, Type type) + : CelestialBody(parentNode, scaleFactor, 2) + , mType(type) + , mPhase(MoonState::Phase::Unspecified) + , mUpdater(new MoonUpdater(*sceneManager.getImageManager(), sceneManager.getForceShaders())) + { + setPhase(MoonState::Phase::Full); + setVisible(true); + + mGeom->addUpdateCallback(mUpdater); + } + + Moon::~Moon() + { + mGeom->removeUpdateCallback(mUpdater); + } + + void Moon::adjustTransparency(const float ratio) + { + mUpdater->mTransparency *= ratio; + } + + void Moon::setState(const MoonState state) + { + float radsX = ((state.mRotationFromHorizon) * static_cast(osg::PI)) / 180.0f; + float radsZ = ((state.mRotationFromNorth) * static_cast(osg::PI)) / 180.0f; + + osg::Quat rotX(radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); + osg::Quat rotZ(radsZ, osg::Vec3f(0.0f, 0.0f, 1.0f)); + + osg::Vec3f direction = rotX * rotZ * osg::Vec3f(0.0f, 1.0f, 0.0f); + mTransform->setPosition(direction * mDistance); + + // The moon quad is initially oriented facing down, so we need to offset its X-axis + // rotation to rotate it to face the camera when sitting at the horizon. + osg::Quat attX((-static_cast(osg::PI) / 2.0f) + radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); + mTransform->setAttitude(attX * rotZ); + + setPhase(state.mPhase); + mUpdater->mTransparency = state.mMoonAlpha; + mUpdater->mShadowBlend = state.mShadowBlend; + } + + void Moon::setAtmosphereColor(const osg::Vec4f& color) + { + mUpdater->mAtmosphereColor = color; + } + + void Moon::setColor(const osg::Vec4f& color) + { + mUpdater->mMoonColor = color; + } + + unsigned int Moon::getPhaseInt() const + { + switch (mPhase) + { + case MoonState::Phase::New: + return 0; + case MoonState::Phase::WaxingCrescent: + return 1; + case MoonState::Phase::WaningCrescent: + return 1; + case MoonState::Phase::FirstQuarter: + return 2; + case MoonState::Phase::ThirdQuarter: + return 2; + case MoonState::Phase::WaxingGibbous: + return 3; + case MoonState::Phase::WaningGibbous: + return 3; + case MoonState::Phase::Full: + return 4; + default: + return 0; + } + } + + void Moon::setPhase(const MoonState::Phase& phase) + { + if(mPhase == phase) + return; + + mPhase = phase; + + std::string textureName = "textures/tx_"; + + if (mType == Moon::Type_Secunda) + textureName += "secunda_"; + else + textureName += "masser_"; + + switch (mPhase) + { + case MoonState::Phase::New: + textureName += "new"; + break; + case MoonState::Phase::WaxingCrescent: + textureName += "one_wax"; + break; + case MoonState::Phase::FirstQuarter: + textureName += "half_wax"; + break; + case MoonState::Phase::WaxingGibbous: + textureName += "three_wax"; + break; + case MoonState::Phase::WaningCrescent: + textureName += "one_wan"; + break; + case MoonState::Phase::ThirdQuarter: + textureName += "half_wan"; + break; + case MoonState::Phase::WaningGibbous: + textureName += "three_wan"; + break; + case MoonState::Phase::Full: + textureName += "full"; + break; + default: + break; + } + + textureName += ".dds"; + + if (mType == Moon::Type_Secunda) + mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_s.dds"); + else + mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_m.dds"); + } + + int RainCounter::numParticlesToCreate(double dt) const + { + // limit dt to avoid large particle emissions if there are jumps in the simulation time + // 0.2 seconds is the same cap as used in Engine's frame loop + dt = std::min(dt, 0.2); + return ConstantRateCounter::numParticlesToCreate(dt); + } + + RainShooter::RainShooter() + : mAngle(0.f) + { } + + void RainShooter::shoot(osgParticle::Particle* particle) const + { + particle->setVelocity(mVelocity); + particle->setAngle(osg::Vec3f(-mAngle, 0, (Misc::Rng::rollProbability() * 2 - 1) * osg::PI)); + } + + void RainShooter::setVelocity(const osg::Vec3f& velocity) + { + mVelocity = velocity; + } + + void RainShooter::setAngle(float angle) + { + mAngle = angle; + } + + osg::Object* RainShooter::cloneType() const + { + return new RainShooter; + } + + osg::Object* RainShooter::clone(const osg::CopyOp &) const + { + return new RainShooter(*this); + } + + ModVertexAlphaVisitor::ModVertexAlphaVisitor(ModVertexAlphaVisitor::MeshType type) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mType(type) + { } + + void ModVertexAlphaVisitor::apply(osg::Geometry& geometry) + { + osg::ref_ptr colors = new osg::Vec4Array(geometry.getVertexArray()->getNumElements()); + for (unsigned int i=0; isize(); ++i) + { + float alpha = 1.f; + + switch (mType) + { + case ModVertexAlphaVisitor::Atmosphere: + { + // this is a cylinder, so every second vertex belongs to the bottom-most row + alpha = (i%2) ? 0.f : 1.f; + break; + } + case ModVertexAlphaVisitor::Clouds: + { + if (i>= 49 && i <= 64) + alpha = 0.f; // bottom-most row + else if (i>= 33 && i <= 48) + alpha = 0.25098; // second row + else + alpha = 1.f; + break; + } + case ModVertexAlphaVisitor::Stars: + { + if (geometry.getColorArray()) + { + osg::Vec4Array* origColors = static_cast(geometry.getColorArray()); + alpha = ((*origColors)[i].x() == 1.f) ? 1.f : 0.f; + } + else + alpha = 1.f; + break; + } + } + + (*colors)[i] = osg::Vec4f(0.f, 0.f, 0.f, alpha); + } + + geometry.setColorArray(colors, osg::Array::BIND_PER_VERTEX); + } +} diff --git a/apps/openmw/mwrender/skyutil.hpp b/apps/openmw/mwrender/skyutil.hpp new file mode 100644 index 0000000000..c2272143a0 --- /dev/null +++ b/apps/openmw/mwrender/skyutil.hpp @@ -0,0 +1,343 @@ +#ifndef OPENMW_MWRENDER_SKYUTIL_H +#define OPENMW_MWRENDER_SKYUTIL_H + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace Resource +{ + class ImageManager; + class SceneManager; +} + +namespace MWRender +{ + struct MoonUpdater; + class SunUpdater; + class SunFlashCallback; + class SunGlareCallback; + + struct WeatherResult + { + std::string mCloudTexture; + std::string mNextCloudTexture; + float mCloudBlendFactor; + + osg::Vec4f mFogColor; + + osg::Vec4f mAmbientColor; + + osg::Vec4f mSkyColor; + + // sun light color + osg::Vec4f mSunColor; + + // alpha is the sun transparency + osg::Vec4f mSunDiscColor; + + float mFogDepth; + + float mDLFogFactor; + float mDLFogOffset; + + float mWindSpeed; + float mBaseWindSpeed; + float mCurrentWindSpeed; + float mNextWindSpeed; + + float mCloudSpeed; + + float mGlareView; + + bool mNight; // use night skybox + float mNightFade; // fading factor for night skybox + + bool mIsStorm; + + std::string mAmbientLoopSoundID; + float mAmbientSoundVolume; + + std::string mParticleEffect; + std::string mRainEffect; + float mPrecipitationAlpha; + + float mRainDiameter; + float mRainMinHeight; + float mRainMaxHeight; + float mRainSpeed; + float mRainEntranceSpeed; + int mRainMaxRaindrops; + + osg::Vec3f mStormDirection; + osg::Vec3f mNextStormDirection; + }; + + struct MoonState + { + enum class Phase + { + Full, + WaningGibbous, + ThirdQuarter, + WaningCrescent, + New, + WaxingCrescent, + FirstQuarter, + WaxingGibbous, + Unspecified + }; + + float mRotationFromHorizon; + float mRotationFromNorth; + Phase mPhase; + float mShadowBlend; + float mMoonAlpha; + }; + + osg::ref_ptr createAlphaTrackingUnlitMaterial(); + osg::ref_ptr createUnlitMaterial(osg::Material::ColorMode colorMode = osg::Material::OFF); + + class OcclusionCallback + { + public: + OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal); + + protected: + float getVisibleRatio (osg::Camera* camera); + + private: + osg::ref_ptr mOcclusionQueryVisiblePixels; + osg::ref_ptr mOcclusionQueryTotalPixels; + + std::map, float> mLastRatio; + }; + + class AtmosphereUpdater : public SceneUtil::StateSetUpdater + { + public: + void setEmissionColor(const osg::Vec4f& emissionColor); + + protected: + void setDefaults(osg::StateSet* stateset) override; + void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override; + + private: + osg::Vec4f mEmissionColor; + }; + + class AtmosphereNightUpdater : public SceneUtil::StateSetUpdater + { + public: + AtmosphereNightUpdater(Resource::ImageManager* imageManager, bool forceShaders); + + void setFade(float fade); + + protected: + void setDefaults(osg::StateSet* stateset) override; + + void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override; + + private: + osg::Vec4f mColor; + osg::ref_ptr mTexture; + bool mForceShaders; + }; + + class CloudUpdater : public SceneUtil::StateSetUpdater + { + public: + CloudUpdater(bool forceShaders); + + void setTexture(osg::ref_ptr texture); + + void setEmissionColor(const osg::Vec4f& emissionColor); + void setOpacity(float opacity); + void setTextureCoord(float timer); + + protected: + void setDefaults(osg::StateSet *stateset) override; + void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override; + + private: + osg::ref_ptr mTexture; + osg::Vec4f mEmissionColor; + float mOpacity; + bool mForceShaders; + osg::Matrixf mTexMat; + }; + + /// Transform that removes the eyepoint of the modelview matrix, + /// i.e. its children are positioned relative to the camera. + class CameraRelativeTransform : public osg::Transform + { + public: + CameraRelativeTransform(); + + CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop); + + META_Node(MWRender, CameraRelativeTransform) + + const osg::Vec3f& getLastViewPoint() const; + + bool computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const override; + + osg::BoundingSphere computeBound() const override; + + private: + // viewPoint for the current frame + mutable osg::Vec3f mViewPoint; + }; + + /// @brief Hides the node subgraph if the eye point is below water. + /// @note Must be added as cull callback. + /// @note Meant to be used on a node that is child of a CameraRelativeTransform. + /// The current view point must be retrieved by the CameraRelativeTransform since we can't get it anymore once we are in camera-relative space. + class UnderwaterSwitchCallback : public SceneUtil::NodeCallback + { + public: + UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform); + bool isUnderwater(); + + void operator()(osg::Node* node, osg::NodeVisitor* nv); + void setEnabled(bool enabled); + void setWaterLevel(float waterLevel); + + private: + osg::ref_ptr mCameraRelativeTransform; + bool mEnabled; + float mWaterLevel; + }; + + /// A base class for the sun and moons. + class CelestialBody + { + public: + CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask=~0u); + + virtual ~CelestialBody() = default; + + virtual void adjustTransparency(const float ratio) = 0; + + void setVisible(bool visible); + + protected: + unsigned int mVisibleMask; + static const float mDistance; + osg::ref_ptr mTransform; + osg::ref_ptr mGeom; + }; + + class Sun : public CelestialBody + { + public: + Sun(osg::Group* parentNode, Resource::ImageManager& imageManager); + + ~Sun(); + + void setColor(const osg::Vec4f& color); + void adjustTransparency(const float ratio) override; + + void setDirection(const osg::Vec3f& direction); + void setGlareTimeOfDayFade(float val); + + private: + /// @param queryVisible If true, queries the amount of visible pixels. If false, queries the total amount of pixels. + osg::ref_ptr createOcclusionQueryNode(osg::Group* parent, bool queryVisible); + + void createSunFlash(Resource::ImageManager& imageManager); + void destroySunFlash(); + + void createSunGlare(); + void destroySunGlare(); + + osg::ref_ptr mUpdater; + osg::ref_ptr mSunFlashNode; + osg::ref_ptr mSunGlareNode; + osg::ref_ptr mSunFlashCallback; + osg::ref_ptr mSunGlareCallback; + osg::ref_ptr mOcclusionQueryVisiblePixels; + osg::ref_ptr mOcclusionQueryTotalPixels; + }; + + class Moon : public CelestialBody + { + public: + enum Type + { + Type_Masser = 0, + Type_Secunda + }; + + Moon(osg::Group* parentNode, Resource::SceneManager& sceneManager, float scaleFactor, Type type); + + ~Moon(); + + void adjustTransparency(const float ratio) override; + void setState(const MoonState state); + void setAtmosphereColor(const osg::Vec4f& color); + void setColor(const osg::Vec4f& color); + + unsigned int getPhaseInt() const; + + private: + Type mType; + MoonState::Phase mPhase; + osg::ref_ptr mUpdater; + + void setPhase(const MoonState::Phase& phase); + }; + + class RainCounter : public osgParticle::ConstantRateCounter + { + public: + int numParticlesToCreate(double dt) const override; + }; + + class RainShooter : public osgParticle::Shooter + { + public: + RainShooter(); + + osg::Object* cloneType() const override; + + osg::Object* clone(const osg::CopyOp &) const override; + + void shoot(osgParticle::Particle* particle) const override; + + void setVelocity(const osg::Vec3f& velocity); + void setAngle(float angle); + + private: + osg::Vec3f mVelocity; + float mAngle; + }; + + class ModVertexAlphaVisitor : public osg::NodeVisitor + { + public: + enum MeshType + { + Atmosphere, + Stars, + Clouds + }; + + ModVertexAlphaVisitor(MeshType type); + + void apply(osg::Geometry& geometry) override; + + private: + MeshType mType; + }; +} + +#endif diff --git a/apps/openmw/mwrender/vismask.hpp b/apps/openmw/mwrender/vismask.hpp index 87ca9415fa..a7a28614cb 100644 --- a/apps/openmw/mwrender/vismask.hpp +++ b/apps/openmw/mwrender/vismask.hpp @@ -58,6 +58,9 @@ namespace MWRender Mask_Groundcover = (1<<20), }; + // Defines masks to remove when using ToggleWorld command + constexpr static unsigned int sToggleWorldMask = Mask_Debug | Mask_Actor | Mask_Terrain | Mask_Object | Mask_Static | Mask_Groundcover; + } #endif diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 5ae71b9979..ca42423efe 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -261,6 +261,7 @@ class Refraction : public SceneUtil::RTTNode public: Refraction(uint32_t rttSize) : RTTNode(rttSize, rttSize, 1, false) + , mNodeMask(Refraction::sDefaultCullMask) { mClipCullNode = new ClipCullNode; } @@ -274,8 +275,6 @@ public: camera->addCullCallback(new InheritViewPointCallback); camera->setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); - camera->setCullMask(Mask_Effect | Mask_Scene | Mask_Object | Mask_Static | Mask_Terrain | Mask_Actor | Mask_ParticleSystem | Mask_Sky | Mask_Sun | Mask_Player | Mask_Lighting | Mask_Groundcover); - // No need for fog here, we are already applying fog on the water surface itself as well as underwater fog // assign large value to effectively turn off fog // shaders don't respect glDisable(GL_FOG) @@ -294,6 +293,7 @@ public: void apply(osg::Camera* camera) override { camera->setViewMatrix(mViewMatrix); + camera->setCullMask(mNodeMask); } void setScene(osg::Node* scene) @@ -306,8 +306,7 @@ public: void setWaterLevel(float waterLevel) { - const float refractionScale = std::min(1.0f, std::max(0.0f, - Settings::Manager::getFloat("refraction scale", "Water"))); + const float refractionScale = std::clamp(Settings::Manager::getFloat("refraction scale", "Water"), 0.f, 1.f); mViewMatrix = osg::Matrix::scale(1, 1, refractionScale) * osg::Matrix::translate(0, 0, (1.0 - refractionScale) * waterLevel); @@ -315,10 +314,22 @@ public: mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0, 0, -1), osg::Vec3d(0, 0, waterLevel))); } + void showWorld(bool show) + { + if (show) + mNodeMask = Refraction::sDefaultCullMask; + else + mNodeMask = Refraction::sDefaultCullMask & ~sToggleWorldMask; + } + private: osg::ref_ptr mClipCullNode; osg::ref_ptr mScene; osg::Matrix mViewMatrix{ osg::Matrix::identity() }; + + unsigned int mNodeMask; + + static constexpr unsigned int sDefaultCullMask = Mask_Effect | Mask_Scene | Mask_Object | Mask_Static | Mask_Terrain | Mask_Actor | Mask_ParticleSystem | Mask_Sky | Mask_Sun | Mask_Player | Mask_Lighting | Mask_Groundcover; }; class Reflection : public SceneUtil::RTTNode @@ -358,15 +369,8 @@ public: void setInterior(bool isInterior) { - int reflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); - reflectionDetail = std::min(5, std::max(isInterior ? 2 : 0, reflectionDetail)); - unsigned int extraMask = 0; - if(reflectionDetail >= 1) extraMask |= Mask_Terrain; - if(reflectionDetail >= 2) extraMask |= Mask_Static; - if(reflectionDetail >= 3) extraMask |= Mask_Effect | Mask_ParticleSystem | Mask_Object; - if(reflectionDetail >= 4) extraMask |= Mask_Player | Mask_Actor; - if(reflectionDetail >= 5) extraMask |= Mask_Groundcover; - mNodeMask = Mask_Scene | Mask_Sky | Mask_Lighting | extraMask; + mInterior = isInterior; + mNodeMask = calcNodeMask(); } void setWaterLevel(float waterLevel) @@ -383,11 +387,34 @@ public: mClipCullNode->addChild(scene); } + void showWorld(bool show) + { + if (show) + mNodeMask = calcNodeMask(); + else + mNodeMask = calcNodeMask() & ~sToggleWorldMask; + } + private: + + unsigned int calcNodeMask() + { + int reflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); + reflectionDetail = std::clamp(reflectionDetail, mInterior ? 2 : 0, 5); + unsigned int extraMask = 0; + if(reflectionDetail >= 1) extraMask |= Mask_Terrain; + if(reflectionDetail >= 2) extraMask |= Mask_Static; + if(reflectionDetail >= 3) extraMask |= Mask_Effect | Mask_ParticleSystem | Mask_Object; + if(reflectionDetail >= 4) extraMask |= Mask_Player | Mask_Actor; + if(reflectionDetail >= 5) extraMask |= Mask_Groundcover; + return Mask_Scene | Mask_Sky | Mask_Lighting | extraMask; + } + osg::ref_ptr mClipCullNode; osg::ref_ptr mScene; osg::Node::NodeMask mNodeMask; osg::Matrix mViewMatrix{ osg::Matrix::identity() }; + bool mInterior; }; /// DepthClampCallback enables GL_DEPTH_CLAMP for the current draw, if supported. @@ -423,6 +450,7 @@ Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem , mToggled(true) , mTop(0) , mInterior(false) + , mShowWorld(true) , mCullCallback(nullptr) , mShaderWaterStateSetUpdater(nullptr) { @@ -520,6 +548,8 @@ void Water::updateWaterMaterial() mParent->addChild(mRefraction); } + showWorld(mShowWorld); + createShaderWaterStateSet(mWaterNode, mReflection, mRefraction); } else @@ -553,7 +583,7 @@ void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) // Add animated textures std::vector > textures; - int frameCount = std::max(0, std::min(Fallback::Map::getInt("Water_SurfaceFrameCount"), 320)); + const int frameCount = std::clamp(Fallback::Map::getInt("Water_SurfaceFrameCount"), 0, 320); const std::string& texture = Fallback::Map::getString("Water_SurfaceTexture"); for (int i=0; i &textures) { - int frameCount = std::max(0, std::min(Fallback::Map::getInt("Water_SurfaceFrameCount"), 320)); + const int frameCount = std::clamp(Fallback::Map::getInt("Water_SurfaceFrameCount"), 0, 320); const std::string& texture = Fallback::Map::getString("Water_SurfaceTexture"); for (int i=0; iclear(); } +void Water::showWorld(bool show) +{ + if (mReflection) + mReflection->showWorld(show); + if (mRefraction) + mRefraction->showWorld(show); + mShowWorld = show; +} + } diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index 719e4fdc2b..c7acbf708f 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -71,6 +71,7 @@ namespace MWRender bool mToggled; float mTop; bool mInterior; + bool mShowWorld; osg::Callback* mCullCallback; osg::ref_ptr mShaderWaterStateSetUpdater; @@ -124,6 +125,8 @@ namespace MWRender osg::Vec3d getPosition() const; void processChangedSettings(const Settings::CategorySettingVector& settings); + + void showWorld(bool show); }; } diff --git a/apps/openmw/mwrender/weaponanimation.cpp b/apps/openmw/mwrender/weaponanimation.cpp index 3db415126b..0572eae3be 100644 --- a/apps/openmw/mwrender/weaponanimation.cpp +++ b/apps/openmw/mwrender/weaponanimation.cpp @@ -172,14 +172,13 @@ void WeaponAnimation::releaseArrow(MWWorld::Ptr actor, float attackStrength) } } -void WeaponAnimation::addControllers(const std::map >& nodes, - std::vector, osg::ref_ptr>> &map, osg::Node* objectRoot) +void WeaponAnimation::addControllers(const Animation::NodeMap& nodes, std::vector, osg::ref_ptr>> &map, osg::Node* objectRoot) { for (int i=0; i<2; ++i) { mSpineControllers[i] = nullptr; - std::map >::const_iterator found = nodes.find(i == 0 ? "bip01 spine1" : "bip01 spine2"); + Animation::NodeMap::const_iterator found = nodes.find(i == 0 ? "bip01 spine1" : "bip01 spine2"); if (found != nodes.end()) { osg::Node* node = found->second; diff --git a/apps/openmw/mwrender/weaponanimation.hpp b/apps/openmw/mwrender/weaponanimation.hpp index 1f614463a6..125587c1bd 100644 --- a/apps/openmw/mwrender/weaponanimation.hpp +++ b/apps/openmw/mwrender/weaponanimation.hpp @@ -42,8 +42,7 @@ namespace MWRender void releaseArrow(MWWorld::Ptr actor, float attackStrength); /// Add WeaponAnimation-related controllers to \a nodes and store the added controllers in \a map. - void addControllers(const std::map >& nodes, - std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot); + void addControllers(const Animation::NodeMap& nodes, std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot); void deleteControllers(); diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index c5a4bb6dfc..33f16aea3f 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -208,8 +208,7 @@ namespace MWScript { if(!repeat) repeat = true; - Interpreter::Type_Integer idleValue = runtime[0].mInteger; - idleValue = std::min(255, std::max(0, idleValue)); + Interpreter::Type_Integer idleValue = std::clamp(runtime[0].mInteger, 0, 255); idleList.push_back(idleValue); runtime.pop(); --arg0; diff --git a/apps/openmw/mwscript/compilercontext.cpp b/apps/openmw/mwscript/compilercontext.cpp index 4a7038e1cb..983365e06a 100644 --- a/apps/openmw/mwscript/compilercontext.cpp +++ b/apps/openmw/mwscript/compilercontext.cpp @@ -86,14 +86,4 @@ namespace MWScript store.get().search (name) || store.get().search (name); } - - bool CompilerContext::isJournalId (const std::string& name) const - { - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); - - const ESM::Dialogue *topic = store.get().search (name); - - return topic && topic->mType==ESM::Dialogue::Journal; - } } diff --git a/apps/openmw/mwscript/compilercontext.hpp b/apps/openmw/mwscript/compilercontext.hpp index 00b10ea06d..d800781fd8 100644 --- a/apps/openmw/mwscript/compilercontext.hpp +++ b/apps/openmw/mwscript/compilercontext.hpp @@ -39,9 +39,6 @@ namespace MWScript bool isId (const std::string& name) const override; ///< Does \a name match an ID, that can be referenced? - - bool isJournalId (const std::string& name) const override; - ///< Does \a name match a journal ID? }; } diff --git a/apps/openmw/mwscript/skyextensions.cpp b/apps/openmw/mwscript/skyextensions.cpp index 2b6bf826f9..4f4891b424 100644 --- a/apps/openmw/mwscript/skyextensions.cpp +++ b/apps/openmw/mwscript/skyextensions.cpp @@ -108,7 +108,7 @@ namespace MWScript chances.reserve(10); while(arg0 > 0) { - chances.push_back(std::max(0, std::min(127, runtime[0].mInteger))); + chances.push_back(std::clamp(runtime[0].mInteger, 0, 127)); runtime.pop(); arg0--; } diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index 3d00b24ef8..5cfb2b2989 100644 --- a/apps/openmw/mwscript/transformationextensions.cpp +++ b/apps/openmw/mwscript/transformationextensions.cpp @@ -32,7 +32,7 @@ namespace MWScript std::vector actors; MWBase::Environment::get().getWorld()->getActorsStandingOn (ptr, actors); for (auto& actor : actors) - MWBase::Environment::get().getWorld()->moveObjectBy(actor, diff, false, false); + MWBase::Environment::get().getWorld()->moveObjectBy(actor, diff); } template @@ -312,7 +312,7 @@ namespace MWScript } dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObjectBy(ptr, newPos - curPos, true, true)); + MWBase::Environment::get().getWorld()->moveObject(ptr, newPos, true, true)); } }; @@ -731,7 +731,7 @@ namespace MWScript // This approach can be used to create elevators. moveStandingActors(ptr, diff); dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff, false, true)); + MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff)); } }; @@ -767,7 +767,7 @@ namespace MWScript // This approach can be used to create elevators. moveStandingActors(ptr, diff); dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff, false, true)); + MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff)); } }; diff --git a/apps/openmw/mwsound/loudness.cpp b/apps/openmw/mwsound/loudness.cpp index ae31d60949..a36615ee4a 100644 --- a/apps/openmw/mwsound/loudness.cpp +++ b/apps/openmw/mwsound/loudness.cpp @@ -40,7 +40,7 @@ void Sound_Loudness::analyzeLoudness(const std::vector< char >& data) else if (mSampleType == SampleType_Float32) { value = *reinterpret_cast(&mQueue[sample*advance]); - value = std::max(-1.f, std::min(1.f, value)); // Float samples *should* be scaled to [-1,1] already. + value = std::clamp(value, -1.f, 1.f); // Float samples *should* be scaled to [-1,1] already. } sum += value*value; @@ -64,8 +64,7 @@ float Sound_Loudness::getLoudnessAtTime(float sec) const if(mSamplesPerSec <= 0.0f || mSamples.empty() || sec < 0.0f) return 0.0f; - size_t index = static_cast(sec * mSamplesPerSec); - index = std::max(0, std::min(index, mSamples.size()-1)); + size_t index = std::clamp(sec * mSamplesPerSec, 0, mSamples.size() - 1); return mSamples[index]; } diff --git a/apps/openmw/mwsound/volumesettings.cpp b/apps/openmw/mwsound/volumesettings.cpp index cc4eac3d6d..fd79b97e9b 100644 --- a/apps/openmw/mwsound/volumesettings.cpp +++ b/apps/openmw/mwsound/volumesettings.cpp @@ -10,7 +10,7 @@ namespace MWSound { float clamp(float value) { - return std::max(0.0f, std::min(1.0f, value)); + return std::clamp(value, 0.f, 1.f); } } diff --git a/apps/openmw/mwstate/character.cpp b/apps/openmw/mwstate/character.cpp index df1ab1bdfb..59ddd2cd10 100644 --- a/apps/openmw/mwstate/character.cpp +++ b/apps/openmw/mwstate/character.cpp @@ -13,6 +13,15 @@ bool MWState::operator< (const Slot& left, const Slot& right) return left.mTimeStamp& contentFiles) +{ + for (const std::string& c : contentFiles) + { + if (Misc::StringUtils::ciEndsWith(c, ".esm") || Misc::StringUtils::ciEndsWith(c, ".omwgame")) + return c; + } + return ""; +} void MWState::Character::addSlot (const boost::filesystem::path& path, const std::string& game) { @@ -30,8 +39,7 @@ void MWState::Character::addSlot (const boost::filesystem::path& path, const std slot.mProfile.load (reader); - if (Misc::StringUtils::lowerCase (slot.mProfile.mContentFiles.at (0))!= - Misc::StringUtils::lowerCase (game)) + if (!Misc::StringUtils::ciEqual(getFirstGameFile(slot.mProfile.mContentFiles), game)) return; // this file is for a different game -> ignore mSlots.push_back (slot); diff --git a/apps/openmw/mwstate/character.hpp b/apps/openmw/mwstate/character.hpp index 32c79a183e..e12de9ca64 100644 --- a/apps/openmw/mwstate/character.hpp +++ b/apps/openmw/mwstate/character.hpp @@ -16,6 +16,8 @@ namespace MWState bool operator< (const Slot& left, const Slot& right); + std::string getFirstGameFile(const std::vector& contentFiles); + class Character { public: diff --git a/apps/openmw/mwstate/charactermanager.cpp b/apps/openmw/mwstate/charactermanager.cpp index a324dfe0f7..027a4f38a4 100644 --- a/apps/openmw/mwstate/charactermanager.cpp +++ b/apps/openmw/mwstate/charactermanager.cpp @@ -6,8 +6,8 @@ #include MWState::CharacterManager::CharacterManager (const boost::filesystem::path& saves, - const std::string& game) -: mPath (saves), mCurrent (nullptr), mGame (game) + const std::vector& contentFiles) +: mPath (saves), mCurrent (nullptr), mGame (getFirstGameFile(contentFiles)) { if (!boost::filesystem::is_directory (mPath)) { diff --git a/apps/openmw/mwstate/charactermanager.hpp b/apps/openmw/mwstate/charactermanager.hpp index 2daf73401f..8b3f2b8f8f 100644 --- a/apps/openmw/mwstate/charactermanager.hpp +++ b/apps/openmw/mwstate/charactermanager.hpp @@ -29,7 +29,7 @@ namespace MWState public: - CharacterManager (const boost::filesystem::path& saves, const std::string& game); + CharacterManager (const boost::filesystem::path& saves, const std::vector& contentFiles); Character *getCurrentCharacter (); ///< @note May return null diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 4387d7faa7..b9825a0f90 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -88,8 +88,8 @@ std::map MWState::StateManager::buildContentFileIndexMap (const ESM::E return map; } -MWState::StateManager::StateManager (const boost::filesystem::path& saves, const std::string& game) -: mQuitRequest (false), mAskLoadRecent(false), mState (State_NoGame), mCharacterManager (saves, game), mTimePlayed (0) +MWState::StateManager::StateManager (const boost::filesystem::path& saves, const std::vector& contentFiles) +: mQuitRequest (false), mAskLoadRecent(false), mState (State_NoGame), mCharacterManager (saves, contentFiles), mTimePlayed (0) { } @@ -558,6 +558,8 @@ void MWState::StateManager::loadGame (const Character *character, const std::str // Since we passed "changeEvent=false" to changeCell, we shouldn't have triggered the cell change flag. // But make sure the flag is cleared anyway in case it was set from an earlier game. MWBase::Environment::get().getWorld()->markCellAsUnchanged(); + + MWBase::Environment::get().getLuaManager()->gameLoaded(); } catch (const std::exception& e) { diff --git a/apps/openmw/mwstate/statemanagerimp.hpp b/apps/openmw/mwstate/statemanagerimp.hpp index 3534dabf2b..a29e72b3ad 100644 --- a/apps/openmw/mwstate/statemanagerimp.hpp +++ b/apps/openmw/mwstate/statemanagerimp.hpp @@ -31,7 +31,7 @@ namespace MWState public: - StateManager (const boost::filesystem::path& saves, const std::string& game); + StateManager (const boost::filesystem::path& saves, const std::vector& contentFiles); void requestQuit() override; diff --git a/apps/openmw/mwworld/esmloader.cpp b/apps/openmw/mwworld/esmloader.cpp index a01128fe36..1917c41428 100644 --- a/apps/openmw/mwworld/esmloader.cpp +++ b/apps/openmw/mwworld/esmloader.cpp @@ -42,8 +42,8 @@ void EsmLoader::load(const boost::filesystem::path& filepath, int& index) ESM::ESMReader lEsm; lEsm.setEncoder(mEncoder); lEsm.setIndex(index); - lEsm.setGlobalReaderList(&mEsm); lEsm.open(filepath.string()); + lEsm.resolveParentFileIndices(mEsm); mEsm[index] = lEsm; mStore.load(mEsm[index], &mListener); } @@ -104,8 +104,8 @@ void EsmLoader::load(const boost::filesystem::path& filepath, int& index) effect.mMagnitude = magnitude; effect.mMinMagnitude = magnitude; effect.mMaxMagnitude = magnitude; - // Prevent recalculation of resistances - effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances; + // Prevent recalculation of resistances and don't reflect or absorb the effect + effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; } else { @@ -172,8 +172,8 @@ void EsmLoader::load(const boost::filesystem::path& filepath, int& index) effect.mDuration = -1; effect.mTimeLeft = -1; effect.mEffectIndex = static_cast(effectIndex); - // Prevent recalculation of resistances - effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances; + // Prevent recalculation of resistances and don't reflect or absorb the effect + effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; params.mEffects.emplace_back(effect); } auto [begin, end] = equippedItems.equal_range(id); diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index c5b0fff00f..bafdc8f37d 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -1,14 +1,14 @@ #include "esmstore.hpp" #include +#include #include -#include - #include -#include #include #include +#include +#include #include #include "../mwmechanics/spelllist.hpp" @@ -27,6 +27,7 @@ namespace void readRefs(const ESM::Cell& cell, std::vector& refs, std::vector& refIDs, std::vector& readers) { + // TODO: we have many similar copies of this code. for (size_t i = 0; i < cell.mContextList.size(); i++) { size_t index = cell.mContextList[i].index; @@ -59,7 +60,7 @@ namespace } } - std::vector getNPCsToReplace(const MWWorld::Store& factions, const MWWorld::Store& classes, const std::map& npcs) + std::vector getNPCsToReplace(const MWWorld::Store& factions, const MWWorld::Store& classes, const std::unordered_map& npcs) { // Cache first class from store - we will use it if current class is not found std::string defaultCls; @@ -112,8 +113,8 @@ namespace // Custom enchanted items can reference scripts that no longer exist, this doesn't necessarily mean the base item no longer exists however. // So instead of removing the item altogether, we're only removing the script. - template - void removeMissingScripts(const MWWorld::Store& scripts, std::map& items) + template + void removeMissingScripts(const MWWorld::Store& scripts, MapT& items) { for(auto& [id, item] : items) { @@ -150,38 +151,9 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) ESM::Dialogue *dialogue = nullptr; // Land texture loading needs to use a separate internal store for each plugin. - // We set the number of plugins here to avoid continual resizes during loading, - // and so we can properly verify if valid plugin indices are being passed to the - // LandTexture Store retrieval methods. - mLandTextures.resize(esm.getGlobalReaderList()->size()); - - /// \todo Move this to somewhere else. ESMReader? - // Cache parent esX files by tracking their indices in the global list of - // all files/readers used by the engine. This will greaty accelerate - // refnumber mangling, as required for handling moved references. - const std::vector &masters = esm.getGameFiles(); - std::vector *allPlugins = esm.getGlobalReaderList(); - for (size_t j = 0; j < masters.size(); j++) { - const ESM::Header::MasterData &mast = masters[j]; - std::string fname = mast.name; - int index = ~0; - for (int i = 0; i < esm.getIndex(); i++) { - const std::string candidate = allPlugins->at(i).getContext().filename; - std::string fnamecandidate = boost::filesystem::path(candidate).filename().string(); - if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) { - index = i; - break; - } - } - if (index == (int)~0) { - // Tried to load a parent file that has not been loaded yet. This is bad, - // the launcher should have taken care of this. - std::string fstring = "File " + esm.getName() + " asks for parent file " + masters[j].name - + ", but it has not been loaded yet. Please check your load order."; - esm.fail(fstring); - } - esm.addParentFileIndex(index); - } + // We set the number of plugins here so we can properly verify if valid plugin + // indices are being passed to the LandTexture Store retrieval methods. + mLandTextures.addPlugin(); // Loop through all records while(esm.hasMoreRecs()) @@ -213,6 +185,13 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) // ignore project file only records esm.skipRecord(); } + else if (n.toInt() == ESM::REC_LUAL) + { + ESM::LuaScriptsCfg cfg; + cfg.load(esm); + // TODO: update refnums in cfg.mScripts[].mInitializationData according to load order + mLuaContent.push_back(std::move(cfg)); + } else { throw std::runtime_error("Unknown record: " + n.toString()); } @@ -234,6 +213,32 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) } } +ESM::LuaScriptsCfg ESMStore::getLuaScriptsCfg() const +{ + ESM::LuaScriptsCfg cfg; + for (const LuaContent& c : mLuaContent) + { + if (std::holds_alternative(c)) + { + // *.omwscripts are intentionally reloaded every time when `getLuaScriptsCfg` is called. + // It is important for the `reloadlua` console command. + try + { + auto file = std::ifstream(std::get(c)); + std::string fileContent(std::istreambuf_iterator(file), {}); + LuaUtil::parseOMWScripts(cfg, fileContent); + } + catch (std::exception& e) { Log(Debug::Error) << e.what(); } + } + else + { + const ESM::LuaScriptsCfg& addition = std::get(c); + cfg.mScripts.insert(cfg.mScripts.end(), addition.mScripts.begin(), addition.mScripts.end()); + } + } + return cfg; +} + void ESMStore::setUp(bool validateRecords) { mIds.clear(); @@ -263,12 +268,14 @@ void ESMStore::setUp(bool validateRecords) if (validateRecords) { validate(); - countRecords(); + countAllCellRefs(); } } -void ESMStore::countRecords() +void ESMStore::countAllCellRefs() { + // TODO: We currently need to read entire files here again. + // We should consider consolidating or deferring this reading. if(!mRefCount.empty()) return; std::vector refs; @@ -286,6 +293,7 @@ void ESMStore::countRecords() if (value.mRefID != deletedRefID) { std::string& refId = refIDs[value.mRefID]; + // We manually lower case IDs here for the time being to improve performance. Misc::StringUtils::lowerCaseInPlace(refId); ++mRefCount[std::move(refId)]; } @@ -495,9 +503,8 @@ void ESMStore::removeMissingObjects(Store& store) throw std::runtime_error ("Invalid player record (race or class unavailable"); } - std::pair, bool> ESMStore::getSpellList(const std::string& originalId) const + std::pair, bool> ESMStore::getSpellList(const std::string& id) const { - const std::string id = Misc::StringUtils::lowerCase(originalId); auto result = mSpellListCache.find(id); std::shared_ptr ptr; if (result != mSpellListCache.end()) diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 6ad479f8bf..8582a1daca 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include "store.hpp" @@ -74,8 +75,9 @@ namespace MWWorld // Lookup of all IDs. Makes looking up references faster. Just // maps the id name to the record type. - std::map mIds; - std::map mStaticIds; + using IDMap = std::unordered_map; + IDMap mIds; + IDMap mStaticIds; std::unordered_map mRefCount; @@ -83,16 +85,25 @@ namespace MWWorld unsigned int mDynamicCount; - mutable std::map > mSpellListCache; + mutable std::unordered_map, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> mSpellListCache; /// Validate entries in store after setup void validate(); - void countRecords(); + void countAllCellRefs(); template void removeMissingObjects(Store& store); + + using LuaContent = std::variant< + ESM::LuaScriptsCfg, // data from an omwaddon + std::string>; // path to an omwscripts file + std::vector mLuaContent; + public: + void addOMWScripts(std::string filePath) { mLuaContent.push_back(std::move(filePath)); } + ESM::LuaScriptsCfg getLuaScriptsCfg() const; + /// \todo replace with SharedIterator typedef std::map::const_iterator iterator; @@ -105,10 +116,9 @@ namespace MWWorld } /// Look up the given ID in 'all'. Returns 0 if not found. - /// \note id must be in lower case. int find(const std::string &id) const { - std::map::const_iterator it = mIds.find(id); + IDMap::const_iterator it = mIds.find(id); if (it == mIds.end()) { return 0; } @@ -116,7 +126,7 @@ namespace MWWorld } int findStatic(const std::string &id) const { - std::map::const_iterator it = mStaticIds.find(id); + IDMap::const_iterator it = mStaticIds.find(id); if (it == mStaticIds.end()) { return 0; } diff --git a/apps/openmw/mwworld/livecellref.cpp b/apps/openmw/mwworld/livecellref.cpp index c74817911e..62c9f3a2f0 100644 --- a/apps/openmw/mwworld/livecellref.cpp +++ b/apps/openmw/mwworld/livecellref.cpp @@ -73,3 +73,8 @@ bool MWWorld::LiveCellRefBase::checkStateImp (const ESM::ObjectState& state) { return true; } + +unsigned int MWWorld::LiveCellRefBase::getType() const +{ + return mClass->getType(); +} diff --git a/apps/openmw/mwworld/livecellref.hpp b/apps/openmw/mwworld/livecellref.hpp index 48e237bce4..1ead0395fd 100644 --- a/apps/openmw/mwworld/livecellref.hpp +++ b/apps/openmw/mwworld/livecellref.hpp @@ -43,6 +43,9 @@ namespace MWWorld virtual std::string_view getTypeDescription() const = 0; + unsigned int getType() const; + ///< @see MWWorld::Class::getType + protected: void loadImp (const ESM::ObjectState& state); diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index 9ee137fab6..e6a7195f5a 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -432,7 +432,7 @@ namespace MWWorld float speed = fTargetSpellMaxSpeed * magicBoltState.mSpeed; osg::Vec3f direction = orient * osg::Vec3f(0,1,0); direction.normalize(); - osg::Vec3f newPos = projectile->getPosition() + direction * duration * speed; + projectile->setVelocity(direction * speed); update(magicBoltState, duration); @@ -441,8 +441,6 @@ namespace MWWorld if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); projectile->setValidTargets(targetActors); - - mPhysics->updateProjectile(magicBoltState.mProjectileId, newPos); } } @@ -460,7 +458,7 @@ namespace MWWorld // simulating aerodynamics at all projectileState.mVelocity -= osg::Vec3f(0, 0, Constants::GravityConst * Constants::UnitsPerMeter * 0.1f) * duration; - osg::Vec3f newPos = projectile->getPosition() + projectileState.mVelocity * duration; + projectile->setVelocity(projectileState.mVelocity); // rotation does not work well for throwing projectiles - their roll angle will depend on shooting direction. if (!projectileState.mThrown) @@ -479,8 +477,6 @@ namespace MWWorld if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); projectile->setValidTargets(targetActors); - - mPhysics->updateProjectile(projectileState.mProjectileId, newPos); } } @@ -493,7 +489,7 @@ namespace MWWorld auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId); - const auto pos = projectile->getPosition(); + const auto pos = projectile->getSimulationPosition(); projectileState.mNode->setPosition(pos); if (projectile->isActive()) @@ -529,7 +525,7 @@ namespace MWWorld auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId); - const auto pos = projectile->getPosition(); + const auto pos = projectile->getSimulationPosition(); magicBoltState.mNode->setPosition(pos); for (const auto& sound : magicBoltState.mSounds) sound->setPosition(pos); @@ -546,7 +542,7 @@ namespace MWWorld cast.mId = magicBoltState.mSpellId; cast.mSourceName = magicBoltState.mSourceName; cast.mSlot = magicBoltState.mSlot; - cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, false, true); + cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, true); MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName, false, magicBoltState.mSlot); magicBoltState.mToDelete = true; diff --git a/apps/openmw/mwworld/ptr.cpp b/apps/openmw/mwworld/ptr.cpp deleted file mode 100644 index b18e3b1689..0000000000 --- a/apps/openmw/mwworld/ptr.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "ptr.hpp" - -#include - -#include "containerstore.hpp" -#include "class.hpp" -#include "livecellref.hpp" - -unsigned int MWWorld::Ptr::getType() const -{ - if(mRef != nullptr) - return mRef->mClass->getType(); - throw std::runtime_error("Can't get type name from an empty object."); -} - -MWWorld::LiveCellRefBase *MWWorld::Ptr::getBase() const -{ - if (!mRef) - throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); - - return mRef; -} - -MWWorld::CellRef& MWWorld::Ptr::getCellRef() const -{ - assert(mRef); - - return mRef->mRef; -} - -MWWorld::RefData& MWWorld::Ptr::getRefData() const -{ - assert(mRef); - - return mRef->mData; -} - -void MWWorld::Ptr::setContainerStore (ContainerStore *store) -{ - assert (store); - assert (!mCell); - - mContainerStore = store; -} - -MWWorld::ContainerStore *MWWorld::Ptr::getContainerStore() const -{ - return mContainerStore; -} - -MWWorld::Ptr::operator const void *() -{ - return mRef; -} - -// ------------------------------------------------------------------------------- - -unsigned int MWWorld::ConstPtr::getType() const -{ - if(mRef != nullptr) - return mRef->mClass->getType(); - throw std::runtime_error("Can't get type name from an empty object."); -} - -const MWWorld::LiveCellRefBase *MWWorld::ConstPtr::getBase() const -{ - if (!mRef) - throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); - - return mRef; -} - -void MWWorld::ConstPtr::setContainerStore (const ContainerStore *store) -{ - assert (store); - assert (!mCell); - - mContainerStore = store; -} - -const MWWorld::ContainerStore *MWWorld::ConstPtr::getContainerStore() const -{ - return mContainerStore; -} - -MWWorld::ConstPtr::operator const void *() -{ - return mRef; -} diff --git a/apps/openmw/mwworld/ptr.hpp b/apps/openmw/mwworld/ptr.hpp index a82abaa212..4dbdfa5545 100644 --- a/apps/openmw/mwworld/ptr.hpp +++ b/apps/openmw/mwworld/ptr.hpp @@ -2,8 +2,9 @@ #define GAME_MWWORLD_PTR_H #include - +#include #include +#include #include #include "livecellref.hpp" @@ -15,20 +16,19 @@ namespace MWWorld struct LiveCellRefBase; /// \brief Pointer to a LiveCellRef - - class Ptr + /// @note PtrBase is never used directly and needed only to define Ptr and ConstPtr + template class TypeTransform> + class PtrBase { public: - MWWorld::LiveCellRefBase *mRef; - CellStore *mCell; - ContainerStore *mContainerStore; + typedef TypeTransform LiveCellRefBaseType; + typedef TypeTransform CellStoreType; + typedef TypeTransform ContainerStoreType; - public: - Ptr(MWWorld::LiveCellRefBase *liveCellRef=nullptr, CellStore *cell=nullptr) - : mRef(liveCellRef), mCell(cell), mContainerStore(nullptr) - { - } + LiveCellRefBaseType *mRef; + CellStoreType *mCell; + ContainerStoreType *mContainerStore; bool isEmpty() const { @@ -40,7 +40,12 @@ namespace MWWorld // Note 1: ids are not sequential. E.g. for a creature `getType` returns 0x41455243. // Note 2: Life is not easy and full of surprises. For example // prison marker reuses ESM::Door record. Player is ESM::NPC. - unsigned int getType() const; + unsigned int getType() const + { + if(mRef != nullptr) + return mRef->getType(); + throw std::runtime_error("Can't get type name from an empty object."); + } std::string_view getTypeDescription() const { @@ -55,9 +60,9 @@ namespace MWWorld } template - MWWorld::LiveCellRef *get() const + TypeTransform> *get() const { - MWWorld::LiveCellRef *ref = dynamic_cast*>(mRef); + TypeTransform> *ref = dynamic_cast>*>(mRef); if(ref) return ref; std::stringstream str; @@ -68,13 +73,26 @@ namespace MWWorld throw std::runtime_error(str.str()); } - MWWorld::LiveCellRefBase *getBase() const; + LiveCellRefBaseType *getBase() const + { + if (!mRef) + throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); + return mRef; + } - MWWorld::CellRef& getCellRef() const; + TypeTransform& getCellRef() const + { + assert(mRef); + return mRef->mRef; + } - RefData& getRefData() const; + TypeTransform& getRefData() const + { + assert(mRef); + return mRef->mData; + } - CellStore *getCell() const + CellStoreType *getCell() const { assert(mCell); return mCell; @@ -85,164 +103,47 @@ namespace MWWorld return (mContainerStore == nullptr) && (mCell != nullptr); } - void setContainerStore (ContainerStore *store); + void setContainerStore (ContainerStoreType *store) ///< Must not be called on references that are in a cell. + { + assert (store); + assert (!mCell); + mContainerStore = store; + } - ContainerStore *getContainerStore() const; + ContainerStoreType *getContainerStore() const ///< May return a 0-pointer, if reference is not in a container. + { + return mContainerStore; + } - operator const void *(); + operator const void *() const ///< Return a 0-pointer, if Ptr is empty; return a non-0-pointer, if Ptr is not empty + { + return mRef; + } + + protected: + PtrBase(LiveCellRefBaseType *liveCellRef, CellStoreType *cell, ContainerStoreType* containerStore) : mRef(liveCellRef), mCell(cell), mContainerStore(containerStore) {} }; - /// \brief Pointer to a const LiveCellRef + /// @note It is possible to get mutable values from const Ptr. So if a function accepts const Ptr&, the object is still mutable. + /// To make it really const the argument should be const ConstPtr&. + class Ptr : public PtrBase + { + public: + Ptr(LiveCellRefBase *liveCellRef=nullptr, CellStoreType *cell=nullptr) : PtrBase(liveCellRef, cell, nullptr) {} + }; + + /// @note The difference between Ptr and ConstPtr is that the second one adds const to the underlying pointers. /// @note a Ptr can be implicitely converted to a ConstPtr, but you can not convert a ConstPtr to a Ptr. - class ConstPtr + class ConstPtr : public PtrBase { public: - - const MWWorld::LiveCellRefBase *mRef; - const CellStore *mCell; - const ContainerStore *mContainerStore; - - public: - ConstPtr(const MWWorld::LiveCellRefBase *liveCellRef=nullptr, const CellStore *cell=nullptr) - : mRef(liveCellRef), mCell(cell), mContainerStore(nullptr) - { - } - - ConstPtr(const MWWorld::Ptr& ptr) - : mRef(ptr.mRef), mCell(ptr.mCell), mContainerStore(ptr.mContainerStore) - { - } - - bool isEmpty() const - { - return mRef == nullptr; - } - - unsigned int getType() const; - - std::string_view getTypeDescription() const - { - return mRef ? mRef->getTypeDescription() : "nullptr"; - } - - const Class& getClass() const - { - if(mRef != nullptr) - return *(mRef->mClass); - throw std::runtime_error("Cannot get class of an empty object"); - } - - template - const MWWorld::LiveCellRef *get() const - { - const MWWorld::LiveCellRef *ref = dynamic_cast*>(mRef); - if(ref) return ref; - - std::stringstream str; - str<< "Bad LiveCellRef cast to "<mRef; - } - - const RefData& getRefData() const - { - assert(mRef); - return mRef->mData; - } - - const CellStore *getCell() const - { - assert(mCell); - return mCell; - } - - bool isInCell() const - { - return (mContainerStore == nullptr) && (mCell != nullptr); - } - - void setContainerStore (const ContainerStore *store); - ///< Must not be called on references that are in a cell. - - const ContainerStore *getContainerStore() const; - ///< May return a 0-pointer, if reference is not in a container. - - operator const void *(); - ///< Return a 0-pointer, if Ptr is empty; return a non-0-pointer, if Ptr is not empty + ConstPtr(const Ptr& ptr) : PtrBase(ptr.mRef, ptr.mCell, ptr.mContainerStore) {} + ConstPtr(const LiveCellRefBase *liveCellRef=nullptr, const CellStoreType *cell=nullptr) : PtrBase(liveCellRef, cell, nullptr) {} }; - inline bool operator== (const Ptr& left, const Ptr& right) - { - return left.mRef==right.mRef; - } - - inline bool operator!= (const Ptr& left, const Ptr& right) - { - return !(left==right); - } - - inline bool operator< (const Ptr& left, const Ptr& right) - { - return left.mRef= (const Ptr& left, const Ptr& right) - { - return !(left (const Ptr& left, const Ptr& right) - { - return rightright); - } - - inline bool operator== (const ConstPtr& left, const ConstPtr& right) - { - return left.mRef==right.mRef; - } - - inline bool operator!= (const ConstPtr& left, const ConstPtr& right) - { - return !(left==right); - } - - inline bool operator< (const ConstPtr& left, const ConstPtr& right) - { - return left.mRef= (const ConstPtr& left, const ConstPtr& right) - { - return !(left (const ConstPtr& left, const ConstPtr& right) - { - return rightright); - } } #endif diff --git a/apps/openmw/mwworld/recordcmp.hpp b/apps/openmw/mwworld/recordcmp.hpp deleted file mode 100644 index f749351cea..0000000000 --- a/apps/openmw/mwworld/recordcmp.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef OPENMW_MWWORLD_RECORDCMP_H -#define OPENMW_MWWORLD_RECORDCMP_H - -#include - -#include - -namespace MWWorld -{ - struct RecordCmp - { - template - bool operator()(const T &x, const T& y) const { - return x.mId < y.mId; - } - }; - - template <> - inline bool RecordCmp::operator()(const ESM::Dialogue &x, const ESM::Dialogue &y) const { - return Misc::StringUtils::ciLess(x.mId, y.mId); - } - - template <> - inline bool RecordCmp::operator()(const ESM::Cell &x, const ESM::Cell &y) const { - return Misc::StringUtils::ciLess(x.mName, y.mName); - } - - template <> - inline bool RecordCmp::operator()(const ESM::Pathgrid &x, const ESM::Pathgrid &y) const { - return Misc::StringUtils::ciLess(x.mCell, y.mCell); - } - -} // end namespace -#endif diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index d49e6c0e8b..769817810a 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -132,7 +132,7 @@ namespace { btVector3 aabbMin; btVector3 aabbMax; - object->getShapeInstance()->getCollisionShape()->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); + object->getShapeInstance()->mCollisionShape->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); const auto center = (aabbMax + aabbMin) * 0.5f; @@ -147,12 +147,12 @@ namespace transform.getOrigin() ); - const auto start = Misc::Convert::makeOsgVec3f(closedDoorTransform(center + toPoint)); + const auto start = Misc::Convert::toOsg(closedDoorTransform(center + toPoint)); const auto startPoint = physics.castRay(start, start - osg::Vec3f(0, 0, 1000), ptr, {}, MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Water); const auto connectionStart = startPoint.mHit ? startPoint.mHitPos : start; - const auto end = Misc::Convert::makeOsgVec3f(closedDoorTransform(center - toPoint)); + const auto end = Misc::Convert::toOsg(closedDoorTransform(center - toPoint)); const auto endPoint = physics.castRay(end, end - osg::Vec3f(0, 0, 1000), ptr, {}, MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Water); const auto connectionEnd = endPoint.mHit ? endPoint.mHitPos : end; diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index 963e3b78cd..4d720a11a7 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -38,8 +38,8 @@ namespace MWWorld bool isDeleted = false; record.load(esm, isDeleted); - - mStatic.insert_or_assign(record.mIndex, record); + auto idx = record.mIndex; + mStatic.insert_or_assign(idx, std::move(record)); } template int IndexedStore::getSize() const @@ -98,13 +98,11 @@ namespace MWWorld template const T *Store::search(const std::string &id) const { - std::string idLower = Misc::StringUtils::lowerCase(id); - - typename Dynamic::const_iterator dit = mDynamic.find(idLower); + typename Dynamic::const_iterator dit = mDynamic.find(id); if (dit != mDynamic.end()) return &dit->second; - typename std::map::const_iterator it = mStatic.find(idLower); + typename Static::const_iterator it = mStatic.find(id); if (it != mStatic.end()) return &(it->second); @@ -113,8 +111,7 @@ namespace MWWorld template const T *Store::searchStatic(const std::string &id) const { - std::string idLower = Misc::StringUtils::lowerCase(id); - typename std::map::const_iterator it = mStatic.find(idLower); + typename Static::const_iterator it = mStatic.find(id); if (it != mStatic.end()) return &(it->second); @@ -159,7 +156,7 @@ namespace MWWorld bool isDeleted = false; record.load(esm, isDeleted); - Misc::StringUtils::lowerCaseInPlace(record.mId); + Misc::StringUtils::lowerCaseInPlace(record.mId); // TODO: remove this line once we have ported our remaining code base to lowercase on lookup std::pair inserted = mStatic.insert_or_assign(record.mId, record); if (inserted.second) @@ -206,14 +203,13 @@ namespace MWWorld template T *Store::insert(const T &item, bool overrideOnly) { - std::string id = Misc::StringUtils::lowerCase(item.mId); if(overrideOnly) { - auto it = mStatic.find(id); + auto it = mStatic.find(item.mId); if(it == mStatic.end()) return nullptr; } - std::pair result = mDynamic.insert_or_assign(id, item); + std::pair result = mDynamic.insert_or_assign(item.mId, item); T *ptr = &result.first->second; if (result.second) mShared.push_back(ptr); @@ -222,8 +218,7 @@ namespace MWWorld template T *Store::insertStatic(const T &item) { - std::string id = Misc::StringUtils::lowerCase(item.mId); - std::pair result = mStatic.insert_or_assign(id, item); + std::pair result = mStatic.insert_or_assign(item.mId, item); T *ptr = &result.first->second; if (result.second) mShared.push_back(ptr); @@ -232,9 +227,7 @@ namespace MWWorld template bool Store::eraseStatic(const std::string &id) { - std::string idLower = Misc::StringUtils::lowerCase(id); - - typename std::map::iterator it = mStatic.find(idLower); + typename Static::iterator it = mStatic.find(id); if (it != mStatic.end()) { // delete from the static part of mShared @@ -242,7 +235,7 @@ namespace MWWorld typename std::vector::iterator end = sharedIter + mStatic.size(); while (sharedIter != mShared.end() && sharedIter != end) { - if((*sharedIter)->mId == idLower) { + if(Misc::StringUtils::ciEqual((*sharedIter)->mId, id)) { mShared.erase(sharedIter); break; } @@ -257,17 +250,13 @@ namespace MWWorld template bool Store::erase(const std::string &id) { - std::string key = Misc::StringUtils::lowerCase(id); - typename Dynamic::iterator it = mDynamic.find(key); - if (it == mDynamic.end()) { + if (!mDynamic.erase(id)) return false; - } - mDynamic.erase(it); // have to reinit the whole shared part assert(mShared.size() >= mStatic.size()); mShared.erase(mShared.begin() + mStatic.size(), mShared.end()); - for (it = mDynamic.begin(); it != mDynamic.end(); ++it) { + for (auto it = mDynamic.begin(); it != mDynamic.end(); ++it) { mShared.push_back(&it->second); } return true; @@ -304,11 +293,6 @@ namespace MWWorld //========================================================================= Store::Store() { - mStatic.emplace_back(); - LandTextureList <exl = mStatic[0]; - // More than enough to hold Morrowind.esm. Extra lists for plugins will we - // added on-the-fly in a different method. - ltexl.reserve(128); } const ESM::LandTexture *Store::search(size_t index, size_t plugin) const { @@ -338,42 +322,33 @@ namespace MWWorld assert(plugin < mStatic.size()); return mStatic[plugin].size(); } - RecordId Store::load(ESM::ESMReader &esm, size_t plugin) + RecordId Store::load(ESM::ESMReader &esm) { ESM::LandTexture lt; bool isDeleted = false; lt.load(esm, isDeleted); - assert(plugin < mStatic.size()); - // Replace texture for records with given ID and index from all plugins. for (unsigned int i=0; i(search(lt.mIndex, i)); if (tex) { - const std::string texId = Misc::StringUtils::lowerCase(tex->mId); - const std::string ltId = Misc::StringUtils::lowerCase(lt.mId); - if (texId == ltId) - { + if (Misc::StringUtils::ciEqual(tex->mId, lt.mId)) tex->mTexture = lt.mTexture; - } } } - LandTextureList <exl = mStatic[plugin]; + LandTextureList <exl = mStatic.back(); if(lt.mIndex + 1 > (int)ltexl.size()) ltexl.resize(lt.mIndex+1); // Store it - ltexl[lt.mIndex] = lt; + auto idx = lt.mIndex; + ltexl[idx] = std::move(lt); - return RecordId(lt.mId, isDeleted); - } - RecordId Store::load(ESM::ESMReader &esm) - { - return load(esm, esm.getIndex()); + return RecordId(ltexl[idx].mId, isDeleted); } Store::iterator Store::begin(size_t plugin) const { @@ -385,11 +360,6 @@ namespace MWWorld assert(plugin < mStatic.size()); return mStatic[plugin].end(); } - void Store::resize(size_t num) - { - if (mStatic.size() < num) - mStatic.resize(num); - } // Land //========================================================================= @@ -503,16 +473,12 @@ namespace MWWorld } const ESM::Cell *Store::search(const std::string &id) const { - ESM::Cell cell; - cell.mName = Misc::StringUtils::lowerCase(id); - - std::map::const_iterator it = mInt.find(cell.mName); - + DynamicInt::const_iterator it = mInt.find(id); if (it != mInt.end()) { return &(it->second); } - DynamicInt::const_iterator dit = mDynamicInt.find(cell.mName); + DynamicInt::const_iterator dit = mDynamicInt.find(id); if (dit != mDynamicInt.end()) { return &dit->second; } @@ -521,48 +487,34 @@ namespace MWWorld } const ESM::Cell *Store::search(int x, int y) const { - ESM::Cell cell; - cell.mData.mX = x; - cell.mData.mY = y; - std::pair key(x, y); DynamicExt::const_iterator it = mExt.find(key); - if (it != mExt.end()) { + if (it != mExt.end()) return &(it->second); - } DynamicExt::const_iterator dit = mDynamicExt.find(key); - if (dit != mDynamicExt.end()) { + if (dit != mDynamicExt.end()) return &dit->second; - } return nullptr; } const ESM::Cell *Store::searchStatic(int x, int y) const { - ESM::Cell cell; - cell.mData.mX = x; - cell.mData.mY = y; - - std::pair key(x, y); - DynamicExt::const_iterator it = mExt.find(key); - if (it != mExt.end()) { + DynamicExt::const_iterator it = mExt.find(std::make_pair(x,y)); + if (it != mExt.end()) return &(it->second); - } return nullptr; } const ESM::Cell *Store::searchOrCreate(int x, int y) { std::pair key(x, y); DynamicExt::const_iterator it = mExt.find(key); - if (it != mExt.end()) { + if (it != mExt.end()) return &(it->second); - } DynamicExt::const_iterator dit = mDynamicExt.find(key); - if (dit != mDynamicExt.end()) { + if (dit != mDynamicExt.end()) return &dit->second; - } ESM::Cell newCell; newCell.mData.mX = x; @@ -625,12 +577,11 @@ namespace MWWorld // Load the (x,y) coordinates of the cell, if it is an exterior cell, // so we can find the cell we need to merge with cell.loadNameAndData(esm, isDeleted); - std::string idLower = Misc::StringUtils::lowerCase(cell.mName); if(cell.mData.mFlags & ESM::Cell::Interior) { // Store interior cell by name, try to merge with existing parent data. - ESM::Cell *oldcell = const_cast(search(idLower)); + ESM::Cell *oldcell = const_cast(search(cell.mName)); if (oldcell) { // merge new cell into old cell // push the new references on the list of references to manage (saveContext = true) @@ -642,7 +593,7 @@ namespace MWWorld // spawn a new cell cell.loadCell(esm, true); - mInt[idLower] = cell; + mInt[cell.mName] = cell; } } else @@ -780,27 +731,19 @@ namespace MWWorld const std::string cellType = (cell.isExterior()) ? "exterior" : "interior"; throw std::runtime_error("Failed to create " + cellType + " cell"); } - ESM::Cell *ptr; if (cell.isExterior()) { std::pair key(cell.getGridX(), cell.getGridY()); // duplicate insertions are avoided by search(ESM::Cell &) - std::pair result = - mDynamicExt.insert(std::make_pair(key, cell)); - - ptr = &result.first->second; - mSharedExt.push_back(ptr); + DynamicExt::iterator result = mDynamicExt.emplace(key, cell).first; + mSharedExt.push_back(&result->second); + return &result->second; } else { - std::string key = Misc::StringUtils::lowerCase(cell.mName); - // duplicate insertions are avoided by search(ESM::Cell &) - std::pair result = - mDynamicInt.insert(std::make_pair(key, cell)); - - ptr = &result.first->second; - mSharedInt.push_back(ptr); + DynamicInt::iterator result = mDynamicInt.emplace(cell.mName, cell).first; + mSharedInt.push_back(&result->second); + return &result->second; } - return ptr; } bool Store::erase(const ESM::Cell &cell) { @@ -811,8 +754,7 @@ namespace MWWorld } bool Store::erase(const std::string &id) { - std::string key = Misc::StringUtils::lowerCase(id); - DynamicInt::iterator it = mDynamicInt.find(key); + DynamicInt::iterator it = mDynamicInt.find(id); if (it == mDynamicInt.end()) { return false; @@ -1052,6 +994,9 @@ namespace MWWorld mShared.reserve(mStatic.size()); for (auto & [_, dial] : mStatic) mShared.push_back(&dial); + // TODO: verify and document this inconsistent behaviour + // TODO: if we require this behaviour, maybe we should move it to the place that requires it + std::sort(mShared.begin(), mShared.end(), [](const ESM::Dialogue* l, const ESM::Dialogue* r) -> bool { return l->mId < r->mId; }); } template <> @@ -1062,12 +1007,11 @@ namespace MWWorld dialogue.loadId(esm); - std::string idLower = Misc::StringUtils::lowerCase(dialogue.mId); - std::map::iterator found = mStatic.find(idLower); + Static::iterator found = mStatic.find(dialogue.mId); if (found == mStatic.end()) { dialogue.loadData(esm, isDeleted); - mStatic.insert(std::make_pair(idLower, dialogue)); + mStatic.emplace(dialogue.mId, dialogue); } else { @@ -1081,11 +1025,7 @@ namespace MWWorld template<> bool Store::eraseStatic(const std::string &id) { - auto it = mStatic.find(Misc::StringUtils::lowerCase(id)); - - if (it != mStatic.end()) - mStatic.erase(it); - + mStatic.erase(id); return true; } diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index 4b1d648703..17a37c23ea 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -5,9 +5,11 @@ #include #include #include +#include #include -#include "recordcmp.hpp" +#include +#include namespace ESM { @@ -147,14 +149,15 @@ namespace MWWorld template class Store : public StoreBase { - std::map mStatic; - std::vector mShared; // Preserves the record order as it came from the content files (this - // is relevant for the spell autocalc code and selection order - // for heads/hairs in the character creation) - std::map mDynamic; - - typedef std::map Dynamic; - typedef std::map Static; + typedef std::unordered_map Static; + Static mStatic; + /// @par mShared usually preserves the record order as it came from the content files (this + /// is relevant for the spell autocalc code and selection order + /// for heads/hairs in the character creation) + /// @warning ESM::Dialogue Store currently implements a sorted order for unknown reasons. + std::vector mShared; + typedef std::unordered_map Dynamic; + Dynamic mDynamic; friend class ESMStore; @@ -219,13 +222,12 @@ namespace MWWorld const ESM::LandTexture *search(size_t index, size_t plugin) const; const ESM::LandTexture *find(size_t index, size_t plugin) const; - /// Resize the internal store to hold at least \a num plugins. - void resize(size_t num); + /// Resize the internal store to hold another plugin. + void addPlugin() { mStatic.emplace_back(); } size_t getSize() const override; size_t getSize(size_t plugin) const; - RecordId load(ESM::ESMReader &esm, size_t plugin); RecordId load(ESM::ESMReader &esm) override; iterator begin(size_t plugin) const; @@ -294,7 +296,7 @@ namespace MWWorld } }; - typedef std::map DynamicInt; + typedef std::unordered_map DynamicInt; typedef std::map, ESM::Cell, DynamicExtCmp> DynamicExt; DynamicInt mInt; @@ -354,7 +356,7 @@ namespace MWWorld class Store : public StoreBase { private: - typedef std::map Interior; + typedef std::unordered_map Interior; typedef std::map, ESM::Pathgrid> Exterior; Interior mInt; diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 4bdd784db1..965c690238 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -22,8 +22,6 @@ #include -using namespace MWWorld; - namespace { static const int invalidWeatherID = -1; @@ -38,1226 +36,1240 @@ namespace { return x * (1-factor) + y * factor; } -} -template -T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const -{ - WeatherSetting setting = timeSettings.getSetting(prefix); - float preSunriseTime = setting.mPreSunriseTime; - float postSunriseTime = setting.mPostSunriseTime; - float preSunsetTime = setting.mPreSunsetTime; - float postSunsetTime = setting.mPostSunsetTime; - - // night - if (gameHour < timeSettings.mNightEnd - preSunriseTime || gameHour > timeSettings.mNightStart + postSunsetTime) - return mNightValue; - // sunrise - else if (gameHour >= timeSettings.mNightEnd - preSunriseTime && gameHour <= timeSettings.mDayStart + postSunriseTime) + osg::Vec3f calculateStormDirection(const std::string& particleEffect) { - float duration = timeSettings.mDayStart + postSunriseTime - timeSettings.mNightEnd + preSunriseTime; - float middle = timeSettings.mNightEnd - preSunriseTime + duration / 2.f; - - if (gameHour <= middle) + osg::Vec3f stormDirection = MWWorld::Weather::defaultDirection(); + if (particleEffect == "meshes\\ashcloud.nif" || particleEffect == "meshes\\blightcloud.nif") { - // fade in - float advance = middle - gameHour; - float factor = 0.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunriseValue, mNightValue, factor); - } - else - { - // fade out - float advance = gameHour - middle; - float factor = 1.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunriseValue, mDayValue, factor); + osg::Vec3f playerPos = MWMechanics::getPlayer().getRefData().getPosition().asVec3(); + playerPos.z() = 0; + osg::Vec3f redMountainPos = osg::Vec3f(25000.f, 70000.f, 0.f); + stormDirection = (playerPos - redMountainPos); + stormDirection.normalize(); } + return stormDirection; } - // day - else if (gameHour > timeSettings.mDayStart + postSunriseTime && gameHour < timeSettings.mDayEnd - preSunsetTime) - return mDayValue; - // sunset - else if (gameHour >= timeSettings.mDayEnd - preSunsetTime && gameHour <= timeSettings.mNightStart + postSunsetTime) +} + +namespace MWWorld +{ + template + T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const { - float duration = timeSettings.mNightStart + postSunsetTime - timeSettings.mDayEnd + preSunsetTime; - float middle = timeSettings.mDayEnd - preSunsetTime + duration / 2.f; + WeatherSetting setting = timeSettings.getSetting(prefix); + float preSunriseTime = setting.mPreSunriseTime; + float postSunriseTime = setting.mPostSunriseTime; + float preSunsetTime = setting.mPreSunsetTime; + float postSunsetTime = setting.mPostSunsetTime; - if (gameHour <= middle) + // night + if (gameHour < timeSettings.mNightEnd - preSunriseTime || gameHour > timeSettings.mNightStart + postSunsetTime) + return mNightValue; + // sunrise + else if (gameHour >= timeSettings.mNightEnd - preSunriseTime && gameHour <= timeSettings.mDayStart + postSunriseTime) { - // fade in - float advance = middle - gameHour; - float factor = 0.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunsetValue, mDayValue, factor); - } - else - { - // fade out - float advance = gameHour - middle; - float factor = 1.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunsetValue, mNightValue, factor); - } - } - // shut up compiler - return T(); -} + float duration = timeSettings.mDayStart + postSunriseTime - timeSettings.mNightEnd + preSunriseTime; + float middle = timeSettings.mNightEnd - preSunriseTime + duration / 2.f; - - -template class MWWorld::TimeOfDayInterpolator; -template class MWWorld::TimeOfDayInterpolator; - -Weather::Weather(const std::string& name, - float stormWindSpeed, - float rainSpeed, - float dlFactor, - float dlOffset, - const std::string& particleEffect) - : mCloudTexture(Fallback::Map::getString("Weather_" + name + "_Cloud_Texture")) - , mSkyColor(Fallback::Map::getColour("Weather_" + name +"_Sky_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sky_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sky_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sky_Night_Color")) - , mFogColor(Fallback::Map::getColour("Weather_" + name + "_Fog_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Fog_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Fog_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Fog_Night_Color")) - , mAmbientColor(Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Ambient_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Ambient_Night_Color")) - , mSunColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sun_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sun_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sun_Night_Color")) - , mLandFogDepth(Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), - Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), - Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), - Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Night_Depth")) - , mSunDiscSunsetColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Disc_Sunset_Color")) - , mWindSpeed(Fallback::Map::getFloat("Weather_" + name + "_Wind_Speed")) - , mCloudSpeed(Fallback::Map::getFloat("Weather_" + name + "_Cloud_Speed")) - , mGlareView(Fallback::Map::getFloat("Weather_" + name + "_Glare_View")) - , mIsStorm(mWindSpeed > stormWindSpeed) - , mRainSpeed(rainSpeed) - , mRainEntranceSpeed(Fallback::Map::getFloat("Weather_" + name + "_Rain_Entrance_Speed")) - , mRainMaxRaindrops(Fallback::Map::getFloat("Weather_" + name + "_Max_Raindrops")) - , mRainDiameter(Fallback::Map::getFloat("Weather_" + name + "_Rain_Diameter")) - , mRainThreshold(Fallback::Map::getFloat("Weather_" + name + "_Rain_Threshold")) - , mRainMinHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Min")) - , mRainMaxHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Max")) - , mParticleEffect(particleEffect) - , mRainEffect(Fallback::Map::getBool("Weather_" + name + "_Using_Precip") ? "meshes\\raindrop.nif" : "") - , mTransitionDelta(Fallback::Map::getFloat("Weather_" + name + "_Transition_Delta")) - , mCloudsMaximumPercent(Fallback::Map::getFloat("Weather_" + name + "_Clouds_Maximum_Percent")) - , mThunderFrequency(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Frequency")) - , mThunderThreshold(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Threshold")) - , mThunderSoundID() - , mFlashDecrement(Fallback::Map::getFloat("Weather_" + name + "_Flash_Decrement")) - , mFlashBrightness(0.0f) -{ - mDL.FogFactor = dlFactor; - mDL.FogOffset = dlOffset; - mThunderSoundID[0] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_0"); - mThunderSoundID[1] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_1"); - mThunderSoundID[2] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_2"); - mThunderSoundID[3] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_3"); - - // TODO: support weathers that have both "Ambient Loop Sound ID" and "Rain Loop Sound ID", need to play both sounds at the same time. - - if (!mRainEffect.empty()) // NOTE: in vanilla, the weathers with rain seem to be hardcoded; changing Using_Precip has no effect - { - mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Rain_Loop_Sound_ID"); - if (mAmbientLoopSoundID.empty()) // default to "rain" if not set - mAmbientLoopSoundID = "rain"; - } - else - mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Ambient_Loop_Sound_ID"); - - if (Misc::StringUtils::ciEqual(mAmbientLoopSoundID, "None")) - mAmbientLoopSoundID.clear(); -} - -float Weather::transitionDelta() const -{ - // Transition Delta describes how quickly transitioning to the weather in question will take, in Hz. Note that the - // measurement is in real time, not in-game time. - return mTransitionDelta; -} - -float Weather::cloudBlendFactor(const float transitionRatio) const -{ - // Clouds Maximum Percent affects how quickly the sky transitions from one sky texture to the next. - return transitionRatio / mCloudsMaximumPercent; -} - -float Weather::calculateThunder(const float transitionRatio, const float elapsedSeconds, const bool isPaused) -{ - // When paused, the flash brightness remains the same and no new strikes can occur. - if(!isPaused) - { - // Morrowind doesn't appear to do any calculations unless the transition ratio is higher than the Thunder Threshold. - if(transitionRatio >= mThunderThreshold && mThunderFrequency > 0.0f) - { - flashDecrement(elapsedSeconds); - - if(Misc::Rng::rollProbability() <= thunderChance(transitionRatio, elapsedSeconds)) + if (gameHour <= middle) { - lightningAndThunder(); + // fade in + float advance = middle - gameHour; + float factor = 0.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunriseValue, mNightValue, factor); + } + else + { + // fade out + float advance = gameHour - middle; + float factor = 1.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunriseValue, mDayValue, factor); + } + } + // day + else if (gameHour > timeSettings.mDayStart + postSunriseTime && gameHour < timeSettings.mDayEnd - preSunsetTime) + return mDayValue; + // sunset + else if (gameHour >= timeSettings.mDayEnd - preSunsetTime && gameHour <= timeSettings.mNightStart + postSunsetTime) + { + float duration = timeSettings.mNightStart + postSunsetTime - timeSettings.mDayEnd + preSunsetTime; + float middle = timeSettings.mDayEnd - preSunsetTime + duration / 2.f; + + if (gameHour <= middle) + { + // fade in + float advance = middle - gameHour; + float factor = 0.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunsetValue, mDayValue, factor); + } + else + { + // fade out + float advance = gameHour - middle; + float factor = 1.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunsetValue, mNightValue, factor); + } + } + // shut up compiler + return T(); + } + + template class MWWorld::TimeOfDayInterpolator; + template class MWWorld::TimeOfDayInterpolator; + + osg::Vec3f Weather::defaultDirection() + { + static const osg::Vec3f direction = osg::Vec3f(0.f, 1.f, 0.f); + return direction; + } + + Weather::Weather(const std::string& name, + float stormWindSpeed, + float rainSpeed, + float dlFactor, + float dlOffset, + const std::string& particleEffect) + : mCloudTexture(Fallback::Map::getString("Weather_" + name + "_Cloud_Texture")) + , mSkyColor(Fallback::Map::getColour("Weather_" + name +"_Sky_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sky_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sky_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sky_Night_Color")) + , mFogColor(Fallback::Map::getColour("Weather_" + name + "_Fog_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Fog_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Fog_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Fog_Night_Color")) + , mAmbientColor(Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Ambient_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Ambient_Night_Color")) + , mSunColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sun_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sun_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sun_Night_Color")) + , mLandFogDepth(Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), + Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), + Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), + Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Night_Depth")) + , mSunDiscSunsetColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Disc_Sunset_Color")) + , mWindSpeed(Fallback::Map::getFloat("Weather_" + name + "_Wind_Speed")) + , mCloudSpeed(Fallback::Map::getFloat("Weather_" + name + "_Cloud_Speed")) + , mGlareView(Fallback::Map::getFloat("Weather_" + name + "_Glare_View")) + , mIsStorm(mWindSpeed > stormWindSpeed) + , mRainSpeed(rainSpeed) + , mRainEntranceSpeed(Fallback::Map::getFloat("Weather_" + name + "_Rain_Entrance_Speed")) + , mRainMaxRaindrops(Fallback::Map::getFloat("Weather_" + name + "_Max_Raindrops")) + , mRainDiameter(Fallback::Map::getFloat("Weather_" + name + "_Rain_Diameter")) + , mRainThreshold(Fallback::Map::getFloat("Weather_" + name + "_Rain_Threshold")) + , mRainMinHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Min")) + , mRainMaxHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Max")) + , mParticleEffect(particleEffect) + , mRainEffect(Fallback::Map::getBool("Weather_" + name + "_Using_Precip") ? "meshes\\raindrop.nif" : "") + , mStormDirection(Weather::defaultDirection()) + , mTransitionDelta(Fallback::Map::getFloat("Weather_" + name + "_Transition_Delta")) + , mCloudsMaximumPercent(Fallback::Map::getFloat("Weather_" + name + "_Clouds_Maximum_Percent")) + , mThunderFrequency(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Frequency")) + , mThunderThreshold(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Threshold")) + , mThunderSoundID() + , mFlashDecrement(Fallback::Map::getFloat("Weather_" + name + "_Flash_Decrement")) + , mFlashBrightness(0.0f) + { + mDL.FogFactor = dlFactor; + mDL.FogOffset = dlOffset; + mThunderSoundID[0] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_0"); + mThunderSoundID[1] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_1"); + mThunderSoundID[2] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_2"); + mThunderSoundID[3] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_3"); + + // TODO: support weathers that have both "Ambient Loop Sound ID" and "Rain Loop Sound ID", need to play both sounds at the same time. + + if (!mRainEffect.empty()) // NOTE: in vanilla, the weathers with rain seem to be hardcoded; changing Using_Precip has no effect + { + mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Rain_Loop_Sound_ID"); + if (mAmbientLoopSoundID.empty()) // default to "rain" if not set + mAmbientLoopSoundID = "rain"; + } + else + mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Ambient_Loop_Sound_ID"); + + if (Misc::StringUtils::ciEqual(mAmbientLoopSoundID, "None")) + mAmbientLoopSoundID.clear(); + } + + float Weather::transitionDelta() const + { + // Transition Delta describes how quickly transitioning to the weather in question will take, in Hz. Note that the + // measurement is in real time, not in-game time. + return mTransitionDelta; + } + + float Weather::cloudBlendFactor(const float transitionRatio) const + { + // Clouds Maximum Percent affects how quickly the sky transitions from one sky texture to the next. + return transitionRatio / mCloudsMaximumPercent; + } + + float Weather::calculateThunder(const float transitionRatio, const float elapsedSeconds, const bool isPaused) + { + // When paused, the flash brightness remains the same and no new strikes can occur. + if(!isPaused) + { + // Morrowind doesn't appear to do any calculations unless the transition ratio is higher than the Thunder Threshold. + if(transitionRatio >= mThunderThreshold && mThunderFrequency > 0.0f) + { + flashDecrement(elapsedSeconds); + + if(Misc::Rng::rollProbability() <= thunderChance(transitionRatio, elapsedSeconds)) + { + lightningAndThunder(); + } + } + else + { + mFlashBrightness = 0.0f; + } + } + + return mFlashBrightness; + } + + inline void Weather::flashDecrement(const float elapsedSeconds) + { + // The Flash Decrement is measured in whole units per second. This means that if the flash brightness was + // currently 1.0, then it should take approximately 0.25 seconds to decay to 0.0 (the minimum). + float decrement = mFlashDecrement * elapsedSeconds; + mFlashBrightness = decrement > mFlashBrightness ? 0.0f : mFlashBrightness - decrement; + } + + inline float Weather::thunderChance(const float transitionRatio, const float elapsedSeconds) const + { + // This formula is reversed from the observation that with Thunder Frequency set to 1, there are roughly 10 strikes + // per minute. It doesn't appear to be tied to in game time as Timescale doesn't affect it. Various values of + // Thunder Frequency seem to change the average number of strikes in a linear fashion.. During a transition, it appears to + // scaled based on how far past it is past the Thunder Threshold. + float scaleFactor = (transitionRatio - mThunderThreshold) / (1.0f - mThunderThreshold); + return ((mThunderFrequency * 10.0f) / 60.0f) * elapsedSeconds * scaleFactor; + } + + inline void Weather::lightningAndThunder(void) + { + // Morrowind seems to vary the intensity of the brightness based on which of the four sound IDs it selects. + // They appear to go from 0 (brightest, closest) to 3 (faintest, farthest). The value of 0.25 per distance + // was derived by setting the Flash Decrement to 0.1 and measuring how long each value took to decay to 0. + // TODO: Determine the distribution of each distance to see if it's evenly weighted. + unsigned int distance = Misc::Rng::rollDice(4); + // Flash brightness appears additive, since if multiple strikes occur, it takes longer for it to decay to 0. + mFlashBrightness += 1 - (distance * 0.25f); + MWBase::Environment::get().getSoundManager()->playSound(mThunderSoundID[distance], 1.0, 1.0); + } + + RegionWeather::RegionWeather(const ESM::Region& region) + : mWeather(invalidWeatherID) + , mChances() + { + mChances.reserve(10); + mChances.push_back(region.mData.mClear); + mChances.push_back(region.mData.mCloudy); + mChances.push_back(region.mData.mFoggy); + mChances.push_back(region.mData.mOvercast); + mChances.push_back(region.mData.mRain); + mChances.push_back(region.mData.mThunder); + mChances.push_back(region.mData.mAsh); + mChances.push_back(region.mData.mBlight); + mChances.push_back(region.mData.mA); + mChances.push_back(region.mData.mB); + } + + RegionWeather::RegionWeather(const ESM::RegionWeatherState& state) + : mWeather(state.mWeather) + , mChances(state.mChances) + { + } + + RegionWeather::operator ESM::RegionWeatherState() const + { + ESM::RegionWeatherState state = + { + mWeather, + mChances + }; + + return state; + } + + void RegionWeather::setChances(const std::vector& chances) + { + if(mChances.size() < chances.size()) + { + mChances.reserve(chances.size()); + } + + int i = 0; + for(char chance : chances) + { + mChances[i] = chance; + i++; + } + + // Regional weather no longer supports the current type, select a new weather pattern. + if((static_cast(mWeather) >= mChances.size()) || (mChances[mWeather] == 0)) + { + chooseNewWeather(); + } + } + + void RegionWeather::setWeather(int weatherID) + { + mWeather = weatherID; + } + + int RegionWeather::getWeather() + { + // If the region weather was already set (by ChangeWeather, or by a previous call) then just return that value. + // Note that the region weather will be expired periodically when the weather update timer expires. + if(mWeather == invalidWeatherID) + { + chooseNewWeather(); + } + + return mWeather; + } + + void RegionWeather::chooseNewWeather() + { + // All probabilities must add to 100 (responsibility of the user). + // If chances A and B has values 30 and 70 then by generating 100 numbers 1..100, 30% will be lesser or equal 30 + // and 70% will be greater than 30 (in theory). + int chance = Misc::Rng::rollDice(100) + 1; // 1..100 + int sum = 0; + int i = 0; + for(; static_cast(i) < mChances.size(); ++i) + { + sum += mChances[i]; + if(chance <= sum) + { + mWeather = i; + return; + } + } + + // if we hit this path then the chances don't add to 100, choose a default weather instead + mWeather = 0; + } + + MoonModel::MoonModel(const std::string& name) + : mFadeInStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Start")) + , mFadeInFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Finish")) + , mFadeOutStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Start")) + , mFadeOutFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Finish")) + , mAxisOffset(Fallback::Map::getFloat("Moons_" + name + "_Axis_Offset")) + , mSpeed(Fallback::Map::getFloat("Moons_" + name + "_Speed")) + , mDailyIncrement(Fallback::Map::getFloat("Moons_" + name + "_Daily_Increment")) + , mFadeStartAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_Start_Angle")) + , mFadeEndAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_End_Angle")) + , mMoonShadowEarlyFadeAngle(Fallback::Map::getFloat("Moons_" + name + "_Moon_Shadow_Early_Fade_Angle")) + { + // Morrowind appears to have a minimum speed in order to avoid situations where the moon couldn't conceivably + // complete a rotation in a single 24 hour period. The value of 180/23 was deduced from reverse engineering. + mSpeed = std::min(mSpeed, 180.0f / 23.0f); + } + + MWRender::MoonState MoonModel::calculateState(const TimeStamp& gameTime) const + { + float rotationFromHorizon = angle(gameTime); + MWRender::MoonState state = + { + rotationFromHorizon, + mAxisOffset, // Reverse engineered from Morrowind's scene graph rotation matrices. + phase(gameTime), + shadowBlend(rotationFromHorizon), + earlyMoonShadowAlpha(rotationFromHorizon) * hourlyAlpha(gameTime.getHour()) + }; + + return state; + } + + inline float MoonModel::angle(const TimeStamp& gameTime) const + { + // Morrowind's moons start travel on one side of the horizon (let's call it H-rise) and travel 180 degrees to the + // opposite horizon (let's call it H-set). Upon reaching H-set, they reset to H-rise until the next moon rise. + + // When calculating the angle of the moon, several cases have to be taken into account: + // 1. Moon rises and then sets in one day. + // 2. Moon sets and doesn't rise in one day (occurs when the moon rise hour is >= 24). + // 3. Moon sets and then rises in one day. + float moonRiseHourToday = moonRiseHour(gameTime.getDay()); + float moonRiseAngleToday = 0; + + if(gameTime.getHour() < moonRiseHourToday) + { + float moonRiseHourYesterday = moonRiseHour(gameTime.getDay() - 1); + if(moonRiseHourYesterday < 24) + { + float moonRiseAngleYesterday = rotation(24 - moonRiseHourYesterday); + if(moonRiseAngleYesterday < 180) + { + // The moon rose but did not set yesterday, so accumulate yesterday's angle with how much we've travelled today. + moonRiseAngleToday = rotation(gameTime.getHour()) + moonRiseAngleYesterday; + } } } else { - mFlashBrightness = 0.0f; + moonRiseAngleToday = rotation(gameTime.getHour() - moonRiseHourToday); } + + if(moonRiseAngleToday >= 180) + { + // The moon set today, reset the angle to the horizon. + moonRiseAngleToday = 0; + } + + return moonRiseAngleToday; } - return mFlashBrightness; -} + inline float MoonModel::moonRiseHour(unsigned int daysPassed) const + { + // This arises from the start date of 16 Last Seed, 427 + // TODO: Find an alternate formula that doesn't rely on this day being fixed. + static const unsigned int startDay = 16; -inline void Weather::flashDecrement(const float elapsedSeconds) -{ - // The Flash Decrement is measured in whole units per second. This means that if the flash brightness was - // currently 1.0, then it should take approximately 0.25 seconds to decay to 0.0 (the minimum). - float decrement = mFlashDecrement * elapsedSeconds; - mFlashBrightness = decrement > mFlashBrightness ? 0.0f : mFlashBrightness - decrement; -} + // This odd formula arises from the fact that on 16 Last Seed, 17 increments have occurred, meaning + // that upon starting a new game, it must only calculate the moon phase as far back as 1 Last Seed. + // Note that we don't modulo after adding the latest daily increment because other calculations need to + // know if doing so would cause the moon rise to be postponed until the next day (which happens when + // the moon rise hour is >= 24 in Morrowind). + return mDailyIncrement + std::fmod((daysPassed - 1 + startDay) * mDailyIncrement, 24.0f); + } -inline float Weather::thunderChance(const float transitionRatio, const float elapsedSeconds) const -{ - // This formula is reversed from the observation that with Thunder Frequency set to 1, there are roughly 10 strikes - // per minute. It doesn't appear to be tied to in game time as Timescale doesn't affect it. Various values of - // Thunder Frequency seem to change the average number of strikes in a linear fashion.. During a transition, it appears to - // scaled based on how far past it is past the Thunder Threshold. - float scaleFactor = (transitionRatio - mThunderThreshold) / (1.0f - mThunderThreshold); - return ((mThunderFrequency * 10.0f) / 60.0f) * elapsedSeconds * scaleFactor; -} + inline float MoonModel::rotation(float hours) const + { + // 15 degrees per hour was reverse engineered from the rotation matrices of the Morrowind scene graph. + // Note that this correlates to 360 / 24, which is a full rotation every 24 hours, so speed is a measure + // of whole rotations that could be completed in a day. + return 15.0f * mSpeed * hours; + } -inline void Weather::lightningAndThunder(void) -{ - // Morrowind seems to vary the intensity of the brightness based on which of the four sound IDs it selects. - // They appear to go from 0 (brightest, closest) to 3 (faintest, farthest). The value of 0.25 per distance - // was derived by setting the Flash Decrement to 0.1 and measuring how long each value took to decay to 0. - // TODO: Determine the distribution of each distance to see if it's evenly weighted. - unsigned int distance = Misc::Rng::rollDice(4); - // Flash brightness appears additive, since if multiple strikes occur, it takes longer for it to decay to 0. - mFlashBrightness += 1 - (distance * 0.25f); - MWBase::Environment::get().getSoundManager()->playSound(mThunderSoundID[distance], 1.0, 1.0); -} + MWRender::MoonState::Phase MoonModel::phase(const TimeStamp& gameTime) const + { + // Morrowind starts with a full moon on 16 Last Seed and then begins to wane 17 Last Seed, working on 3 day phase cycle. -RegionWeather::RegionWeather(const ESM::Region& region) - : mWeather(invalidWeatherID) - , mChances() -{ - mChances.reserve(10); - mChances.push_back(region.mData.mClear); - mChances.push_back(region.mData.mCloudy); - mChances.push_back(region.mData.mFoggy); - mChances.push_back(region.mData.mOvercast); - mChances.push_back(region.mData.mRain); - mChances.push_back(region.mData.mThunder); - mChances.push_back(region.mData.mAsh); - mChances.push_back(region.mData.mBlight); - mChances.push_back(region.mData.mA); - mChances.push_back(region.mData.mB); -} + // If the moon didn't rise yet today, use yesterday's moon phase. + if(gameTime.getHour() < moonRiseHour(gameTime.getDay())) + return static_cast((gameTime.getDay() / 3) % 8); + else + return static_cast(((gameTime.getDay() + 1) / 3) % 8); + } -RegionWeather::RegionWeather(const ESM::RegionWeatherState& state) - : mWeather(state.mWeather) - , mChances(state.mChances) -{ -} + inline float MoonModel::shadowBlend(float angle) const + { + // The Fade End Angle and Fade Start Angle describe a region where the moon transitions from a solid disk + // that is roughly the color of the sky, to a textured surface. + // Depending on the current angle, the following values describe the ratio between the textured moon + // and the solid disk: + // 1. From Fade End Angle 1 to Fade Start Angle 1 (during moon rise): 0..1 + // 2. From Fade Start Angle 1 to Fade Start Angle 2 (between moon rise and moon set): 1 (textured) + // 3. From Fade Start Angle 2 to Fade End Angle 2 (during moon set): 1..0 + // 4. From Fade End Angle 2 to Fade End Angle 1 (between moon set and moon rise): 0 (solid disk) + float fadeAngle = mFadeStartAngle - mFadeEndAngle; + float fadeEndAngle2 = 180.0f - mFadeEndAngle; + float fadeStartAngle2 = 180.0f - mFadeStartAngle; + if((angle >= mFadeEndAngle) && (angle < mFadeStartAngle)) + return (angle - mFadeEndAngle) / fadeAngle; + else if((angle >= mFadeStartAngle) && (angle < fadeStartAngle2)) + return 1.0f; + else if((angle >= fadeStartAngle2) && (angle < fadeEndAngle2)) + return (fadeEndAngle2 - angle) / fadeAngle; + else + return 0.0f; + } -RegionWeather::operator ESM::RegionWeatherState() const -{ - ESM::RegionWeatherState state = - { - mWeather, - mChances + inline float MoonModel::hourlyAlpha(float gameHour) const + { + // The Fade Out Start / Finish and Fade In Start / Finish describe the hours at which the moon + // appears and disappears. + // Depending on the current hour, the following values describe how transparent the moon is. + // 1. From Fade Out Start to Fade Out Finish: 1..0 + // 2. From Fade Out Finish to Fade In Start: 0 (transparent) + // 3. From Fade In Start to Fade In Finish: 0..1 + // 4. From Fade In Finish to Fade Out Start: 1 (solid) + if((gameHour >= mFadeOutStart) && (gameHour < mFadeOutFinish)) + return (mFadeOutFinish - gameHour) / (mFadeOutFinish - mFadeOutStart); + else if((gameHour >= mFadeOutFinish) && (gameHour < mFadeInStart)) + return 0.0f; + else if((gameHour >= mFadeInStart) && (gameHour < mFadeInFinish)) + return (gameHour - mFadeInStart) / (mFadeInFinish - mFadeInStart); + else + return 1.0f; + } + + inline float MoonModel::earlyMoonShadowAlpha(float angle) const + { + // The Moon Shadow Early Fade Angle describes an arc relative to Fade End Angle. + // Depending on the current angle, the following values describe how transparent the moon is. + // 1. From Moon Shadow Early Fade Angle 1 to Fade End Angle 1 (during moon rise): 0..1 + // 2. From Fade End Angle 1 to Fade End Angle 2 (between moon rise and moon set): 1 (solid) + // 3. From Fade End Angle 2 to Moon Shadow Early Fade Angle 2 (during moon set): 1..0 + // 4. From Moon Shadow Early Fade Angle 2 to Moon Shadow Early Fade Angle 1: 0 (transparent) + float moonShadowEarlyFadeAngle1 = mFadeEndAngle - mMoonShadowEarlyFadeAngle; + float fadeEndAngle2 = 180.0f - mFadeEndAngle; + float moonShadowEarlyFadeAngle2 = fadeEndAngle2 + mMoonShadowEarlyFadeAngle; + if((angle >= moonShadowEarlyFadeAngle1) && (angle < mFadeEndAngle)) + return (angle - moonShadowEarlyFadeAngle1) / mMoonShadowEarlyFadeAngle; + else if((angle >= mFadeEndAngle) && (angle < fadeEndAngle2)) + return 1.0f; + else if((angle >= fadeEndAngle2) && (angle < moonShadowEarlyFadeAngle2)) + return (moonShadowEarlyFadeAngle2 - angle) / mMoonShadowEarlyFadeAngle; + else + return 0.0f; + } + + WeatherManager::WeatherManager(MWRender::RenderingManager& rendering, MWWorld::ESMStore& store) + : mStore(store) + , mRendering(rendering) + , mSunriseTime(Fallback::Map::getFloat("Weather_Sunrise_Time")) + , mSunsetTime(Fallback::Map::getFloat("Weather_Sunset_Time")) + , mSunriseDuration(Fallback::Map::getFloat("Weather_Sunrise_Duration")) + , mSunsetDuration(Fallback::Map::getFloat("Weather_Sunset_Duration")) + , mSunPreSunsetTime(Fallback::Map::getFloat("Weather_Sun_Pre-Sunset_Time")) + , mNightFade(0, 0, 0, 1) + , mHoursBetweenWeatherChanges(Fallback::Map::getFloat("Weather_Hours_Between_Weather_Changes")) + , mRainSpeed(Fallback::Map::getFloat("Weather_Precip_Gravity")) + , mUnderwaterFog(Fallback::Map::getFloat("Water_UnderwaterSunriseFog"), + Fallback::Map::getFloat("Water_UnderwaterDayFog"), + Fallback::Map::getFloat("Water_UnderwaterSunsetFog"), + Fallback::Map::getFloat("Water_UnderwaterNightFog")) + , mWeatherSettings() + , mMasser("Masser") + , mSecunda("Secunda") + , mWindSpeed(0.f) + , mCurrentWindSpeed(0.f) + , mNextWindSpeed(0.f) + , mIsStorm(false) + , mPrecipitation(false) + , mStormDirection(Weather::defaultDirection()) + , mCurrentRegion() + , mTimePassed(0) + , mFastForward(false) + , mWeatherUpdateTime(mHoursBetweenWeatherChanges) + , mTransitionFactor(0) + , mNightDayMode(Default) + , mCurrentWeather(0) + , mNextWeather(0) + , mQueuedWeather(0) + , mRegions() + , mResult() + , mAmbientSound(nullptr) + , mPlayingSoundID() + { + mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration; + mTimeSettings.mNightEnd = mSunriseTime; + mTimeSettings.mDayStart = mSunriseTime + mSunriseDuration; + mTimeSettings.mDayEnd = mSunsetTime; + + mTimeSettings.addSetting("Sky"); + mTimeSettings.addSetting("Ambient"); + mTimeSettings.addSetting("Fog"); + mTimeSettings.addSetting("Sun"); + + // Morrowind handles stars settings differently for other ones + mTimeSettings.mStarsPostSunsetStart = Fallback::Map::getFloat("Weather_Stars_Post-Sunset_Start"); + mTimeSettings.mStarsPreSunriseFinish = Fallback::Map::getFloat("Weather_Stars_Pre-Sunrise_Finish"); + mTimeSettings.mStarsFadingDuration = Fallback::Map::getFloat("Weather_Stars_Fading_Duration"); + + WeatherSetting starSetting = { + mTimeSettings.mStarsPreSunriseFinish, + mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPreSunriseFinish, + mTimeSettings.mStarsPostSunsetStart, + mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPostSunsetStart }; - return state; -} + mTimeSettings.mSunriseTransitions["Stars"] = starSetting; -void RegionWeather::setChances(const std::vector& chances) -{ - if(mChances.size() < chances.size()) - { - mChances.reserve(chances.size()); - } + mWeatherSettings.reserve(10); + // These distant land fog factor and offset values are the defaults MGE XE provides. Should be + // provided by settings somewhere? + addWeather("Clear", 1.0f, 0.0f); // 0 + addWeather("Cloudy", 0.9f, 0.0f); // 1 + addWeather("Foggy", 0.2f, 30.0f); // 2 + addWeather("Overcast", 0.7f, 0.0f); // 3 + addWeather("Rain", 0.5f, 10.0f); // 4 + addWeather("Thunderstorm", 0.5f, 20.0f); // 5 + addWeather("Ashstorm", 0.2f, 50.0f, "meshes\\ashcloud.nif"); // 6 + addWeather("Blight", 0.2f, 60.0f, "meshes\\blightcloud.nif"); // 7 + addWeather("Snow", 0.5f, 40.0f, "meshes\\snow.nif"); // 8 + addWeather("Blizzard", 0.16f, 70.0f, "meshes\\blizzard.nif"); // 9 - int i = 0; - for(char chance : chances) - { - mChances[i] = chance; - i++; - } - - // Regional weather no longer supports the current type, select a new weather pattern. - if((static_cast(mWeather) >= mChances.size()) || (mChances[mWeather] == 0)) - { - chooseNewWeather(); - } -} - -void RegionWeather::setWeather(int weatherID) -{ - mWeather = weatherID; -} - -int RegionWeather::getWeather() -{ - // If the region weather was already set (by ChangeWeather, or by a previous call) then just return that value. - // Note that the region weather will be expired periodically when the weather update timer expires. - if(mWeather == invalidWeatherID) - { - chooseNewWeather(); - } - - return mWeather; -} - -void RegionWeather::chooseNewWeather() -{ - // All probabilities must add to 100 (responsibility of the user). - // If chances A and B has values 30 and 70 then by generating 100 numbers 1..100, 30% will be lesser or equal 30 - // and 70% will be greater than 30 (in theory). - int chance = Misc::Rng::rollDice(100) + 1; // 1..100 - int sum = 0; - int i = 0; - for(; static_cast(i) < mChances.size(); ++i) - { - sum += mChances[i]; - if(chance <= sum) + Store::iterator it = store.get().begin(); + for(; it != store.get().end(); ++it) { - mWeather = i; - return; + std::string regionID = Misc::StringUtils::lowerCase(it->mId); + mRegions.insert(std::make_pair(regionID, RegionWeather(*it))); } + + forceWeather(0); } - // if we hit this path then the chances don't add to 100, choose a default weather instead - mWeather = 0; -} - -MoonModel::MoonModel(const std::string& name) - : mFadeInStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Start")) - , mFadeInFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Finish")) - , mFadeOutStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Start")) - , mFadeOutFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Finish")) - , mAxisOffset(Fallback::Map::getFloat("Moons_" + name + "_Axis_Offset")) - , mSpeed(Fallback::Map::getFloat("Moons_" + name + "_Speed")) - , mDailyIncrement(Fallback::Map::getFloat("Moons_" + name + "_Daily_Increment")) - , mFadeStartAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_Start_Angle")) - , mFadeEndAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_End_Angle")) - , mMoonShadowEarlyFadeAngle(Fallback::Map::getFloat("Moons_" + name + "_Moon_Shadow_Early_Fade_Angle")) -{ - // Morrowind appears to have a minimum speed in order to avoid situations where the moon couldn't conceivably - // complete a rotation in a single 24 hour period. The value of 180/23 was deduced from reverse engineering. - mSpeed = std::min(mSpeed, 180.0f / 23.0f); -} - -MWRender::MoonState MoonModel::calculateState(const TimeStamp& gameTime) const -{ - float rotationFromHorizon = angle(gameTime); - MWRender::MoonState state = - { - rotationFromHorizon, - mAxisOffset, // Reverse engineered from Morrowind's scene graph rotation matrices. - phase(gameTime), - shadowBlend(rotationFromHorizon), - earlyMoonShadowAlpha(rotationFromHorizon) * hourlyAlpha(gameTime.getHour()) - }; - - return state; -} - -inline float MoonModel::angle(const TimeStamp& gameTime) const -{ - // Morrowind's moons start travel on one side of the horizon (let's call it H-rise) and travel 180 degrees to the - // opposite horizon (let's call it H-set). Upon reaching H-set, they reset to H-rise until the next moon rise. - - // When calculating the angle of the moon, several cases have to be taken into account: - // 1. Moon rises and then sets in one day. - // 2. Moon sets and doesn't rise in one day (occurs when the moon rise hour is >= 24). - // 3. Moon sets and then rises in one day. - float moonRiseHourToday = moonRiseHour(gameTime.getDay()); - float moonRiseAngleToday = 0; - - if(gameTime.getHour() < moonRiseHourToday) + WeatherManager::~WeatherManager() { - float moonRiseHourYesterday = moonRiseHour(gameTime.getDay() - 1); - if(moonRiseHourYesterday < 24) + stopSounds(); + } + + void WeatherManager::changeWeather(const std::string& regionID, const unsigned int weatherID) + { + // In Morrowind, this seems to have the following behavior, when applied to the current region: + // - When there is no transition in progress, start transitioning to the new weather. + // - If there is a transition in progress, queue up the transition and process it when the current one completes. + // - If there is a transition in progress, and a queued transition, overwrite the queued transition. + // - If multiple calls to ChangeWeather are made while paused (console up), only the last call will be used, + // meaning that if there was no transition in progress, only the last ChangeWeather will be processed. + // If the region isn't current, Morrowind will store the new weather for the region in question. + + if(weatherID < mWeatherSettings.size()) { - float moonRiseAngleYesterday = rotation(24 - moonRiseHourYesterday); - if(moonRiseAngleYesterday < 180) + std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); + std::map::iterator it = mRegions.find(lowerCaseRegionID); + if(it != mRegions.end()) { - // The moon rose but did not set yesterday, so accumulate yesterday's angle with how much we've travelled today. - moonRiseAngleToday = rotation(gameTime.getHour()) + moonRiseAngleYesterday; + it->second.setWeather(weatherID); + regionalWeatherChanged(it->first, it->second); } } } - else + + void WeatherManager::modRegion(const std::string& regionID, const std::vector& chances) { - moonRiseAngleToday = rotation(gameTime.getHour() - moonRiseHourToday); - } + // Sets the region's probability for various weather patterns. Note that this appears to be saved permanently. + // In Morrowind, this seems to have the following behavior when applied to the current region: + // - If the region supports the current weather, no change in current weather occurs. + // - If the region no longer supports the current weather, and there is no transition in progress, begin to + // transition to a new supported weather type. + // - If the region no longer supports the current weather, and there is a transition in progress, queue a + // transition to a new supported weather type. - if(moonRiseAngleToday >= 180) - { - // The moon set today, reset the angle to the horizon. - moonRiseAngleToday = 0; - } - - return moonRiseAngleToday; -} - -inline float MoonModel::moonRiseHour(unsigned int daysPassed) const -{ - // This arises from the start date of 16 Last Seed, 427 - // TODO: Find an alternate formula that doesn't rely on this day being fixed. - static const unsigned int startDay = 16; - - // This odd formula arises from the fact that on 16 Last Seed, 17 increments have occurred, meaning - // that upon starting a new game, it must only calculate the moon phase as far back as 1 Last Seed. - // Note that we don't modulo after adding the latest daily increment because other calculations need to - // know if doing so would cause the moon rise to be postponed until the next day (which happens when - // the moon rise hour is >= 24 in Morrowind). - return mDailyIncrement + std::fmod((daysPassed - 1 + startDay) * mDailyIncrement, 24.0f); -} - -inline float MoonModel::rotation(float hours) const -{ - // 15 degrees per hour was reverse engineered from the rotation matrices of the Morrowind scene graph. - // Note that this correlates to 360 / 24, which is a full rotation every 24 hours, so speed is a measure - // of whole rotations that could be completed in a day. - return 15.0f * mSpeed * hours; -} - -MWRender::MoonState::Phase MoonModel::phase(const TimeStamp& gameTime) const -{ - // Morrowind starts with a full moon on 16 Last Seed and then begins to wane 17 Last Seed, working on 3 day phase cycle. - - // If the moon didn't rise yet today, use yesterday's moon phase. - if(gameTime.getHour() < moonRiseHour(gameTime.getDay())) - return static_cast((gameTime.getDay() / 3) % 8); - else - return static_cast(((gameTime.getDay() + 1) / 3) % 8); -} - -inline float MoonModel::shadowBlend(float angle) const -{ - // The Fade End Angle and Fade Start Angle describe a region where the moon transitions from a solid disk - // that is roughly the color of the sky, to a textured surface. - // Depending on the current angle, the following values describe the ratio between the textured moon - // and the solid disk: - // 1. From Fade End Angle 1 to Fade Start Angle 1 (during moon rise): 0..1 - // 2. From Fade Start Angle 1 to Fade Start Angle 2 (between moon rise and moon set): 1 (textured) - // 3. From Fade Start Angle 2 to Fade End Angle 2 (during moon set): 1..0 - // 4. From Fade End Angle 2 to Fade End Angle 1 (between moon set and moon rise): 0 (solid disk) - float fadeAngle = mFadeStartAngle - mFadeEndAngle; - float fadeEndAngle2 = 180.0f - mFadeEndAngle; - float fadeStartAngle2 = 180.0f - mFadeStartAngle; - if((angle >= mFadeEndAngle) && (angle < mFadeStartAngle)) - return (angle - mFadeEndAngle) / fadeAngle; - else if((angle >= mFadeStartAngle) && (angle < fadeStartAngle2)) - return 1.0f; - else if((angle >= fadeStartAngle2) && (angle < fadeEndAngle2)) - return (fadeEndAngle2 - angle) / fadeAngle; - else - return 0.0f; -} - -inline float MoonModel::hourlyAlpha(float gameHour) const -{ - // The Fade Out Start / Finish and Fade In Start / Finish describe the hours at which the moon - // appears and disappears. - // Depending on the current hour, the following values describe how transparent the moon is. - // 1. From Fade Out Start to Fade Out Finish: 1..0 - // 2. From Fade Out Finish to Fade In Start: 0 (transparent) - // 3. From Fade In Start to Fade In Finish: 0..1 - // 4. From Fade In Finish to Fade Out Start: 1 (solid) - if((gameHour >= mFadeOutStart) && (gameHour < mFadeOutFinish)) - return (mFadeOutFinish - gameHour) / (mFadeOutFinish - mFadeOutStart); - else if((gameHour >= mFadeOutFinish) && (gameHour < mFadeInStart)) - return 0.0f; - else if((gameHour >= mFadeInStart) && (gameHour < mFadeInFinish)) - return (gameHour - mFadeInStart) / (mFadeInFinish - mFadeInStart); - else - return 1.0f; -} - -inline float MoonModel::earlyMoonShadowAlpha(float angle) const -{ - // The Moon Shadow Early Fade Angle describes an arc relative to Fade End Angle. - // Depending on the current angle, the following values describe how transparent the moon is. - // 1. From Moon Shadow Early Fade Angle 1 to Fade End Angle 1 (during moon rise): 0..1 - // 2. From Fade End Angle 1 to Fade End Angle 2 (between moon rise and moon set): 1 (solid) - // 3. From Fade End Angle 2 to Moon Shadow Early Fade Angle 2 (during moon set): 1..0 - // 4. From Moon Shadow Early Fade Angle 2 to Moon Shadow Early Fade Angle 1: 0 (transparent) - float moonShadowEarlyFadeAngle1 = mFadeEndAngle - mMoonShadowEarlyFadeAngle; - float fadeEndAngle2 = 180.0f - mFadeEndAngle; - float moonShadowEarlyFadeAngle2 = fadeEndAngle2 + mMoonShadowEarlyFadeAngle; - if((angle >= moonShadowEarlyFadeAngle1) && (angle < mFadeEndAngle)) - return (angle - moonShadowEarlyFadeAngle1) / mMoonShadowEarlyFadeAngle; - else if((angle >= mFadeEndAngle) && (angle < fadeEndAngle2)) - return 1.0f; - else if((angle >= fadeEndAngle2) && (angle < moonShadowEarlyFadeAngle2)) - return (moonShadowEarlyFadeAngle2 - angle) / mMoonShadowEarlyFadeAngle; - else - return 0.0f; -} - -WeatherManager::WeatherManager(MWRender::RenderingManager& rendering, MWWorld::ESMStore& store) - : mStore(store) - , mRendering(rendering) - , mSunriseTime(Fallback::Map::getFloat("Weather_Sunrise_Time")) - , mSunsetTime(Fallback::Map::getFloat("Weather_Sunset_Time")) - , mSunriseDuration(Fallback::Map::getFloat("Weather_Sunrise_Duration")) - , mSunsetDuration(Fallback::Map::getFloat("Weather_Sunset_Duration")) - , mSunPreSunsetTime(Fallback::Map::getFloat("Weather_Sun_Pre-Sunset_Time")) - , mNightFade(0, 0, 0, 1) - , mHoursBetweenWeatherChanges(Fallback::Map::getFloat("Weather_Hours_Between_Weather_Changes")) - , mRainSpeed(Fallback::Map::getFloat("Weather_Precip_Gravity")) - , mUnderwaterFog(Fallback::Map::getFloat("Water_UnderwaterSunriseFog"), - Fallback::Map::getFloat("Water_UnderwaterDayFog"), - Fallback::Map::getFloat("Water_UnderwaterSunsetFog"), - Fallback::Map::getFloat("Water_UnderwaterNightFog")) - , mWeatherSettings() - , mMasser("Masser") - , mSecunda("Secunda") - , mWindSpeed(0.f) - , mCurrentWindSpeed(0.f) - , mNextWindSpeed(0.f) - , mIsStorm(false) - , mPrecipitation(false) - , mStormDirection(0,1,0) - , mCurrentRegion() - , mTimePassed(0) - , mFastForward(false) - , mWeatherUpdateTime(mHoursBetweenWeatherChanges) - , mTransitionFactor(0) - , mNightDayMode(Default) - , mCurrentWeather(0) - , mNextWeather(0) - , mQueuedWeather(0) - , mRegions() - , mResult() - , mAmbientSound(nullptr) - , mPlayingSoundID() -{ - mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration; - mTimeSettings.mNightEnd = mSunriseTime; - mTimeSettings.mDayStart = mSunriseTime + mSunriseDuration; - mTimeSettings.mDayEnd = mSunsetTime; - - mTimeSettings.addSetting("Sky"); - mTimeSettings.addSetting("Ambient"); - mTimeSettings.addSetting("Fog"); - mTimeSettings.addSetting("Sun"); - - // Morrowind handles stars settings differently for other ones - mTimeSettings.mStarsPostSunsetStart = Fallback::Map::getFloat("Weather_Stars_Post-Sunset_Start"); - mTimeSettings.mStarsPreSunriseFinish = Fallback::Map::getFloat("Weather_Stars_Pre-Sunrise_Finish"); - mTimeSettings.mStarsFadingDuration = Fallback::Map::getFloat("Weather_Stars_Fading_Duration"); - - WeatherSetting starSetting = { - mTimeSettings.mStarsPreSunriseFinish, - mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPreSunriseFinish, - mTimeSettings.mStarsPostSunsetStart, - mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPostSunsetStart - }; - - mTimeSettings.mSunriseTransitions["Stars"] = starSetting; - - mWeatherSettings.reserve(10); - // These distant land fog factor and offset values are the defaults MGE XE provides. Should be - // provided by settings somewhere? - addWeather("Clear", 1.0f, 0.0f); // 0 - addWeather("Cloudy", 0.9f, 0.0f); // 1 - addWeather("Foggy", 0.2f, 30.0f); // 2 - addWeather("Overcast", 0.7f, 0.0f); // 3 - addWeather("Rain", 0.5f, 10.0f); // 4 - addWeather("Thunderstorm", 0.5f, 20.0f); // 5 - addWeather("Ashstorm", 0.2f, 50.0f, "meshes\\ashcloud.nif"); // 6 - addWeather("Blight", 0.2f, 60.0f, "meshes\\blightcloud.nif"); // 7 - addWeather("Snow", 0.5f, 40.0f, "meshes\\snow.nif"); // 8 - addWeather("Blizzard", 0.16f, 70.0f, "meshes\\blizzard.nif"); // 9 - - Store::iterator it = store.get().begin(); - for(; it != store.get().end(); ++it) - { - std::string regionID = Misc::StringUtils::lowerCase(it->mId); - mRegions.insert(std::make_pair(regionID, RegionWeather(*it))); - } - - forceWeather(0); -} - -WeatherManager::~WeatherManager() -{ - stopSounds(); -} - -void WeatherManager::changeWeather(const std::string& regionID, const unsigned int weatherID) -{ - // In Morrowind, this seems to have the following behavior, when applied to the current region: - // - When there is no transition in progress, start transitioning to the new weather. - // - If there is a transition in progress, queue up the transition and process it when the current one completes. - // - If there is a transition in progress, and a queued transition, overwrite the queued transition. - // - If multiple calls to ChangeWeather are made while paused (console up), only the last call will be used, - // meaning that if there was no transition in progress, only the last ChangeWeather will be processed. - // If the region isn't current, Morrowind will store the new weather for the region in question. - - if(weatherID < mWeatherSettings.size()) - { std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); std::map::iterator it = mRegions.find(lowerCaseRegionID); if(it != mRegions.end()) { - it->second.setWeather(weatherID); + it->second.setChances(chances); regionalWeatherChanged(it->first, it->second); } } -} -void WeatherManager::modRegion(const std::string& regionID, const std::vector& chances) -{ - // Sets the region's probability for various weather patterns. Note that this appears to be saved permanently. - // In Morrowind, this seems to have the following behavior when applied to the current region: - // - If the region supports the current weather, no change in current weather occurs. - // - If the region no longer supports the current weather, and there is no transition in progress, begin to - // transition to a new supported weather type. - // - If the region no longer supports the current weather, and there is a transition in progress, queue a - // transition to a new supported weather type. - - std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); - std::map::iterator it = mRegions.find(lowerCaseRegionID); - if(it != mRegions.end()) + void WeatherManager::playerTeleported(const std::string& playerRegion, bool isExterior) { - it->second.setChances(chances); - regionalWeatherChanged(it->first, it->second); - } -} - -void WeatherManager::playerTeleported(const std::string& playerRegion, bool isExterior) -{ - // If the player teleports to an outdoors cell in a new region (for instance, by travelling), the weather needs to - // be changed immediately, and any transitions for the previous region discarded. - { - std::map::iterator it = mRegions.find(playerRegion); - if(it != mRegions.end() && playerRegion != mCurrentRegion) + // If the player teleports to an outdoors cell in a new region (for instance, by travelling), the weather needs to + // be changed immediately, and any transitions for the previous region discarded. { - mCurrentRegion = playerRegion; - forceWeather(it->second.getWeather()); - } - } -} - -float WeatherManager::calculateWindSpeed(int weatherId, float currentSpeed) -{ - float targetSpeed = std::min(8.0f * mWeatherSettings[weatherId].mWindSpeed, 70.f); - if (currentSpeed == 0.f) - currentSpeed = targetSpeed; - - float multiplier = mWeatherSettings[weatherId].mRainEffect.empty() ? 1.f : 0.5f; - float updatedSpeed = (Misc::Rng::rollClosedProbability() - 0.5f) * multiplier * targetSpeed + currentSpeed; - - if (updatedSpeed > 0.5f * targetSpeed && updatedSpeed < 2.f * targetSpeed) - currentSpeed = updatedSpeed; - - return currentSpeed; -} - -void WeatherManager::update(float duration, bool paused, const TimeStamp& time, bool isExterior) -{ - MWWorld::ConstPtr player = MWMechanics::getPlayer(); - - if(!paused || mFastForward) - { - // Add new transitions when either the player's current external region changes. - std::string playerRegion = Misc::StringUtils::lowerCase(player.getCell()->getCell()->mRegion); - if(updateWeatherTime() || updateWeatherRegion(playerRegion)) - { - std::map::iterator it = mRegions.find(mCurrentRegion); - if(it != mRegions.end()) + std::map::iterator it = mRegions.find(playerRegion); + if(it != mRegions.end() && playerRegion != mCurrentRegion) { - addWeatherTransition(it->second.getWeather()); + mCurrentRegion = playerRegion; + forceWeather(it->second.getWeather()); } } - - updateWeatherTransitions(duration); } - bool isDay = time.getHour() >= mSunriseTime && time.getHour() <= mTimeSettings.mNightStart; - if (isExterior && !isDay) - mNightDayMode = ExteriorNight; - else if (!isExterior && isDay && mWeatherSettings[mCurrentWeather].mGlareView >= 0.5f) - mNightDayMode = InteriorDay; - else - mNightDayMode = Default; - - if(!isExterior) + float WeatherManager::calculateWindSpeed(int weatherId, float currentSpeed) { - mRendering.setSkyEnabled(false); - stopSounds(); - mWindSpeed = 0.f; - mCurrentWindSpeed = 0.f; - mNextWindSpeed = 0.f; - return; + float targetSpeed = std::min(8.0f * mWeatherSettings[weatherId].mWindSpeed, 70.f); + if (currentSpeed == 0.f) + currentSpeed = targetSpeed; + + float multiplier = mWeatherSettings[weatherId].mRainEffect.empty() ? 1.f : 0.5f; + float updatedSpeed = (Misc::Rng::rollClosedProbability() - 0.5f) * multiplier * targetSpeed + currentSpeed; + + if (updatedSpeed > 0.5f * targetSpeed && updatedSpeed < 2.f * targetSpeed) + currentSpeed = updatedSpeed; + + return currentSpeed; } - calculateWeatherResult(time.getHour(), duration, paused); - - if (!paused) + void WeatherManager::update(float duration, bool paused, const TimeStamp& time, bool isExterior) { - mWindSpeed = mResult.mWindSpeed; - mCurrentWindSpeed = mResult.mCurrentWindSpeed; - mNextWindSpeed = mResult.mNextWindSpeed; - } + MWWorld::ConstPtr player = MWMechanics::getPlayer(); - mIsStorm = mResult.mIsStorm; - - // For some reason Ash Storm is not considered as a precipitation weather in game - mPrecipitation = !(mResult.mParticleEffect.empty() && mResult.mRainEffect.empty()) - && mResult.mParticleEffect != "meshes\\ashcloud.nif"; - - if (mIsStorm) - { - osg::Vec3f stormDirection(0, 1, 0); - if (mResult.mParticleEffect == "meshes\\ashcloud.nif" || mResult.mParticleEffect == "meshes\\blightcloud.nif") + if(!paused || mFastForward) { - osg::Vec3f playerPos (MWMechanics::getPlayer().getRefData().getPosition().asVec3()); - playerPos.z() = 0; - osg::Vec3f redMountainPos (25000, 70000, 0); - stormDirection = (playerPos - redMountainPos); - stormDirection.normalize(); - } - mStormDirection = stormDirection; - mRendering.getSkyManager()->setStormDirection(mStormDirection); - } - - // disable sun during night - if (time.getHour() >= mTimeSettings.mNightStart || time.getHour() <= mSunriseTime) - mRendering.getSkyManager()->sunDisable(); - else - mRendering.getSkyManager()->sunEnable(); - - // Update the sun direction. Run it east to west at a fixed angle from overhead. - // The sun's speed at day and night may differ, since mSunriseTime and mNightStart - // mark when the sun is level with the horizon. - { - // Shift times into a 24-hour window beginning at mSunriseTime... - float adjustedHour = time.getHour(); - float adjustedNightStart = mTimeSettings.mNightStart; - if ( time.getHour() < mSunriseTime ) - adjustedHour += 24.f; - if ( mTimeSettings.mNightStart < mSunriseTime ) - adjustedNightStart += 24.f; - - const bool is_night = adjustedHour >= adjustedNightStart; - const float dayDuration = adjustedNightStart - mSunriseTime; - const float nightDuration = 24.f - dayDuration; - - double theta; - if ( !is_night ) - { - theta = static_cast(osg::PI) * (adjustedHour - mSunriseTime) / dayDuration; - } - else - { - theta = static_cast(osg::PI) - static_cast(osg::PI) * (adjustedHour - adjustedNightStart) / nightDuration; - } - - osg::Vec3f final( - static_cast(cos(theta)), - -0.268f, // approx tan( -15 degrees ) - static_cast(sin(theta))); - mRendering.setSunDirection( final * -1 ); - } - - float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings, "Fog"); - - float peakHour = mSunriseTime + (mTimeSettings.mNightStart - mSunriseTime) / 2; - float glareFade = 1.f; - if (time.getHour() < mSunriseTime || time.getHour() > mTimeSettings.mNightStart) - glareFade = 0.f; - else if (time.getHour() < peakHour) - glareFade = 1.f - (peakHour - time.getHour()) / (peakHour - mSunriseTime); - else - glareFade = 1.f - (time.getHour() - peakHour) / (mTimeSettings.mNightStart - peakHour); - - mRendering.getSkyManager()->setGlareTimeOfDayFade(glareFade); - - mRendering.getSkyManager()->setMasserState(mMasser.calculateState(time)); - mRendering.getSkyManager()->setSecundaState(mSecunda.calculateState(time)); - - mRendering.configureFog(mResult.mFogDepth, underwaterFog, mResult.mDLFogFactor, - mResult.mDLFogOffset/100.0f, mResult.mFogColor); - mRendering.setAmbientColour(mResult.mAmbientColor); - mRendering.setSunColour(mResult.mSunColor, mResult.mSunColor * mResult.mGlareView * glareFade); - - mRendering.getSkyManager()->setWeather(mResult); - - // Play sounds - if (mPlayingSoundID != mResult.mAmbientLoopSoundID) - { - stopSounds(); - if (!mResult.mAmbientLoopSoundID.empty()) - mAmbientSound = MWBase::Environment::get().getSoundManager()->playSound( - mResult.mAmbientLoopSoundID, mResult.mAmbientSoundVolume, 1.0, - MWSound::Type::Sfx, MWSound::PlayMode::Loop - ); - mPlayingSoundID = mResult.mAmbientLoopSoundID; - } - else if (mAmbientSound) - mAmbientSound->setVolume(mResult.mAmbientSoundVolume); -} - -void WeatherManager::stopSounds() -{ - if (mAmbientSound) - MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); - mAmbientSound = nullptr; - mPlayingSoundID.clear(); -} - -float WeatherManager::getWindSpeed() const -{ - return mWindSpeed; -} - -bool WeatherManager::isInStorm() const -{ - return mIsStorm; -} - -osg::Vec3f WeatherManager::getStormDirection() const -{ - return mStormDirection; -} - -void WeatherManager::advanceTime(double hours, bool incremental) -{ - // In Morrowind, when the player sleeps/waits, serves jail time, travels, or trains, all weather transitions are - // immediately applied, regardless of whatever transition time might have been remaining. - mTimePassed += hours; - mFastForward = !incremental ? true : mFastForward; -} - -unsigned int WeatherManager::getWeatherID() const -{ - return mCurrentWeather; -} - -NightDayMode WeatherManager::getNightDayMode() const -{ - return mNightDayMode; -} - -bool WeatherManager::useTorches(float hour) const -{ - bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart; - - return isDark && !mPrecipitation; -} - -void WeatherManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) -{ - ESM::WeatherState state; - state.mCurrentRegion = mCurrentRegion; - state.mTimePassed = mTimePassed; - state.mFastForward = mFastForward; - state.mWeatherUpdateTime = mWeatherUpdateTime; - state.mTransitionFactor = mTransitionFactor; - state.mCurrentWeather = mCurrentWeather; - state.mNextWeather = mNextWeather; - state.mQueuedWeather = mQueuedWeather; - - std::map::iterator it = mRegions.begin(); - for(; it != mRegions.end(); ++it) - { - state.mRegions.insert(std::make_pair(it->first, it->second)); - } - - writer.startRecord(ESM::REC_WTHR); - state.save(writer); - writer.endRecord(ESM::REC_WTHR); -} - -bool WeatherManager::readRecord(ESM::ESMReader& reader, uint32_t type) -{ - if(ESM::REC_WTHR == type) - { - static const int oldestCompatibleSaveFormat = 2; - if(reader.getFormat() < oldestCompatibleSaveFormat) - { - // Weather state isn't really all that important, so to preserve older save games, we'll just discard the - // older weather records, rather than fail to handle the record. - reader.skipRecord(); - } - else - { - ESM::WeatherState state; - state.load(reader); - - mCurrentRegion.swap(state.mCurrentRegion); - mTimePassed = state.mTimePassed; - mFastForward = state.mFastForward; - mWeatherUpdateTime = state.mWeatherUpdateTime; - mTransitionFactor = state.mTransitionFactor; - mCurrentWeather = state.mCurrentWeather; - mNextWeather = state.mNextWeather; - mQueuedWeather = state.mQueuedWeather; - - mRegions.clear(); - importRegions(); - - for(std::map::iterator it = state.mRegions.begin(); it != state.mRegions.end(); ++it) + // Add new transitions when either the player's current external region changes. + std::string playerRegion = Misc::StringUtils::lowerCase(player.getCell()->getCell()->mRegion); + if(updateWeatherTime() || updateWeatherRegion(playerRegion)) { - std::map::iterator found = mRegions.find(it->first); - if (found != mRegions.end()) + std::map::iterator it = mRegions.find(mCurrentRegion); + if(it != mRegions.end()) { - found->second = RegionWeather(it->second); + addWeatherTransition(it->second.getWeather()); } } + + updateWeatherTransitions(duration); } - return true; - } + bool isDay = time.getHour() >= mSunriseTime && time.getHour() <= mTimeSettings.mNightStart; + if (isExterior && !isDay) + mNightDayMode = ExteriorNight; + else if (!isExterior && isDay && mWeatherSettings[mCurrentWeather].mGlareView >= 0.5f) + mNightDayMode = InteriorDay; + else + mNightDayMode = Default; - return false; -} - -void WeatherManager::clear() -{ - stopSounds(); - - mCurrentRegion = ""; - mTimePassed = 0.0f; - mWeatherUpdateTime = 0.0f; - forceWeather(0); - mRegions.clear(); - importRegions(); -} - -inline void WeatherManager::addWeather(const std::string& name, - float dlFactor, float dlOffset, - const std::string& particleEffect) -{ - static const float fStromWindSpeed = mStore.get().find("fStromWindSpeed")->mValue.getFloat(); - - Weather weather(name, fStromWindSpeed, mRainSpeed, dlFactor, dlOffset, particleEffect); - - mWeatherSettings.push_back(weather); -} - -inline void WeatherManager::importRegions() -{ - for(const ESM::Region& region : mStore.get()) - { - std::string regionID = Misc::StringUtils::lowerCase(region.mId); - mRegions.insert(std::make_pair(regionID, RegionWeather(region))); - } -} - -inline void WeatherManager::regionalWeatherChanged(const std::string& regionID, RegionWeather& region) -{ - // If the region is current, then add a weather transition for it. - MWWorld::ConstPtr player = MWMechanics::getPlayer(); - if(player.isInCell()) - { - if(Misc::StringUtils::ciEqual(regionID, mCurrentRegion)) + if(!isExterior) { - addWeatherTransition(region.getWeather()); - } - } -} - -inline bool WeatherManager::updateWeatherTime() -{ - mWeatherUpdateTime -= mTimePassed; - mTimePassed = 0.0f; - if(mWeatherUpdateTime <= 0.0f) - { - // Expire all regional weather, so that any call to getWeather() will return a new weather ID. - std::map::iterator it = mRegions.begin(); - for(; it != mRegions.end(); ++it) - { - it->second.setWeather(invalidWeatherID); + mRendering.setSkyEnabled(false); + stopSounds(); + mWindSpeed = 0.f; + mCurrentWindSpeed = 0.f; + mNextWindSpeed = 0.f; + return; } - mWeatherUpdateTime += mHoursBetweenWeatherChanges; + calculateWeatherResult(time.getHour(), duration, paused); - return true; - } - - return false; -} - -inline bool WeatherManager::updateWeatherRegion(const std::string& playerRegion) -{ - if(!playerRegion.empty() && playerRegion != mCurrentRegion) - { - mCurrentRegion = playerRegion; - - return true; - } - - return false; -} - -inline void WeatherManager::updateWeatherTransitions(const float elapsedRealSeconds) -{ - // When a player chooses to train, wait, or serves jail time, any transitions will be fast forwarded to the last - // weather type set, regardless of the remaining transition time. - if(!mFastForward && inTransition()) - { - const float delta = mWeatherSettings[mNextWeather].transitionDelta(); - mTransitionFactor -= elapsedRealSeconds * delta; - if(mTransitionFactor <= 0.0f) + if (!paused) { - mCurrentWeather = mNextWeather; - mNextWeather = mQueuedWeather; - mQueuedWeather = invalidWeatherID; + mWindSpeed = mResult.mWindSpeed; + mCurrentWindSpeed = mResult.mCurrentWindSpeed; + mNextWindSpeed = mResult.mNextWindSpeed; + } - // We may have begun processing the queued transition, so we need to apply the remaining time towards it. - if(inTransition()) + mIsStorm = mResult.mIsStorm; + + // For some reason Ash Storm is not considered as a precipitation weather in game + mPrecipitation = !(mResult.mParticleEffect.empty() && mResult.mRainEffect.empty()) + && mResult.mParticleEffect != "meshes\\ashcloud.nif"; + + mStormDirection = calculateStormDirection(mResult.mParticleEffect); + mRendering.getSkyManager()->setStormParticleDirection(mStormDirection); + + // disable sun during night + if (time.getHour() >= mTimeSettings.mNightStart || time.getHour() <= mSunriseTime) + mRendering.getSkyManager()->sunDisable(); + else + mRendering.getSkyManager()->sunEnable(); + + // Update the sun direction. Run it east to west at a fixed angle from overhead. + // The sun's speed at day and night may differ, since mSunriseTime and mNightStart + // mark when the sun is level with the horizon. + { + // Shift times into a 24-hour window beginning at mSunriseTime... + float adjustedHour = time.getHour(); + float adjustedNightStart = mTimeSettings.mNightStart; + if ( time.getHour() < mSunriseTime ) + adjustedHour += 24.f; + if ( mTimeSettings.mNightStart < mSunriseTime ) + adjustedNightStart += 24.f; + + const bool is_night = adjustedHour >= adjustedNightStart; + const float dayDuration = adjustedNightStart - mSunriseTime; + const float nightDuration = 24.f - dayDuration; + + double theta; + if ( !is_night ) { - const float newDelta = mWeatherSettings[mNextWeather].transitionDelta(); - const float remainingSeconds = -(mTransitionFactor / delta); - mTransitionFactor = 1.0f - (remainingSeconds * newDelta); + theta = static_cast(osg::PI) * (adjustedHour - mSunriseTime) / dayDuration; } else { - mTransitionFactor = 0.0f; + theta = static_cast(osg::PI) - static_cast(osg::PI) * (adjustedHour - adjustedNightStart) / nightDuration; + } + + osg::Vec3f final( + static_cast(cos(theta)), + -0.268f, // approx tan( -15 degrees ) + static_cast(sin(theta))); + mRendering.setSunDirection( final * -1 ); + } + + float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings, "Fog"); + + float peakHour = mSunriseTime + (mTimeSettings.mNightStart - mSunriseTime) / 2; + float glareFade = 1.f; + if (time.getHour() < mSunriseTime || time.getHour() > mTimeSettings.mNightStart) + glareFade = 0.f; + else if (time.getHour() < peakHour) + glareFade = 1.f - (peakHour - time.getHour()) / (peakHour - mSunriseTime); + else + glareFade = 1.f - (time.getHour() - peakHour) / (mTimeSettings.mNightStart - peakHour); + + mRendering.getSkyManager()->setGlareTimeOfDayFade(glareFade); + + mRendering.getSkyManager()->setMasserState(mMasser.calculateState(time)); + mRendering.getSkyManager()->setSecundaState(mSecunda.calculateState(time)); + + mRendering.configureFog(mResult.mFogDepth, underwaterFog, mResult.mDLFogFactor, + mResult.mDLFogOffset/100.0f, mResult.mFogColor); + mRendering.setAmbientColour(mResult.mAmbientColor); + mRendering.setSunColour(mResult.mSunColor, mResult.mSunColor * mResult.mGlareView * glareFade); + + mRendering.getSkyManager()->setWeather(mResult); + + // Play sounds + if (mPlayingSoundID != mResult.mAmbientLoopSoundID) + { + stopSounds(); + if (!mResult.mAmbientLoopSoundID.empty()) + mAmbientSound = MWBase::Environment::get().getSoundManager()->playSound( + mResult.mAmbientLoopSoundID, mResult.mAmbientSoundVolume, 1.0, + MWSound::Type::Sfx, MWSound::PlayMode::Loop + ); + mPlayingSoundID = mResult.mAmbientLoopSoundID; + } + else if (mAmbientSound) + mAmbientSound->setVolume(mResult.mAmbientSoundVolume); + } + + void WeatherManager::stopSounds() + { + if (mAmbientSound) + MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); + mAmbientSound = nullptr; + mPlayingSoundID.clear(); + } + + float WeatherManager::getWindSpeed() const + { + return mWindSpeed; + } + + bool WeatherManager::isInStorm() const + { + return mIsStorm; + } + + osg::Vec3f WeatherManager::getStormDirection() const + { + return mStormDirection; + } + + void WeatherManager::advanceTime(double hours, bool incremental) + { + // In Morrowind, when the player sleeps/waits, serves jail time, travels, or trains, all weather transitions are + // immediately applied, regardless of whatever transition time might have been remaining. + mTimePassed += hours; + mFastForward = !incremental ? true : mFastForward; + } + + unsigned int WeatherManager::getWeatherID() const + { + return mCurrentWeather; + } + + NightDayMode WeatherManager::getNightDayMode() const + { + return mNightDayMode; + } + + bool WeatherManager::useTorches(float hour) const + { + bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart; + + return isDark && !mPrecipitation; + } + + void WeatherManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) + { + ESM::WeatherState state; + state.mCurrentRegion = mCurrentRegion; + state.mTimePassed = mTimePassed; + state.mFastForward = mFastForward; + state.mWeatherUpdateTime = mWeatherUpdateTime; + state.mTransitionFactor = mTransitionFactor; + state.mCurrentWeather = mCurrentWeather; + state.mNextWeather = mNextWeather; + state.mQueuedWeather = mQueuedWeather; + + std::map::iterator it = mRegions.begin(); + for(; it != mRegions.end(); ++it) + { + state.mRegions.insert(std::make_pair(it->first, it->second)); + } + + writer.startRecord(ESM::REC_WTHR); + state.save(writer); + writer.endRecord(ESM::REC_WTHR); + } + + bool WeatherManager::readRecord(ESM::ESMReader& reader, uint32_t type) + { + if(ESM::REC_WTHR == type) + { + static const int oldestCompatibleSaveFormat = 2; + if(reader.getFormat() < oldestCompatibleSaveFormat) + { + // Weather state isn't really all that important, so to preserve older save games, we'll just discard the + // older weather records, rather than fail to handle the record. + reader.skipRecord(); + } + else + { + ESM::WeatherState state; + state.load(reader); + + mCurrentRegion.swap(state.mCurrentRegion); + mTimePassed = state.mTimePassed; + mFastForward = state.mFastForward; + mWeatherUpdateTime = state.mWeatherUpdateTime; + mTransitionFactor = state.mTransitionFactor; + mCurrentWeather = state.mCurrentWeather; + mNextWeather = state.mNextWeather; + mQueuedWeather = state.mQueuedWeather; + + mRegions.clear(); + importRegions(); + + for(std::map::iterator it = state.mRegions.begin(); it != state.mRegions.end(); ++it) + { + std::map::iterator found = mRegions.find(it->first); + if (found != mRegions.end()) + { + found->second = RegionWeather(it->second); + } + } + } + + return true; + } + + return false; + } + + void WeatherManager::clear() + { + stopSounds(); + + mCurrentRegion = ""; + mTimePassed = 0.0f; + mWeatherUpdateTime = 0.0f; + forceWeather(0); + mRegions.clear(); + importRegions(); + } + + inline void WeatherManager::addWeather(const std::string& name, + float dlFactor, float dlOffset, + const std::string& particleEffect) + { + static const float fStromWindSpeed = mStore.get().find("fStromWindSpeed")->mValue.getFloat(); + + Weather weather(name, fStromWindSpeed, mRainSpeed, dlFactor, dlOffset, particleEffect); + + mWeatherSettings.push_back(weather); + } + + inline void WeatherManager::importRegions() + { + for(const ESM::Region& region : mStore.get()) + { + std::string regionID = Misc::StringUtils::lowerCase(region.mId); + mRegions.insert(std::make_pair(regionID, RegionWeather(region))); + } + } + + inline void WeatherManager::regionalWeatherChanged(const std::string& regionID, RegionWeather& region) + { + // If the region is current, then add a weather transition for it. + MWWorld::ConstPtr player = MWMechanics::getPlayer(); + if(player.isInCell()) + { + if(Misc::StringUtils::ciEqual(regionID, mCurrentRegion)) + { + addWeatherTransition(region.getWeather()); } } } - else + + inline bool WeatherManager::updateWeatherTime() { - if(mQueuedWeather != invalidWeatherID) + mWeatherUpdateTime -= mTimePassed; + mTimePassed = 0.0f; + if(mWeatherUpdateTime <= 0.0f) { - mCurrentWeather = mQueuedWeather; - } - else if(mNextWeather != invalidWeatherID) - { - mCurrentWeather = mNextWeather; + // Expire all regional weather, so that any call to getWeather() will return a new weather ID. + std::map::iterator it = mRegions.begin(); + for(; it != mRegions.end(); ++it) + { + it->second.setWeather(invalidWeatherID); + } + + mWeatherUpdateTime += mHoursBetweenWeatherChanges; + + return true; } + return false; + } + + inline bool WeatherManager::updateWeatherRegion(const std::string& playerRegion) + { + if(!playerRegion.empty() && playerRegion != mCurrentRegion) + { + mCurrentRegion = playerRegion; + + return true; + } + + return false; + } + + inline void WeatherManager::updateWeatherTransitions(const float elapsedRealSeconds) + { + // When a player chooses to train, wait, or serves jail time, any transitions will be fast forwarded to the last + // weather type set, regardless of the remaining transition time. + if(!mFastForward && inTransition()) + { + const float delta = mWeatherSettings[mNextWeather].transitionDelta(); + mTransitionFactor -= elapsedRealSeconds * delta; + if(mTransitionFactor <= 0.0f) + { + mCurrentWeather = mNextWeather; + mNextWeather = mQueuedWeather; + mQueuedWeather = invalidWeatherID; + + // We may have begun processing the queued transition, so we need to apply the remaining time towards it. + if(inTransition()) + { + const float newDelta = mWeatherSettings[mNextWeather].transitionDelta(); + const float remainingSeconds = -(mTransitionFactor / delta); + mTransitionFactor = 1.0f - (remainingSeconds * newDelta); + } + else + { + mTransitionFactor = 0.0f; + } + } + } + else + { + if(mQueuedWeather != invalidWeatherID) + { + mCurrentWeather = mQueuedWeather; + } + else if(mNextWeather != invalidWeatherID) + { + mCurrentWeather = mNextWeather; + } + + mNextWeather = invalidWeatherID; + mQueuedWeather = invalidWeatherID; + mFastForward = false; + } + } + + inline void WeatherManager::forceWeather(const int weatherID) + { + mTransitionFactor = 0.0f; + mCurrentWeather = weatherID; mNextWeather = invalidWeatherID; mQueuedWeather = invalidWeatherID; - mFastForward = false; } -} -inline void WeatherManager::forceWeather(const int weatherID) -{ - mTransitionFactor = 0.0f; - mCurrentWeather = weatherID; - mNextWeather = invalidWeatherID; - mQueuedWeather = invalidWeatherID; -} - -inline bool WeatherManager::inTransition() -{ - return mNextWeather != invalidWeatherID; -} - -inline void WeatherManager::addWeatherTransition(const int weatherID) -{ - // In order to work like ChangeWeather expects, this method begins transitioning to the new weather immediately if - // no transition is in progress, otherwise it queues it to be transitioned. - - assert(weatherID >= 0 && static_cast(weatherID) < mWeatherSettings.size()); - - if(!inTransition() && (weatherID != mCurrentWeather)) + inline bool WeatherManager::inTransition() { - mNextWeather = weatherID; - mTransitionFactor = 1.0f; + return mNextWeather != invalidWeatherID; } - else if(inTransition() && (weatherID != mNextWeather)) + + inline void WeatherManager::addWeatherTransition(const int weatherID) { - mQueuedWeather = weatherID; + // In order to work like ChangeWeather expects, this method begins transitioning to the new weather immediately if + // no transition is in progress, otherwise it queues it to be transitioned. + + assert(weatherID >= 0 && static_cast(weatherID) < mWeatherSettings.size()); + + if(!inTransition() && (weatherID != mCurrentWeather)) + { + mNextWeather = weatherID; + mTransitionFactor = 1.0f; + } + else if(inTransition() && (weatherID != mNextWeather)) + { + mQueuedWeather = weatherID; + } } -} -inline void WeatherManager::calculateWeatherResult(const float gameHour, - const float elapsedSeconds, - const bool isPaused) -{ - float flash = 0.0f; - if(!inTransition()) + inline void WeatherManager::calculateWeatherResult(const float gameHour, + const float elapsedSeconds, + const bool isPaused) { - calculateResult(mCurrentWeather, gameHour); - flash = mWeatherSettings[mCurrentWeather].calculateThunder(1.0f, elapsedSeconds, isPaused); + float flash = 0.0f; + if(!inTransition()) + { + calculateResult(mCurrentWeather, gameHour); + flash = mWeatherSettings[mCurrentWeather].calculateThunder(1.0f, elapsedSeconds, isPaused); + } + else + { + calculateTransitionResult(1 - mTransitionFactor, gameHour); + float currentFlash = mWeatherSettings[mCurrentWeather].calculateThunder(mTransitionFactor, + elapsedSeconds, + isPaused); + float nextFlash = mWeatherSettings[mNextWeather].calculateThunder(1 - mTransitionFactor, + elapsedSeconds, + isPaused); + flash = currentFlash + nextFlash; + } + osg::Vec4f flashColor(flash, flash, flash, 0.0f); + + mResult.mFogColor += flashColor; + mResult.mAmbientColor += flashColor; + mResult.mSunColor += flashColor; } - else + + inline void WeatherManager::calculateResult(const int weatherID, const float gameHour) { - calculateTransitionResult(1 - mTransitionFactor, gameHour); - float currentFlash = mWeatherSettings[mCurrentWeather].calculateThunder(mTransitionFactor, - elapsedSeconds, - isPaused); - float nextFlash = mWeatherSettings[mNextWeather].calculateThunder(1 - mTransitionFactor, - elapsedSeconds, - isPaused); - flash = currentFlash + nextFlash; - } - osg::Vec4f flashColor(flash, flash, flash, 0.0f); + const Weather& current = mWeatherSettings[weatherID]; - mResult.mFogColor += flashColor; - mResult.mAmbientColor += flashColor; - mResult.mSunColor += flashColor; -} + mResult.mCloudTexture = current.mCloudTexture; + mResult.mCloudBlendFactor = 0; + mResult.mNextWindSpeed = 0; + mResult.mWindSpeed = mResult.mCurrentWindSpeed = calculateWindSpeed(weatherID, mWindSpeed); + mResult.mBaseWindSpeed = mWeatherSettings[weatherID].mWindSpeed; -inline void WeatherManager::calculateResult(const int weatherID, const float gameHour) -{ - const Weather& current = mWeatherSettings[weatherID]; + mResult.mCloudSpeed = current.mCloudSpeed; + mResult.mGlareView = current.mGlareView; + mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; + mResult.mAmbientSoundVolume = 1.f; + mResult.mPrecipitationAlpha = 1.f; - mResult.mCloudTexture = current.mCloudTexture; - mResult.mCloudBlendFactor = 0; - mResult.mNextWindSpeed = 0; - mResult.mWindSpeed = mResult.mCurrentWindSpeed = calculateWindSpeed(weatherID, mWindSpeed); - mResult.mBaseWindSpeed = mWeatherSettings[weatherID].mWindSpeed; - - mResult.mCloudSpeed = current.mCloudSpeed; - mResult.mGlareView = current.mGlareView; - mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; - mResult.mAmbientSoundVolume = 1.f; - mResult.mPrecipitationAlpha = 1.f; - - mResult.mIsStorm = current.mIsStorm; - - mResult.mRainSpeed = current.mRainSpeed; - mResult.mRainEntranceSpeed = current.mRainEntranceSpeed; - mResult.mRainDiameter = current.mRainDiameter; - mResult.mRainMinHeight = current.mRainMinHeight; - mResult.mRainMaxHeight = current.mRainMaxHeight; - mResult.mRainMaxRaindrops = current.mRainMaxRaindrops; - - mResult.mParticleEffect = current.mParticleEffect; - mResult.mRainEffect = current.mRainEffect; - - mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart + mTimeSettings.mStarsPostSunsetStart - mTimeSettings.mStarsFadingDuration); - - mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings, "Fog"); - mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings, "Fog"); - mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings, "Ambient"); - mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings, "Sun"); - mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings, "Sky"); - mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings, "Stars"); - mResult.mDLFogFactor = current.mDL.FogFactor; - mResult.mDLFogOffset = current.mDL.FogOffset; - - WeatherSetting setting = mTimeSettings.getSetting("Sun"); - float preSunsetTime = setting.mPreSunsetTime; - - if (gameHour >= mTimeSettings.mDayEnd - preSunsetTime) - { - float factor = 1.f; - if (preSunsetTime > 0) - factor = (gameHour - (mTimeSettings.mDayEnd - preSunsetTime)) / preSunsetTime; - factor = std::min(1.f, factor); - mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor); - // The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because - // MW applied the color to the ambient term as well. After the ambient and emissive terms are added together, the fixed pipeline - // would then clamp the total lighting to (1,1,1). A noticeable change in color tone can be observed when only one of the color components gets clamped. - // Unfortunately that means we can't use the INI color as is, have to replicate the above nonsense. - mResult.mSunDiscColor = mResult.mSunDiscColor + osg::componentMultiply(mResult.mSunDiscColor, mResult.mAmbientColor); - for (int i=0; i<3; ++i) - mResult.mSunDiscColor[i] = std::min(1.f, mResult.mSunDiscColor[i]); - } - else - mResult.mSunDiscColor = osg::Vec4f(1,1,1,1); - - if (gameHour >= mTimeSettings.mDayEnd) - { - // sunset - float fade = std::min(1.f, (gameHour - mTimeSettings.mDayEnd) / (mTimeSettings.mNightStart - mTimeSettings.mDayEnd)); - fade = fade*fade; - mResult.mSunDiscColor.a() = 1.f - fade; - } - else if (gameHour >= mTimeSettings.mNightEnd && gameHour <= mTimeSettings.mNightEnd + mSunriseDuration / 2.f) - { - // sunrise - mResult.mSunDiscColor.a() = gameHour - mTimeSettings.mNightEnd; - } - else - mResult.mSunDiscColor.a() = 1; - -} - -inline void WeatherManager::calculateTransitionResult(const float factor, const float gameHour) -{ - calculateResult(mCurrentWeather, gameHour); - const MWRender::WeatherResult current = mResult; - calculateResult(mNextWeather, gameHour); - const MWRender::WeatherResult other = mResult; - - mResult.mCloudTexture = current.mCloudTexture; - mResult.mNextCloudTexture = other.mCloudTexture; - mResult.mCloudBlendFactor = mWeatherSettings[mNextWeather].cloudBlendFactor(factor); - - mResult.mFogColor = lerp(current.mFogColor, other.mFogColor, factor); - mResult.mSunColor = lerp(current.mSunColor, other.mSunColor, factor); - mResult.mSkyColor = lerp(current.mSkyColor, other.mSkyColor, factor); - - mResult.mAmbientColor = lerp(current.mAmbientColor, other.mAmbientColor, factor); - mResult.mSunDiscColor = lerp(current.mSunDiscColor, other.mSunDiscColor, factor); - mResult.mFogDepth = lerp(current.mFogDepth, other.mFogDepth, factor); - mResult.mDLFogFactor = lerp(current.mDLFogFactor, other.mDLFogFactor, factor); - mResult.mDLFogOffset = lerp(current.mDLFogOffset, other.mDLFogOffset, factor); - - mResult.mCurrentWindSpeed = calculateWindSpeed(mCurrentWeather, mCurrentWindSpeed); - mResult.mNextWindSpeed = calculateWindSpeed(mNextWeather, mNextWindSpeed); - mResult.mBaseWindSpeed = lerp(current.mBaseWindSpeed, other.mBaseWindSpeed, factor); - - mResult.mWindSpeed = lerp(mResult.mCurrentWindSpeed, mResult.mNextWindSpeed, factor); - mResult.mCloudSpeed = lerp(current.mCloudSpeed, other.mCloudSpeed, factor); - mResult.mGlareView = lerp(current.mGlareView, other.mGlareView, factor); - mResult.mNightFade = lerp(current.mNightFade, other.mNightFade, factor); - - mResult.mNight = current.mNight; - - float threshold = mWeatherSettings[mNextWeather].mRainThreshold; - if (threshold <= 0) - threshold = 0.5f; - - if(factor < threshold) - { mResult.mIsStorm = current.mIsStorm; - mResult.mParticleEffect = current.mParticleEffect; - mResult.mRainEffect = current.mRainEffect; + mResult.mRainSpeed = current.mRainSpeed; mResult.mRainEntranceSpeed = current.mRainEntranceSpeed; - mResult.mAmbientSoundVolume = 1 - factor / threshold; - mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; - mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; mResult.mRainDiameter = current.mRainDiameter; mResult.mRainMinHeight = current.mRainMinHeight; mResult.mRainMaxHeight = current.mRainMaxHeight; mResult.mRainMaxRaindrops = current.mRainMaxRaindrops; - } - else - { - mResult.mIsStorm = other.mIsStorm; - mResult.mParticleEffect = other.mParticleEffect; - mResult.mRainEffect = other.mRainEffect; - mResult.mRainSpeed = other.mRainSpeed; - mResult.mRainEntranceSpeed = other.mRainEntranceSpeed; - mResult.mAmbientSoundVolume = (factor - threshold) / (1 - threshold); - mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; - mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID; - mResult.mRainDiameter = other.mRainDiameter; - mResult.mRainMinHeight = other.mRainMinHeight; - mResult.mRainMaxHeight = other.mRainMaxHeight; - mResult.mRainMaxRaindrops = other.mRainMaxRaindrops; + mResult.mParticleEffect = current.mParticleEffect; + mResult.mRainEffect = current.mRainEffect; + + mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart + mTimeSettings.mStarsPostSunsetStart - mTimeSettings.mStarsFadingDuration); + + mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings, "Fog"); + mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings, "Fog"); + mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings, "Ambient"); + mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings, "Sun"); + mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings, "Sky"); + mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings, "Stars"); + mResult.mDLFogFactor = current.mDL.FogFactor; + mResult.mDLFogOffset = current.mDL.FogOffset; + + WeatherSetting setting = mTimeSettings.getSetting("Sun"); + float preSunsetTime = setting.mPreSunsetTime; + + if (gameHour >= mTimeSettings.mDayEnd - preSunsetTime) + { + float factor = 1.f; + if (preSunsetTime > 0) + factor = (gameHour - (mTimeSettings.mDayEnd - preSunsetTime)) / preSunsetTime; + factor = std::min(1.f, factor); + mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor); + // The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because + // MW applied the color to the ambient term as well. After the ambient and emissive terms are added together, the fixed pipeline + // would then clamp the total lighting to (1,1,1). A noticeable change in color tone can be observed when only one of the color components gets clamped. + // Unfortunately that means we can't use the INI color as is, have to replicate the above nonsense. + mResult.mSunDiscColor = mResult.mSunDiscColor + osg::componentMultiply(mResult.mSunDiscColor, mResult.mAmbientColor); + for (int i=0; i<3; ++i) + mResult.mSunDiscColor[i] = std::min(1.f, mResult.mSunDiscColor[i]); + } + else + mResult.mSunDiscColor = osg::Vec4f(1,1,1,1); + + if (gameHour >= mTimeSettings.mDayEnd) + { + // sunset + float fade = std::min(1.f, (gameHour - mTimeSettings.mDayEnd) / (mTimeSettings.mNightStart - mTimeSettings.mDayEnd)); + fade = fade*fade; + mResult.mSunDiscColor.a() = 1.f - fade; + } + else if (gameHour >= mTimeSettings.mNightEnd && gameHour <= mTimeSettings.mNightEnd + mSunriseDuration / 2.f) + { + // sunrise + mResult.mSunDiscColor.a() = gameHour - mTimeSettings.mNightEnd; + } + else + mResult.mSunDiscColor.a() = 1; + + mResult.mStormDirection = calculateStormDirection(mResult.mParticleEffect); + } + + inline void WeatherManager::calculateTransitionResult(const float factor, const float gameHour) + { + calculateResult(mCurrentWeather, gameHour); + const MWRender::WeatherResult current = mResult; + calculateResult(mNextWeather, gameHour); + const MWRender::WeatherResult other = mResult; + + mResult.mStormDirection = current.mStormDirection; + mResult.mNextStormDirection = other.mStormDirection; + + mResult.mCloudTexture = current.mCloudTexture; + mResult.mNextCloudTexture = other.mCloudTexture; + mResult.mCloudBlendFactor = mWeatherSettings[mNextWeather].cloudBlendFactor(factor); + + mResult.mFogColor = lerp(current.mFogColor, other.mFogColor, factor); + mResult.mSunColor = lerp(current.mSunColor, other.mSunColor, factor); + mResult.mSkyColor = lerp(current.mSkyColor, other.mSkyColor, factor); + + mResult.mAmbientColor = lerp(current.mAmbientColor, other.mAmbientColor, factor); + mResult.mSunDiscColor = lerp(current.mSunDiscColor, other.mSunDiscColor, factor); + mResult.mFogDepth = lerp(current.mFogDepth, other.mFogDepth, factor); + mResult.mDLFogFactor = lerp(current.mDLFogFactor, other.mDLFogFactor, factor); + mResult.mDLFogOffset = lerp(current.mDLFogOffset, other.mDLFogOffset, factor); + + mResult.mCurrentWindSpeed = calculateWindSpeed(mCurrentWeather, mCurrentWindSpeed); + mResult.mNextWindSpeed = calculateWindSpeed(mNextWeather, mNextWindSpeed); + mResult.mBaseWindSpeed = lerp(current.mBaseWindSpeed, other.mBaseWindSpeed, factor); + + mResult.mWindSpeed = lerp(mResult.mCurrentWindSpeed, mResult.mNextWindSpeed, factor); + mResult.mCloudSpeed = lerp(current.mCloudSpeed, other.mCloudSpeed, factor); + mResult.mGlareView = lerp(current.mGlareView, other.mGlareView, factor); + mResult.mNightFade = lerp(current.mNightFade, other.mNightFade, factor); + + mResult.mNight = current.mNight; + + float threshold = mWeatherSettings[mNextWeather].mRainThreshold; + if (threshold <= 0.f) + threshold = 0.5f; + + if(factor < threshold) + { + mResult.mIsStorm = current.mIsStorm; + mResult.mParticleEffect = current.mParticleEffect; + mResult.mRainEffect = current.mRainEffect; + mResult.mRainSpeed = current.mRainSpeed; + mResult.mRainEntranceSpeed = current.mRainEntranceSpeed; + mResult.mAmbientSoundVolume = 1.f - factor / threshold; + mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; + mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; + mResult.mRainDiameter = current.mRainDiameter; + mResult.mRainMinHeight = current.mRainMinHeight; + mResult.mRainMaxHeight = current.mRainMaxHeight; + mResult.mRainMaxRaindrops = current.mRainMaxRaindrops; + } + else + { + mResult.mIsStorm = other.mIsStorm; + mResult.mParticleEffect = other.mParticleEffect; + mResult.mRainEffect = other.mRainEffect; + mResult.mRainSpeed = other.mRainSpeed; + mResult.mRainEntranceSpeed = other.mRainEntranceSpeed; + mResult.mAmbientSoundVolume = (factor - threshold) / (1 - threshold); + mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; + mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID; + + mResult.mRainDiameter = other.mRainDiameter; + mResult.mRainMinHeight = other.mRainMinHeight; + mResult.mRainMaxHeight = other.mRainMaxHeight; + mResult.mRainMaxRaindrops = other.mRainMaxRaindrops; + } } } diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index a3928465c4..21b2fae9f8 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -115,6 +115,8 @@ namespace MWWorld class Weather { public: + static osg::Vec3f defaultDirection(); + Weather(const std::string& name, float stormWindSpeed, float rainSpeed, @@ -189,6 +191,8 @@ namespace MWWorld std::string mRainEffect; + osg::Vec3f mStormDirection; + // Note: For Weather Blight, there is a "Disease Chance" (=0.1) setting. But according to MWSFD this feature // is broken in the vanilla game and was disabled. diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index ccd78170bb..cc982e4621 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -111,6 +111,17 @@ namespace MWWorld LoadersContainer mLoaders; }; + struct OMWScriptsLoader : public ContentLoader + { + ESMStore& mStore; + OMWScriptsLoader(Loading::Listener& listener, ESMStore& store) : ContentLoader(listener), mStore(store) {} + void load(const boost::filesystem::path& filepath, int& index) override + { + ContentLoader::load(filepath.filename(), index); + mStore.addOMWScripts(filepath.string()); + } + }; + void World::adjustSky() { if (mSky && (isCellExterior() || isCellQuasiExterior())) @@ -156,6 +167,9 @@ namespace MWWorld gameContentLoader.addLoader(".omwaddon", &esmLoader); gameContentLoader.addLoader(".project", &esmLoader); + OMWScriptsLoader omwScriptsLoader(*listener, mStore); + gameContentLoader.addLoader(".omwscripts", &omwScriptsLoader); + loadContentFiles(fileCollections, contentFiles, groundcoverFiles, gameContentLoader); listener->loadingOff(); @@ -164,6 +178,10 @@ namespace MWWorld if (mEsm[0].getFormat() == 0) ensureNeededRecords(); + // TODO: We can and should validate before we call loadContentFiles(). + // Currently we validate here to prevent merge conflicts with groundcover ESMStore fixes. + validateMasterFiles(mEsm); + mCurrentDate.reset(new DateTimeManager()); fillGlobalVariables(); @@ -393,6 +411,23 @@ namespace MWWorld } } + void World::validateMasterFiles(const std::vector& readers) + { + for (const auto& esm : readers) + { + assert(esm.getGameFiles().size() == esm.getParentFileIndices().size()); + for (unsigned int i=0; i gmst; @@ -1244,14 +1279,14 @@ namespace MWWorld return moveObject(ptr, cell, position, movePhysics); } - MWWorld::Ptr World::moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) + MWWorld::Ptr World::moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec) { auto* actor = mPhysics->getActor(ptr); osg::Vec3f newpos = ptr.getRefData().getPosition().asVec3() + vec; if (actor) - actor->adjustPosition(vec, ignoreCollisions); + actor->adjustPosition(vec); if (ptr.getClass().isActor()) - return moveObject(ptr, newpos, false, moveToActive && ptr != getPlayerPtr()); + return moveObject(ptr, newpos, false, ptr != getPlayerPtr()); return moveObject(ptr, newpos); } @@ -1299,7 +1334,7 @@ namespace MWWorld * currently it's done so for rotating the camera, which needs * clamping. */ - objRot[0] = osg::clampBetween(objRot[0], -osg::PIf / 2, osg::PIf / 2); + objRot[0] = std::clamp(objRot[0], -osg::PI_2, osg::PI_2); objRot[1] = Misc::normalizeAngle(objRot[1]); objRot[2] = Misc::normalizeAngle(objRot[2]); } @@ -1866,7 +1901,7 @@ namespace MWWorld const auto& magicEffects = player.getClass().getCreatureStats(player).getMagicEffects(); if (!mGodMode) blind = static_cast(magicEffects.get(ESM::MagicEffect::Blind).getMagnitude()); - MWBase::Environment::get().getWindowManager()->setBlindness(std::max(0, std::min(100, blind))); + MWBase::Environment::get().getWindowManager()->setBlindness(std::clamp(blind, 0, 100)); int nightEye = static_cast(magicEffects.get(ESM::MagicEffect::NightEye).getMagnitude()); mRendering->setNightEyeFactor(std::min(1.f, (nightEye/100.f))); @@ -3791,7 +3826,7 @@ namespace MWWorld cast.mSlot = slot; ESM::EffectList effectsToApply; effectsToApply.mList = applyPair.second; - cast.inflict(applyPair.first, caster, effectsToApply, rangeType, false, true); + cast.inflict(applyPair.first, caster, effectsToApply, rangeType, true); } } @@ -3934,7 +3969,7 @@ namespace MWWorld btVector3 aabbMin; btVector3 aabbMax; - object->getShapeInstance()->getCollisionShape()->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); + object->getShapeInstance()->mCollisionShape->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); const auto toLocal = object->getTransform().inverse(); const auto localFrom = toLocal(Misc::Convert::toBullet(position)); diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 6e48f045c0..afad359cfd 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -157,6 +157,7 @@ namespace MWWorld void updateNavigatorObject(const MWPhysics::Object& object); void ensureNeededRecords(); + void validateMasterFiles(const std::vector& readers); void fillGlobalVariables(); @@ -376,7 +377,7 @@ namespace MWWorld MWWorld::Ptr moveObject (const Ptr& ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) override; ///< @return an updated Ptr - MWWorld::Ptr moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) override; + MWWorld::Ptr moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec) override; ///< @return an updated Ptr void scaleObject (const Ptr& ptr, float scale) override; diff --git a/apps/openmw/options.cpp b/apps/openmw/options.cpp index 62ea0910dd..d68809468c 100644 --- a/apps/openmw/options.cpp +++ b/apps/openmw/options.cpp @@ -40,14 +40,11 @@ namespace OpenMW "set initial cell") ("content", bpo::value()->default_value(Files::EscapeStringVector(), "") - ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon") + ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon/omwscripts") ("groundcover", bpo::value()->default_value(Files::EscapeStringVector(), "") ->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon") - ("lua-scripts", bpo::value()->default_value(Files::EscapeStringVector(), "") - ->multitoken()->composing(), "file(s) with a list of global Lua scripts: omwscripts") - ("no-sound", bpo::value()->implicit_value(true) ->default_value(false), "disable all sounds") diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 0390b0a772..bf235331cf 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -12,6 +12,8 @@ if (GTEST_FOUND AND GMOCK_FOUND) mwdialogue/test_keywordsearch.cpp + mwscript/test_scripts.cpp + esm/test_fixed_string.cpp esm/variant.cpp @@ -20,7 +22,7 @@ if (GTEST_FOUND AND GMOCK_FOUND) lua/test_utilpackage.cpp lua/test_serialization.cpp lua/test_querypackage.cpp - lua/test_omwscriptsparser.cpp + lua/test_configuration.cpp misc/test_stringops.cpp misc/test_endianness.cpp @@ -36,6 +38,10 @@ if (GTEST_FOUND AND GMOCK_FOUND) detournavigator/recastmeshobject.cpp detournavigator/navmeshtilescache.cpp detournavigator/tilecachedrecastmeshmanager.cpp + detournavigator/serialization/binaryreader.cpp + detournavigator/serialization/binarywriter.cpp + detournavigator/serialization/sizeaccumulator.cpp + detournavigator/serialization/integration.cpp settings/parser.cpp diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp index 62d3f9de04..d4bdcb13b8 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/openmw_test_suite/detournavigator/navigator.cpp @@ -117,7 +117,7 @@ namespace osg::ref_ptr makeBulletShapeInstance(std::unique_ptr&& shape) { osg::ref_ptr bulletShape(new Resource::BulletShape); - bulletShape->mCollisionShape = std::move(shape).release(); + bulletShape->mCollisionShape.reset(std::move(shape).release()); return new Resource::BulletShapeInstance(bulletShape); } @@ -466,7 +466,7 @@ namespace }}; std::unique_ptr shapePtr = makeSquareHeightfieldTerrainShape(heightfieldData); shapePtr->setLocalScaling(btVector3(128, 128, 1)); - bulletShape->mCollisionShape = shapePtr.release(); + bulletShape->mCollisionShape.reset(shapePtr.release()); std::array heightfieldDataAvoid {{ -25, -25, -25, -25, -25, @@ -477,12 +477,12 @@ namespace }}; std::unique_ptr shapeAvoidPtr = makeSquareHeightfieldTerrainShape(heightfieldDataAvoid); shapeAvoidPtr->setLocalScaling(btVector3(128, 128, 1)); - bulletShape->mAvoidCollisionShape = shapeAvoidPtr.release(); + bulletShape->mAvoidCollisionShape.reset(shapeAvoidPtr.release()); osg::ref_ptr instance(new Resource::BulletShapeInstance(bulletShape)); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(instance->getCollisionShape()), ObjectShapes(instance), btTransform::getIdentity()); + mNavigator->addObject(ObjectId(instance->mCollisionShape.get()), ObjectShapes(instance), btTransform::getIdentity()); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); diff --git a/apps/openmw_test_suite/detournavigator/serialization/binaryreader.cpp b/apps/openmw_test_suite/detournavigator/serialization/binaryreader.cpp new file mode 100644 index 0000000000..d071326cf5 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/binaryreader.cpp @@ -0,0 +1,67 @@ +#include "format.hpp" + +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator::Serialization; + using namespace DetourNavigator::SerializationTesting; + + TEST(DetourNavigatorSerializationBinaryReaderTest, shouldReadArithmeticTypeValue) + { + std::uint32_t value = 42; + std::vector data(sizeof(value)); + std::memcpy(data.data(), &value, sizeof(value)); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::uint32_t result = 0; + const TestFormat format; + binaryReader(format, result); + EXPECT_EQ(result, 42); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, shouldReadArithmeticTypeRangeValue) + { + const std::size_t count = 3; + std::vector data(sizeof(std::size_t) + count * sizeof(std::uint32_t)); + std::memcpy(data.data(), &count, sizeof(count)); + const std::uint32_t value1 = 960900021; + std::memcpy(data.data() + sizeof(count), &value1, sizeof(std::uint32_t)); + const std::uint32_t value2 = 1235496234; + std::memcpy(data.data() + sizeof(count) + sizeof(std::uint32_t), &value2, sizeof(std::uint32_t)); + const std::uint32_t value3 = 2342038092; + std::memcpy(data.data() + sizeof(count) + 2 * sizeof(std::uint32_t), &value3, sizeof(std::uint32_t)); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::size_t resultCount = 0; + const TestFormat format; + binaryReader(format, resultCount); + std::vector result(resultCount); + binaryReader(format, result.data(), result.size()); + EXPECT_THAT(result, ElementsAre(value1, value2, value3)); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, forNotEnoughDataForArithmeticTypeShouldThrowException) + { + std::vector data(3); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::uint32_t result = 0; + const TestFormat format; + EXPECT_THROW(binaryReader(format, result), std::runtime_error); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, forNotEnoughDataForArithmeticTypeRangeShouldThrowException) + { + std::vector data(7); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::vector values(2); + const TestFormat format; + EXPECT_THROW(binaryReader(format, values.data(), values.size()), std::runtime_error); + } +} diff --git a/apps/openmw_test_suite/detournavigator/serialization/binarywriter.cpp b/apps/openmw_test_suite/detournavigator/serialization/binarywriter.cpp new file mode 100644 index 0000000000..fccc2be3da --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/binarywriter.cpp @@ -0,0 +1,57 @@ +#include "format.hpp" + +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator::Serialization; + using namespace DetourNavigator::SerializationTesting; + + TEST(DetourNavigatorSerializationBinaryWriterTest, shouldWriteArithmeticTypeValue) + { + std::vector result(4); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + const TestFormat format; + binaryWriter(format, std::uint32_t(42)); + EXPECT_THAT(result, ElementsAre(std::byte(42), std::byte(0), std::byte(0), std::byte(0))); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, shouldWriteArithmeticTypeRangeValue) + { + std::vector result(8); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + std::vector values({42, 13}); + const TestFormat format; + binaryWriter(format, values.data(), values.size()); + constexpr std::array expected { + std::byte(42), std::byte(0), std::byte(0), std::byte(0), + std::byte(13), std::byte(0), std::byte(0), std::byte(0), + }; + EXPECT_THAT(result, ElementsAreArray(expected)); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, forNotEnoughSpaceForArithmeticTypeShouldThrowException) + { + std::vector result(3); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + const TestFormat format; + EXPECT_THROW(binaryWriter(format, std::uint32_t(42)), std::runtime_error); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, forNotEnoughSpaceForArithmeticTypeRangeShouldThrowException) + { + std::vector result(7); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + std::vector values({42, 13}); + const TestFormat format; + EXPECT_THROW(binaryWriter(format, values.data(), values.size()), std::runtime_error); + } +} diff --git a/apps/openmw_test_suite/detournavigator/serialization/format.hpp b/apps/openmw_test_suite/detournavigator/serialization/format.hpp new file mode 100644 index 0000000000..7c5e26a0be --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/format.hpp @@ -0,0 +1,75 @@ +#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H +#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H + +#include + +#include +#include + +namespace DetourNavigator::SerializationTesting +{ + struct Pod + { + int mInt = 42; + double mDouble = 3.14; + + friend bool operator==(const Pod& l, const Pod& r) + { + const auto tuple = [] (const Pod& v) { return std::tuple(v.mInt, v.mDouble); }; + return tuple(l) == tuple(r); + } + }; + + enum Enum + { + A, + B, + C, + }; + + struct Composite + { + short mFloatArray[3] = {0}; + std::vector mIntVector; + std::vector mEnumVector; + std::vector mPodVector; + std::size_t mPodDataSize = 0; + std::vector mPodBuffer; + std::size_t mCharDataSize = 0; + std::vector mCharBuffer; + }; + + template + struct TestFormat : Serialization::Format> + { + using Serialization::Format>::operator(); + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, Pod>> + { + visitor(*this, value.mInt); + visitor(*this, value.mDouble); + } + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, Composite>> + { + visitor(*this, value.mFloatArray); + visitor(*this, value.mIntVector); + visitor(*this, value.mEnumVector); + visitor(*this, value.mPodVector); + visitor(*this, value.mPodDataSize); + if constexpr (mode == Serialization::Mode::Read) + value.mPodBuffer.resize(value.mPodDataSize); + visitor(*this, value.mPodBuffer.data(), value.mPodDataSize); + visitor(*this, value.mCharDataSize); + if constexpr (mode == Serialization::Mode::Read) + value.mCharBuffer.resize(value.mCharDataSize); + visitor(*this, value.mCharBuffer.data(), value.mCharDataSize); + } + }; +} + +#endif diff --git a/apps/openmw_test_suite/detournavigator/serialization/integration.cpp b/apps/openmw_test_suite/detournavigator/serialization/integration.cpp new file mode 100644 index 0000000000..e7e8eacc20 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/integration.cpp @@ -0,0 +1,56 @@ +#include "format.hpp" + +#include +#include +#include + +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator::Serialization; + using namespace DetourNavigator::SerializationTesting; + + struct DetourNavigatorSerializationIntegrationTest : Test + { + Composite mComposite; + + DetourNavigatorSerializationIntegrationTest() + { + mComposite.mIntVector = {4, 5, 6}; + mComposite.mEnumVector = {Enum::A, Enum::B, Enum::C}; + mComposite.mPodVector = {Pod {4, 23.87}, Pod {5, -31.76}, Pod {6, 65.12}}; + mComposite.mPodBuffer = {Pod {7, 456.123}, Pod {8, -628.346}}; + mComposite.mPodDataSize = mComposite.mPodBuffer.size(); + std::string charData = "serialization"; + mComposite.mCharBuffer = {charData.begin(), charData.end()}; + mComposite.mCharDataSize = charData.size(); + } + }; + + TEST_F(DetourNavigatorSerializationIntegrationTest, sizeAccumulatorShouldSupportCustomSerializer) + { + SizeAccumulator sizeAccumulator; + TestFormat{}(sizeAccumulator, mComposite); + EXPECT_EQ(sizeAccumulator.value(), 143); + } + + TEST_F(DetourNavigatorSerializationIntegrationTest, binaryReaderShouldDeserializeDataWrittenByBinaryWriter) + { + std::vector data(143); + TestFormat{}(BinaryWriter(data.data(), data.data() + data.size()), mComposite); + Composite result; + TestFormat{}(BinaryReader(data.data(), data.data() + data.size()), result); + EXPECT_EQ(result.mIntVector, mComposite.mIntVector); + EXPECT_EQ(result.mEnumVector, mComposite.mEnumVector); + EXPECT_EQ(result.mPodVector, mComposite.mPodVector); + EXPECT_EQ(result.mPodDataSize, mComposite.mPodDataSize); + EXPECT_EQ(result.mPodBuffer, mComposite.mPodBuffer); + EXPECT_EQ(result.mCharDataSize, mComposite.mCharDataSize); + EXPECT_EQ(result.mCharBuffer, mComposite.mCharBuffer); + } +} diff --git a/apps/openmw_test_suite/detournavigator/serialization/sizeaccumulator.cpp b/apps/openmw_test_suite/detournavigator/serialization/sizeaccumulator.cpp new file mode 100644 index 0000000000..39b7ea8646 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/sizeaccumulator.cpp @@ -0,0 +1,43 @@ +#include "format.hpp" + +#include + +#include + +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator::Serialization; + using namespace DetourNavigator::SerializationTesting; + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldProvideSizeForArithmeticType) + { + SizeAccumulator sizeAccumulator; + constexpr std::monostate format; + sizeAccumulator(format, std::uint32_t()); + EXPECT_EQ(sizeAccumulator.value(), 4); + } + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldProvideSizeForArithmeticTypeRange) + { + SizeAccumulator sizeAccumulator; + const std::uint64_t* const data = nullptr; + const std::size_t count = 3; + const std::monostate format; + sizeAccumulator(format, data, count); + EXPECT_EQ(sizeAccumulator.value(), 24); + } + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldSupportCustomSerializer) + { + SizeAccumulator sizeAccumulator; + const TestFormat format; + sizeAccumulator(format, Pod {}); + EXPECT_EQ(sizeAccumulator.value(), 12); + } +} diff --git a/apps/openmw_test_suite/lua/test_configuration.cpp b/apps/openmw_test_suite/lua/test_configuration.cpp new file mode 100644 index 0000000000..054ea8cbda --- /dev/null +++ b/apps/openmw_test_suite/lua/test_configuration.cpp @@ -0,0 +1,58 @@ +#include "gmock/gmock.h" +#include + +#include + +#include "testing_util.hpp" + +namespace +{ + + TEST(LuaConfigurationTest, ValidConfiguration) + { + ESM::LuaScriptsCfg cfg; + LuaUtil::parseOMWScripts(cfg, R"X( + # Lines starting with '#' are comments + GLOBAL: my_mod/#some_global_script.lua + + # Script that will be automatically attached to the player + PLAYER :my_mod/player.lua + CUSTOM : my_mod/some_other_script.lua + NPC , CREATURE PLAYER : my_mod/some_other_script.lua)X"); + LuaUtil::parseOMWScripts(cfg, ":my_mod/player.LUA \r\nCONTAINER,CUSTOM: my_mod/container.lua\r\n"); + + ASSERT_EQ(cfg.mScripts.size(), 6); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[0]), "GLOBAL : my_mod/#some_global_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[1]), "PLAYER : my_mod/player.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[2]), "CUSTOM : my_mod/some_other_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[3]), "CREATURE NPC PLAYER : my_mod/some_other_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[4]), ": my_mod/player.LUA"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[5]), "CONTAINER CUSTOM : my_mod/container.lua"); + + LuaUtil::ScriptsConfiguration conf; + conf.init(std::move(cfg)); + ASSERT_EQ(conf.size(), 3); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[0]), "GLOBAL : my_mod/#some_global_script.lua"); + // cfg.mScripts[1] is overridden by cfg.mScripts[4] + // cfg.mScripts[2] is overridden by cfg.mScripts[3] + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[1]), "CREATURE NPC PLAYER : my_mod/some_other_script.lua"); + // cfg.mScripts[4] is removed because there are no flags + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[2]), "CONTAINER CUSTOM : my_mod/container.lua"); + + cfg = ESM::LuaScriptsCfg(); + conf.init(std::move(cfg)); + ASSERT_EQ(conf.size(), 0); + } + + TEST(LuaConfigurationTest, Errors) + { + ESM::LuaScriptsCfg cfg; + EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "GLOBAL: something"), + "Lua script should have suffix '.lua', got: GLOBAL: something"); + EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "something.lua"), + "No flags found in: something.lua"); + EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "GLOBAL, PLAYER: something.lua"), + "Global script can not have local flags"); + } + +} diff --git a/apps/openmw_test_suite/lua/test_lua.cpp b/apps/openmw_test_suite/lua/test_lua.cpp index 32d4ea49b8..4b3ecdcb2b 100644 --- a/apps/openmw_test_suite/lua/test_lua.cpp +++ b/apps/openmw_test_suite/lua/test_lua.cpp @@ -58,7 +58,8 @@ return { {"invalid.lua", &invalidScriptFile} }); - LuaUtil::LuaState mLua{mVFS.get()}; + LuaUtil::ScriptsConfiguration mCfg; + LuaUtil::LuaState mLua{mVFS.get(), &mCfg}; }; TEST_F(LuaStateTest, Sandbox) @@ -148,7 +149,7 @@ return { TEST_F(LuaStateTest, ProvideAPI) { - LuaUtil::LuaState lua(mVFS.get()); + LuaUtil::LuaState lua(mVFS.get(), &mCfg); sol::table api1 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api1")); sol::table api2 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api2")); diff --git a/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp b/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp deleted file mode 100644 index b1526ef9b6..0000000000 --- a/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "gmock/gmock.h" -#include - -#include - -#include "testing_util.hpp" - -namespace -{ - using namespace testing; - - TestFile file1( - "#comment.lua\n" - "\n" - "script1.lua\n" - "some mod/Some Script.lua" - ); - TestFile file2( - "#comment.lua\r\n" - "\r\n" - "script2.lua\r\n" - "some other mod/Some Script.lua\r" - ); - TestFile emptyFile(""); - TestFile invalidFile("Invalid file"); - - struct OMWScriptsParserTest : Test - { - std::unique_ptr mVFS = createTestVFS({ - {"file1.omwscripts", &file1}, - {"file2.omwscripts", &file2}, - {"empty.omwscripts", &emptyFile}, - {"invalid.lua", &file1}, - {"invalid.omwscripts", &invalidFile}, - }); - }; - - TEST_F(OMWScriptsParserTest, Basic) - { - internal::CaptureStdout(); - std::vector res = LuaUtil::parseOMWScriptsFiles( - mVFS.get(), {"file2.omwscripts", "empty.omwscripts", "file1.omwscripts"}); - EXPECT_EQ(internal::GetCapturedStdout(), ""); - EXPECT_THAT(res, ElementsAre("script2.lua", "some other mod/Some Script.lua", - "script1.lua", "some mod/Some Script.lua")); - } - - TEST_F(OMWScriptsParserTest, InvalidFiles) - { - internal::CaptureStdout(); - std::vector res = LuaUtil::parseOMWScriptsFiles( - mVFS.get(), {"invalid.lua", "invalid.omwscripts"}); - EXPECT_EQ(internal::GetCapturedStdout(), - "Script list should have suffix '.omwscripts', got: 'invalid.lua'\n" - "Lua script should have suffix '.lua', got: 'Invalid file'\n"); - EXPECT_THAT(res, ElementsAre()); - } - -} diff --git a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp index 8f05138782..344fbb3c78 100644 --- a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp +++ b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp @@ -18,7 +18,10 @@ namespace TestFile testScript(R"X( return { - engineHandlers = { onUpdate = function(dt) print(' update ' .. tostring(dt)) end }, + engineHandlers = { + onUpdate = function(dt) print(' update ' .. tostring(dt)) end, + onLoad = function() print('load') end, + }, eventHandlers = { Event1 = function(eventData) print(' event1 ' .. tostring(eventData.x)) end, Event2 = function(eventData) print(' event2 ' .. tostring(eventData.x)) end, @@ -75,15 +78,25 @@ return { )X"); TestFile overrideInterfaceScript(R"X( -local old = require('openmw.interfaces').TestInterface +local old = nil +local interface = { + fn = function(x) + print('NEW FN', x) + old.fn(x) + end, + value, +} return { interfaceName = "TestInterface", - interface = { - fn = function(x) - print('NEW FN', x) - old.fn(x) - end, - value = old.value + 1 + interface = interface, + engineHandlers = { + onInit = function() print('init') end, + onLoad = function() print('load') end, + onInterfaceOverride = function(oldInterface) + print('override') + old = oldInterface + interface.value = oldInterface.value + 1 + end }, } )X"); @@ -115,7 +128,25 @@ return { {"useInterface.lua", &useInterfaceScript}, }); - LuaUtil::LuaState mLua{mVFS.get()}; + LuaUtil::ScriptsConfiguration mCfg; + LuaUtil::LuaState mLua{mVFS.get(), &mCfg}; + + LuaScriptsContainerTest() + { + ESM::LuaScriptsCfg cfg; + cfg.mScripts.push_back({"invalid.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"incorrect.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"empty.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"test1.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"stopEvent.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"test2.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"loadSave1.lua", "", ESM::LuaScriptCfg::sNPC}); + cfg.mScripts.push_back({"loadSave2.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sNPC}); + cfg.mScripts.push_back({"testInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); + cfg.mScripts.push_back({"overrideInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); + cfg.mScripts.push_back({"useInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); + mCfg.init(std::move(cfg)); + } }; TEST_F(LuaScriptsContainerTest, VerifyStructure) @@ -123,21 +154,21 @@ return { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); { testing::internal::CaptureStdout(); - EXPECT_FALSE(scripts.addNewScript("invalid.lua")); + EXPECT_FALSE(scripts.addCustomScript(*mCfg.findId("invalid.lua"))); std::string output = testing::internal::GetCapturedStdout(); EXPECT_THAT(output, HasSubstr("Can't start Test[invalid.lua]")); } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("incorrect.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("incorrect.lua"))); std::string output = testing::internal::GetCapturedStdout(); EXPECT_THAT(output, HasSubstr("Not supported handler 'incorrectHandler' in Test[incorrect.lua]")); EXPECT_THAT(output, HasSubstr("Not supported section 'incorrectSection' in Test[incorrect.lua]")); } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("empty.lua")); - EXPECT_FALSE(scripts.addNewScript("empty.lua")); // already present + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("empty.lua"))); + EXPECT_FALSE(scripts.addCustomScript(*mCfg.findId("empty.lua"))); // already present EXPECT_EQ(internal::GetCapturedStdout(), ""); } } @@ -146,9 +177,9 @@ return { { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); scripts.update(1.5f); EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\t update 1.5\n" "Test[test2.lua]:\t update 1.5\n"); @@ -157,9 +188,9 @@ return { TEST_F(LuaScriptsContainerTest, CallEvent) { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); std::string X0 = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5)); std::string X1 = LuaUtil::serialize(mLua.sol().create_table_with("x", 1.5)); @@ -204,9 +235,9 @@ return { TEST_F(LuaScriptsContainerTest, RemoveScript) { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); std::string X = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5)); { @@ -221,8 +252,10 @@ return { } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.removeScript("stopEvent.lua")); - EXPECT_FALSE(scripts.removeScript("stopEvent.lua")); // already removed + int stopEventScriptId = *mCfg.findId("stopEvent.lua"); + EXPECT_TRUE(scripts.hasScript(stopEventScriptId)); + scripts.removeScript(stopEventScriptId); + EXPECT_FALSE(scripts.hasScript(stopEventScriptId)); scripts.update(1.5f); scripts.receiveEvent("Event1", X); EXPECT_EQ(internal::GetCapturedStdout(), @@ -233,7 +266,7 @@ return { } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.removeScript("test1.lua")); + scripts.removeScript(*mCfg.findId("test1.lua")); scripts.update(1.5f); scripts.receiveEvent("Event1", X); EXPECT_EQ(internal::GetCapturedStdout(), @@ -242,17 +275,41 @@ return { } } - TEST_F(LuaScriptsContainerTest, Interface) + TEST_F(LuaScriptsContainerTest, AutoStart) { - LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts(&mLua, "Test", ESM::LuaScriptCfg::sPlayer); testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("testInterface.lua")); - EXPECT_TRUE(scripts.addNewScript("overrideInterface.lua")); - EXPECT_TRUE(scripts.addNewScript("useInterface.lua")); - scripts.update(1.5f); - EXPECT_TRUE(scripts.removeScript("overrideInterface.lua")); + scripts.addAutoStartedScripts(); scripts.update(1.5f); EXPECT_EQ(internal::GetCapturedStdout(), + "Test[overrideInterface.lua]:\toverride\n" + "Test[overrideInterface.lua]:\tinit\n" + "Test[overrideInterface.lua]:\tNEW FN\t4.5\n" + "Test[testInterface.lua]:\tFN\t4.5\n"); + } + + TEST_F(LuaScriptsContainerTest, Interface) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test", ESM::LuaScriptCfg::sCreature); + int addIfaceId = *mCfg.findId("testInterface.lua"); + int overrideIfaceId = *mCfg.findId("overrideInterface.lua"); + int useIfaceId = *mCfg.findId("useInterface.lua"); + + testing::internal::CaptureStdout(); + scripts.addAutoStartedScripts(); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), ""); + + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(addIfaceId)); + EXPECT_TRUE(scripts.addCustomScript(overrideIfaceId)); + EXPECT_TRUE(scripts.addCustomScript(useIfaceId)); + scripts.update(1.5f); + scripts.removeScript(overrideIfaceId); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[overrideInterface.lua]:\toverride\n" + "Test[overrideInterface.lua]:\tinit\n" "Test[overrideInterface.lua]:\tNEW FN\t4.5\n" "Test[testInterface.lua]:\tFN\t4.5\n" "Test[testInterface.lua]:\tFN\t3.5\n"); @@ -260,16 +317,12 @@ return { TEST_F(LuaScriptsContainerTest, LoadSave) { - LuaUtil::ScriptsContainer scripts1(&mLua, "Test"); - LuaUtil::ScriptsContainer scripts2(&mLua, "Test"); - LuaUtil::ScriptsContainer scripts3(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts1(&mLua, "Test", ESM::LuaScriptCfg::sNPC); + LuaUtil::ScriptsContainer scripts2(&mLua, "Test", ESM::LuaScriptCfg::sNPC); + LuaUtil::ScriptsContainer scripts3(&mLua, "Test", ESM::LuaScriptCfg::sPlayer); - EXPECT_TRUE(scripts1.addNewScript("loadSave1.lua")); - EXPECT_TRUE(scripts1.addNewScript("test1.lua")); - EXPECT_TRUE(scripts1.addNewScript("loadSave2.lua")); - - EXPECT_TRUE(scripts3.addNewScript("test2.lua")); - EXPECT_TRUE(scripts3.addNewScript("loadSave2.lua")); + scripts1.addAutoStartedScripts(); + EXPECT_TRUE(scripts1.addCustomScript(*mCfg.findId("test1.lua"))); scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with( "n", 1, @@ -282,23 +335,30 @@ return { ESM::LuaScripts data; scripts1.save(data); - scripts2.load(data, true); - scripts3.load(data, false); { testing::internal::CaptureStdout(); + scripts2.load(data); scripts2.receiveEvent("Print", ""); EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test1.lua]:\tload\n" "Test[loadSave2.lua]:\t0.5\t3.5\n" - "Test[test1.lua]:\tprint\n" - "Test[loadSave1.lua]:\t2.5\t1.5\n"); + "Test[loadSave1.lua]:\t2.5\t1.5\n" + "Test[test1.lua]:\tprint\n"); + EXPECT_FALSE(scripts2.hasScript(*mCfg.findId("testInterface.lua"))); } { testing::internal::CaptureStdout(); + scripts3.load(data); scripts3.receiveEvent("Print", ""); EXPECT_EQ(internal::GetCapturedStdout(), + "Ignoring Test[loadSave1.lua]; this script is not allowed here\n" + "Test[test1.lua]:\tload\n" + "Test[overrideInterface.lua]:\toverride\n" + "Test[overrideInterface.lua]:\tinit\n" "Test[loadSave2.lua]:\t0.5\t3.5\n" - "Test[test2.lua]:\tprint\n"); + "Test[test1.lua]:\tprint\n"); + EXPECT_TRUE(scripts3.hasScript(*mCfg.findId("testInterface.lua"))); } } @@ -306,8 +366,13 @@ return { { using TimeUnit = LuaUtil::ScriptsContainer::TimeUnit; LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + int test1Id = *mCfg.findId("test1.lua"); + int test2Id = *mCfg.findId("test2.lua"); + + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(test1Id)); + EXPECT_TRUE(scripts.addCustomScript(test2Id)); + EXPECT_EQ(internal::GetCapturedStdout(), ""); int counter1 = 0, counter2 = 0, counter3 = 0, counter4 = 0; sol::function fn1 = sol::make_object(mLua.sol(), [&]() { counter1++; }); @@ -315,25 +380,25 @@ return { sol::function fn3 = sol::make_object(mLua.sol(), [&](int d) { counter3 += d; }); sol::function fn4 = sol::make_object(mLua.sol(), [&](int d) { counter4 += d; }); - scripts.registerTimerCallback("test1.lua", "A", fn3); - scripts.registerTimerCallback("test1.lua", "B", fn4); - scripts.registerTimerCallback("test2.lua", "B", fn3); - scripts.registerTimerCallback("test2.lua", "A", fn4); + scripts.registerTimerCallback(test1Id, "A", fn3); + scripts.registerTimerCallback(test1Id, "B", fn4); + scripts.registerTimerCallback(test2Id, "B", fn3); + scripts.registerTimerCallback(test2Id, "A", fn4); scripts.processTimers(1, 2); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 10, "test1.lua", "B", sol::make_object(mLua.sol(), 3)); - scripts.setupSerializableTimer(TimeUnit::HOURS, 10, "test2.lua", "B", sol::make_object(mLua.sol(), 4)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 5, "test1.lua", "A", sol::make_object(mLua.sol(), 1)); - scripts.setupSerializableTimer(TimeUnit::HOURS, 5, "test2.lua", "A", sol::make_object(mLua.sol(), 2)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, "test1.lua", "A", sol::make_object(mLua.sol(), 10)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, "test1.lua", "B", sol::make_object(mLua.sol(), 20)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 10, test1Id, "B", sol::make_object(mLua.sol(), 3)); + scripts.setupSerializableTimer(TimeUnit::HOURS, 10, test2Id, "B", sol::make_object(mLua.sol(), 4)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 5, test1Id, "A", sol::make_object(mLua.sol(), 1)); + scripts.setupSerializableTimer(TimeUnit::HOURS, 5, test2Id, "A", sol::make_object(mLua.sol(), 2)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, test1Id, "A", sol::make_object(mLua.sol(), 10)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, test1Id, "B", sol::make_object(mLua.sol(), 20)); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 10, "test2.lua", fn2); - scripts.setupUnsavableTimer(TimeUnit::HOURS, 10, "test1.lua", fn2); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 5, "test2.lua", fn1); - scripts.setupUnsavableTimer(TimeUnit::HOURS, 5, "test1.lua", fn1); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 15, "test2.lua", fn1); + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 10, test2Id, fn2); + scripts.setupUnsavableTimer(TimeUnit::HOURS, 10, test1Id, fn2); + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 5, test2Id, fn1); + scripts.setupUnsavableTimer(TimeUnit::HOURS, 5, test1Id, fn1); + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 15, test2Id, fn1); EXPECT_EQ(counter1, 0); EXPECT_EQ(counter3, 0); @@ -358,10 +423,12 @@ return { EXPECT_EQ(counter3, 5); EXPECT_EQ(counter4, 5); + testing::internal::CaptureStdout(); ESM::LuaScripts data; scripts.save(data); - scripts.load(data, true); - scripts.registerTimerCallback("test1.lua", "B", fn4); + scripts.load(data); + scripts.registerTimerCallback(test1Id, "B", fn4); + EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\tload\nTest[test2.lua]:\tload\n"); testing::internal::CaptureStdout(); scripts.processTimers(20, 20); diff --git a/apps/openmw_test_suite/lua/testing_util.hpp b/apps/openmw_test_suite/lua/testing_util.hpp index 28c4d59930..2f6810350f 100644 --- a/apps/openmw_test_suite/lua/testing_util.hpp +++ b/apps/openmw_test_suite/lua/testing_util.hpp @@ -52,7 +52,7 @@ namespace } #define EXPECT_ERROR(X, ERR_SUBSTR) try { X; FAIL() << "Expected error"; } \ - catch (std::exception& e) { EXPECT_THAT(e.what(), HasSubstr(ERR_SUBSTR)); } + catch (std::exception& e) { EXPECT_THAT(e.what(), ::testing::HasSubstr(ERR_SUBSTR)); } } diff --git a/apps/openmw_test_suite/misc/test_stringops.cpp b/apps/openmw_test_suite/misc/test_stringops.cpp index 173cfa4447..7d8e93dc28 100644 --- a/apps/openmw_test_suite/misc/test_stringops.cpp +++ b/apps/openmw_test_suite/misc/test_stringops.cpp @@ -1,5 +1,6 @@ #include #include "components/misc/stringops.hpp" +#include "components/misc/algorithm.hpp" #include #include @@ -18,7 +19,7 @@ struct PartialBinarySearchTest : public ::testing::Test bool matches(const std::string& keyword) { - return Misc::StringUtils::partialBinarySearch(mDataVec.begin(), mDataVec.end(), keyword) != mDataVec.end(); + return Misc::partialBinarySearch(mDataVec.begin(), mDataVec.end(), keyword) != mDataVec.end(); } }; diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp new file mode 100644 index 0000000000..79db3bb414 --- /dev/null +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -0,0 +1,834 @@ +#include +#include + +#include "test_utils.hpp" + +namespace +{ + struct MWScriptTest : public ::testing::Test + { + MWScriptTest() : mErrorHandler(), mParser(mErrorHandler, mCompilerContext) {} + + std::optional compile(const std::string& scriptBody, bool shouldFail = false) + { + mParser.reset(); + mErrorHandler.reset(); + std::istringstream input(scriptBody); + Compiler::Scanner scanner(mErrorHandler, input, mCompilerContext.getExtensions()); + scanner.scan(mParser); + if(mErrorHandler.isGood()) + { + std::vector code; + mParser.getCode(code); + return CompiledScript(code, mParser.getLocals()); + } + else if(!shouldFail) + logErrors(); + return {}; + } + + void logErrors() + { + for(const auto& [error, loc] : mErrorHandler.getErrors()) + { + std::cout << error; + if(loc.mLine) + std::cout << " at line" << loc.mLine << " column " << loc.mColumn << " (" << loc.mLiteral << ")"; + std::cout << "\n"; + } + } + + void registerExtensions() + { + Compiler::registerExtensions(mExtensions); + mCompilerContext.setExtensions(&mExtensions); + } + + void run(const CompiledScript& script, TestInterpreterContext& context) + { + mInterpreter.run(&script.mByteCode[0], static_cast(script.mByteCode.size()), context); + } + + void installOpcode(int code, Interpreter::Opcode0* opcode) + { + mInterpreter.installSegment5(code, opcode); + } + protected: + void SetUp() override + { + Interpreter::installOpcodes(mInterpreter); + } + + void TearDown() override {} + private: + TestErrorHandler mErrorHandler; + TestCompilerContext mCompilerContext; + Compiler::FileParser mParser; + Compiler::Extensions mExtensions; + Interpreter::Interpreter mInterpreter; + }; + + const std::string sScript1 = R"mwscript(Begin basic_logic +; Comment +short one +short two + +set one to two + +if ( one == two ) + set one to 1 +elseif ( two == 1 ) + set one to 2 +else + set one to 3 +endif + +while ( one < two ) + set one to ( one + 1 ) +endwhile + +End)mwscript"; + + const std::string sScript2 = R"mwscript(Begin addtopic + +AddTopic "OpenMW Unit Test" + +End)mwscript"; + + const std::string sScript3 = R"mwscript(Begin math + +short a +short b +short c +short d +short e + +set b to ( a + 1 ) +set c to ( a - 1 ) +set d to ( b * c ) +set e to ( d / a ) + +End)mwscript"; + +// https://forum.openmw.org/viewtopic.php?f=6&t=2262 + const std::string sScript4 = R"mwscript(Begin scripting_once_again + +player -> addSpell "fire_bite", 645 + +PositionCell "Rabenfels, Taverne" 4480.000 3968.000 15820.000 0 + +End)mwscript"; + + const std::string sIssue587 = R"mwscript(Begin stalresetScript + +End stalreset Script)mwscript"; + + const std::string sIssue677 = R"mwscript(Begin _ase_dtree_dtree-owls + +End)mwscript"; + + const std::string sIssue685 = R"mwscript(Begin issue685 + +Choice: "Sicher. Hier, nehmt." 1 "Nein, ich denke nicht. Tut mir Leid." 2 +StartScript GetPCGold + +End)mwscript"; + + const std::string sIssue694 = R"mwscript(Begin issue694 + +float timer + +if ( timer < .1 ) +endif + +End)mwscript"; + + const std::string sIssue1062 = R"mwscript(Begin issue1026 + +short end + +End)mwscript"; + + const std::string sIssue1430 = R"mwscript(Begin issue1430 + +short var +If ( menumode == 1 ) + Player->AddItem "fur_boots", 1 + Player->Equip "iron battle axe", 1 + player->addspell "fire bite", 645 + player->additem "ring_keley", 1, +endif + +End)mwscript"; + + const std::string sIssue1593 = R"mwscript(Begin changeWater_-550_400 + +End)mwscript"; + + const std::string sIssue1730 = R"mwscript(Begin 4LOM_Corprusarium_Guards + +End)mwscript"; + + const std::string sIssue1767 = R"mwscript(Begin issue1767 + +player->GetPcRank "temple" + +End)mwscript"; + + const std::string sIssue2185 = R"mwscript(Begin issue2185 + +short a +short b +short eq +short gte +short lte +short ne + +set eq to 0 +if ( a == b ) + set eq to ( eq + 1 ) +endif +if ( a = = b ) + set eq to ( eq + 1 ) +endif + +set gte to 0 +if ( a >= b ) + set gte to ( gte + 1 ) +endif +if ( a > = b ) + set gte to ( gte + 1 ) +endif + +set lte to 0 +if ( a <= b ) + set lte to ( lte + 1 ) +endif +if ( a < = b ) + set lte to ( lte + 1 ) +endif + +set ne to 0 +if ( a != b ) + set ne to ( ne + 1 ) +endif +if ( a ! = b ) + set ne to ( ne + 1 ) +endif + +End)mwscript"; + + const std::string sIssue2206 = R"mwscript(Begin issue2206 + +Choice ."Sklavin kaufen." 1 "Lebt wohl." 2 +Choice Choice "Insister pour qu’il vous réponde." 6 "Le prier de vous accorder un peu de son temps." 6 " Le menacer de révéler qu'il prélève sa part sur les bénéfices de la mine d’ébonite." 7 + +End)mwscript"; + + const std::string sIssue2207 = R"mwscript(Begin issue2207 + +PositionCell -35 –473 -248 0 "Skaal-Dorf, Die Große Halle" + +End)mwscript"; + + const std::string sIssue2794 = R"mwscript(Begin issue2794 + +if ( player->"getlevel" == 1 ) + ; do something +endif + +End)mwscript"; + + const std::string sIssue2830 = R"mwscript(Begin issue2830 + +AddItem "if" 1 +AddItem "endif" 1 +GetItemCount "begin" + +End)mwscript"; + + const std::string sIssue2991 = R"mwscript(Begin issue2991 + +MessageBox "OnActivate" +messagebox "messagebox" +messagebox "if" +messagebox "tcl" + +End)mwscript"; + + const std::string sIssue3006 = R"mwscript(Begin issue3006 + +short a + +if ( a == 1 ) + set a to 2 +else set a to 3 +endif + +End)mwscript"; + + const std::string sIssue3725 = R"mwscript(Begin issue3725 + +onactivate + +if onactivate + ; do something +endif + +End)mwscript"; + + const std::string sIssue3744 = R"mwscript(Begin issue3744 + +short a +short b +short c + +set c to 0 + +if ( a => b ) + set c to ( c + 1 ) +endif +if ( a =< b ) + set c to ( c + 1 ) +endif +if ( a = b ) + set c to ( c + 1 ) +endif +if ( a == b ) + set c to ( c + 1 ) +endif + +End)mwscript"; + + const std::string sIssue3836 = R"mwscript(Begin issue3836 + +MessageBox " Membership Level: %.0f +Account Balance: %.0f +Your Gold: %.0f +Interest Rate: %.3f +Service Charge Rate: %.3f +Total Service Charges: %.0f +Total Interest Earned: %.0f " Membership BankAccount YourGold InterestRate ServiceRate TotalServiceCharges TotalInterestEarned + +End)mwscript"; + + const std::string sIssue3846 = R"mwscript(Begin issue3846 + +Addtopic -spells... +Addtopic -magicka... + +End)mwscript"; + + const std::string sIssue4061 = R"mwscript(Begin 01_Rz_neuvazhay-koryto2 + +End)mwscript"; + + const std::string sIssue4451 = R"mwscript(Begin, GlassDisplayScript + +;[Script body] + +End, GlassDisplayScript)mwscript"; + + const std::string sIssue4597 = R"mwscript(Begin issue4597 + +short a +short b +short c +short d + +set c to 0 +set d to 0 + +if ( a <> b ) + set c to ( c + 1 ) +endif +if ( a << b ) + set c to ( c + 1 ) +endif +if ( a < b ) + set c to ( c + 1 ) +endif + +if ( a >< b ) + set d to ( d + 1 ) +endif +if ( a >> b ) + set d to ( d + 1 ) +endif +if ( a > b ) + set d to ( d + 1 ) +endif + +End)mwscript"; + + const std::string sIssue4598 = R"mwscript(Begin issue4598 + +StartScript kal_S_Pub_Jejubãr_Faraminos + +End)mwscript"; + + const std::string sIssue4803 = R"mwscript( +-- ++-Begin issue4803 + +End)mwscript"; + + const std::string sIssue4867 = R"mwscript(Begin issue4867 + +float PcMagickaMult : The gameplay setting fPcBaseMagickaMult - 1.0000 + +End)mwscript"; + + const std::string sIssue4888 = R"mwscript(Begin issue4888 + +if (player->GameHour == 10) +set player->GameHour to 20 +endif + +End)mwscript"; + + const std::string sIssue5087 = R"mwscript(Begin Begin + +player->sethealth 0 +stopscript Begin + +End Begin)mwscript"; + + const std::string sIssue5097 = R"mwscript(Begin issue5097 + +setscale "0.3" + +End)mwscript"; + + const std::string sIssue5345 = R"mwscript(Begin issue5345 + +StartScript DN_MinionDrain_s" + +End)mwscript"; + + const std::string sIssue6066 = R"mwscript(Begin issue6066 +addtopic "return" + +End)mwscript"; + + const std::string sIssue6282 = R"mwscript(Begin 11AA_LauraScript7.5 + +End)mwscript"; + + const std::string sIssue6363 = R"mwscript(Begin issue6363 + +short 1 + +if ( "1" == 1 ) + PositionCell 0 1 2 3 4 5 "Morrowland" +endif + +set 1 to 42 + +End)mwscript"; + + TEST_F(MWScriptTest, mwscript_test_invalid) + { + EXPECT_THROW(compile("this is not a valid script", true), Compiler::SourceException); + } + + TEST_F(MWScriptTest, mwscript_test_compilation) + { + EXPECT_FALSE(!compile(sScript1)); + } + + TEST_F(MWScriptTest, mwscript_test_no_extensions) + { + EXPECT_THROW(compile(sScript2, true), Compiler::SourceException); + } + + TEST_F(MWScriptTest, mwscript_test_function) + { + registerExtensions(); + bool failed = true; + if(const auto script = compile(sScript2)) + { + class AddTopic : public Interpreter::Opcode0 + { + bool& mFailed; + public: + AddTopic(bool& failed) : mFailed(failed) {} + + void execute(Interpreter::Runtime& runtime) + { + const auto topic = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + mFailed = false; + EXPECT_EQ(topic, "OpenMW Unit Test"); + } + }; + installOpcode(Compiler::Dialogue::opcodeAddTopic, new AddTopic(failed)); + TestInterpreterContext context; + run(*script, context); + } + if(failed) + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_math) + { + if(const auto script = compile(sScript3)) + { + struct Algorithm + { + int a; + int b; + int c; + int d; + int e; + + void run(int input) + { + a = input; + b = a + 1; + c = a - 1; + d = b * c; + e = d / a; + } + + void test(const TestInterpreterContext& context) const + { + EXPECT_EQ(a, context.getLocalShort(0)); + EXPECT_EQ(b, context.getLocalShort(1)); + EXPECT_EQ(c, context.getLocalShort(2)); + EXPECT_EQ(d, context.getLocalShort(3)); + EXPECT_EQ(e, context.getLocalShort(4)); + } + } algorithm; + TestInterpreterContext context; + for(int i = 1; i < 1000; ++i) + { + context.setLocalShort(0, i); + run(*script, context); + algorithm.run(i); + algorithm.test(context); + } + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_forum_thread) + { + registerExtensions(); + EXPECT_FALSE(!compile(sScript4)); + } + + TEST_F(MWScriptTest, mwscript_test_587) + { + EXPECT_FALSE(!compile(sIssue587)); + } + + TEST_F(MWScriptTest, mwscript_test_677) + { + EXPECT_FALSE(!compile(sIssue677)); + } + + TEST_F(MWScriptTest, mwscript_test_685) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue685)); + } + + TEST_F(MWScriptTest, mwscript_test_694) + { + EXPECT_FALSE(!compile(sIssue694)); + } + + TEST_F(MWScriptTest, mwscript_test_1062) + { + if(const auto script = compile(sIssue1062)) + { + EXPECT_EQ(script->mLocals.getIndex("end"), 0); + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_1430) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue1430)); + } + + TEST_F(MWScriptTest, mwscript_test_1593) + { + EXPECT_FALSE(!compile(sIssue1593)); + } + + TEST_F(MWScriptTest, mwscript_test_1730) + { + EXPECT_FALSE(!compile(sIssue1730)); + } + + TEST_F(MWScriptTest, mwscript_test_1767) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue1767)); + } + + TEST_F(MWScriptTest, mwscript_test_2185) + { + if(const auto script = compile(sIssue2185)) + { + TestInterpreterContext context; + for(int a = 0; a < 100; ++a) + { + for(int b = 0; b < 100; ++b) + { + context.setLocalShort(0, a); + context.setLocalShort(1, b); + run(*script, context); + EXPECT_EQ(context.getLocalShort(2), a == b ? 2 : 0); + EXPECT_EQ(context.getLocalShort(3), a >= b ? 2 : 0); + EXPECT_EQ(context.getLocalShort(4), a <= b ? 2 : 0); + EXPECT_EQ(context.getLocalShort(5), a != b ? 2 : 0); + } + } + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_2206) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2206)); + } + + TEST_F(MWScriptTest, mwscript_test_2207) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2207)); + } + + TEST_F(MWScriptTest, mwscript_test_2794) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2794)); + } + + TEST_F(MWScriptTest, mwscript_test_2830) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2830)); + } + + TEST_F(MWScriptTest, mwscript_test_2991) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2991)); + } + + TEST_F(MWScriptTest, mwscript_test_3006) + { + if(const auto script = compile(sIssue3006)) + { + TestInterpreterContext context; + context.setLocalShort(0, 0); + run(*script, context); + EXPECT_EQ(context.getLocalShort(0), 0); + context.setLocalShort(0, 1); + run(*script, context); + EXPECT_EQ(context.getLocalShort(0), 2); + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_3725) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue3725)); + } + + TEST_F(MWScriptTest, mwscript_test_3744) + { + if(const auto script = compile(sIssue3744)) + { + TestInterpreterContext context; + for(int a = 0; a < 100; ++a) + { + for(int b = 0; b < 100; ++b) + { + context.setLocalShort(0, a); + context.setLocalShort(1, b); + run(*script, context); + EXPECT_EQ(context.getLocalShort(2), a == b ? 4 : 0); + } + } + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_3836) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue3836)); + } + + TEST_F(MWScriptTest, mwscript_test_3846) + { + registerExtensions(); + if(const auto script = compile(sIssue3846)) + { + std::vector topics = { "-spells...", "-magicka..." }; + class AddTopic : public Interpreter::Opcode0 + { + std::vector& mTopics; + public: + AddTopic(std::vector& topics) : mTopics(topics) {} + + void execute(Interpreter::Runtime& runtime) + { + const auto topic = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + EXPECT_EQ(topic, mTopics[0]); + mTopics.erase(mTopics.begin()); + } + }; + installOpcode(Compiler::Dialogue::opcodeAddTopic, new AddTopic(topics)); + TestInterpreterContext context; + run(*script, context); + EXPECT_TRUE(topics.empty()); + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_4061) + { + EXPECT_FALSE(!compile(sIssue4061)); + } + + TEST_F(MWScriptTest, mwscript_test_4451) + { + EXPECT_FALSE(!compile(sIssue4451)); + } + + TEST_F(MWScriptTest, mwscript_test_4597) + { + if(const auto script = compile(sIssue4597)) + { + TestInterpreterContext context; + for(int a = 0; a < 100; ++a) + { + for(int b = 0; b < 100; ++b) + { + context.setLocalShort(0, a); + context.setLocalShort(1, b); + run(*script, context); + EXPECT_EQ(context.getLocalShort(2), a < b ? 3 : 0); + EXPECT_EQ(context.getLocalShort(3), a > b ? 3 : 0); + } + } + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_4598) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue4598)); + } + + TEST_F(MWScriptTest, mwscript_test_4803) + { + EXPECT_FALSE(!compile(sIssue4803)); + } + + TEST_F(MWScriptTest, mwscript_test_4867) + { + EXPECT_FALSE(!compile(sIssue4867)); + } + + TEST_F(MWScriptTest, mwscript_test_4888) + { + EXPECT_FALSE(!compile(sIssue4888)); + } + + TEST_F(MWScriptTest, mwscript_test_5087) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue5087)); + } + + TEST_F(MWScriptTest, mwscript_test_5097) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue5097)); + } + + TEST_F(MWScriptTest, mwscript_test_5345) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue5345)); + } + + TEST_F(MWScriptTest, mwscript_test_6066) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue6066)); + } + + TEST_F(MWScriptTest, mwscript_test_6282) + { + EXPECT_FALSE(!compile(sIssue6282)); + } + + TEST_F(MWScriptTest, mwscript_test_6363) + { + registerExtensions(); + if(const auto script = compile(sIssue6363)) + { + class PositionCell : public Interpreter::Opcode0 + { + bool& mRan; + public: + PositionCell(bool& ran) : mRan(ran) {} + + void execute(Interpreter::Runtime& runtime) + { + mRan = true; + } + }; + bool ran = false; + installOpcode(Compiler::Transformation::opcodePositionCell, new PositionCell(ran)); + TestInterpreterContext context; + context.setLocalShort(0, 0); + run(*script, context); + EXPECT_FALSE(ran); + ran = false; + context.setLocalShort(0, 1); + run(*script, context); + EXPECT_TRUE(ran); + } + else + { + FAIL(); + } + } +} \ No newline at end of file diff --git a/apps/openmw_test_suite/mwscript/test_utils.hpp b/apps/openmw_test_suite/mwscript/test_utils.hpp new file mode 100644 index 0000000000..f29cb7bb89 --- /dev/null +++ b/apps/openmw_test_suite/mwscript/test_utils.hpp @@ -0,0 +1,243 @@ +#ifndef MWSCRIPT_TESTING_UTIL_H +#define MWSCRIPT_TESTING_UTIL_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace +{ + class TestCompilerContext : public Compiler::Context + { + public: + bool canDeclareLocals() const override { return true; } + char getGlobalType(const std::string& name) const override { return ' '; } + std::pair getMemberType(const std::string& name, const std::string& id) const override { return {' ', false}; } + bool isId(const std::string& name) const override { return Misc::StringUtils::ciEqual(name, "player"); } + }; + + class TestErrorHandler : public Compiler::ErrorHandler + { + std::vector> mErrors; + + void report(const std::string& message, const Compiler::TokenLoc& loc, Compiler::ErrorHandler::Type type) override + { + if(type == Compiler::ErrorHandler::ErrorMessage) + mErrors.emplace_back(message, loc); + } + + void report(const std::string& message, Compiler::ErrorHandler::Type type) override + { + report(message, {}, type); + } + + public: + void reset() override + { + Compiler::ErrorHandler::reset(); + mErrors.clear(); + } + + const std::vector>& getErrors() const { return mErrors; } + }; + + class LocalVariables + { + std::vector mShorts; + std::vector mLongs; + std::vector mFloats; + + template + T getLocal(std::size_t index, const std::vector& vector) const + { + if(index < vector.size()) + return vector[index]; + return {}; + } + + template + void setLocal(T value, std::size_t index, std::vector& vector) + { + if(index >= vector.size()) + vector.resize(index + 1); + vector[index] = value; + } + public: + void clear() + { + mShorts.clear(); + mLongs.clear(); + mFloats.clear(); + } + + int getShort(std::size_t index) const { return getLocal(index, mShorts); }; + + int getLong(std::size_t index) const { return getLocal(index, mLongs); }; + + float getFloat(std::size_t index) const { return getLocal(index, mFloats); }; + + void setShort(std::size_t index, int value) { setLocal(value, index, mShorts); }; + + void setLong(std::size_t index, int value) { setLocal(value, index, mLongs); }; + + void setFloat(std::size_t index, float value) { setLocal(value, index, mFloats); }; + }; + + class GlobalVariables + { + std::map mShorts; + std::map mLongs; + std::map mFloats; + + template + T getGlobal(const std::string& name, const std::map& map) const + { + auto it = map.find(name); + if(it != map.end()) + return it->second; + return {}; + } + public: + void clear() + { + mShorts.clear(); + mLongs.clear(); + mFloats.clear(); + } + + int getShort(const std::string& name) const { return getGlobal(name, mShorts); }; + + int getLong(const std::string& name) const { return getGlobal(name, mLongs); }; + + float getFloat(const std::string& name) const { return getGlobal(name, mFloats); }; + + void setShort(const std::string& name, int value) { mShorts[name] = value; }; + + void setLong(const std::string& name, int value) { mLongs[name] = value; }; + + void setFloat(const std::string& name, float value) { mFloats[name] = value; }; + }; + + class TestInterpreterContext : public Interpreter::Context + { + LocalVariables mLocals; + std::map mMembers; + public: + std::string getTarget() const override { return {}; }; + + int getLocalShort(int index) const override { return mLocals.getShort(index); }; + + int getLocalLong(int index) const override { return mLocals.getLong(index); }; + + float getLocalFloat(int index) const override { return mLocals.getFloat(index); }; + + void setLocalShort(int index, int value) override { mLocals.setShort(index, value); }; + + void setLocalLong(int index, int value) override { mLocals.setLong(index, value); }; + + void setLocalFloat(int index, float value) override { mLocals.setFloat(index, value); }; + + void messageBox(const std::string& message, const std::vector& buttons) override {}; + + void report(const std::string& message) override {}; + + int getGlobalShort(const std::string& name) const override { return {}; }; + + int getGlobalLong(const std::string& name) const override { return {}; }; + + float getGlobalFloat(const std::string& name) const override { return {}; }; + + void setGlobalShort(const std::string& name, int value) override {}; + + void setGlobalLong(const std::string& name, int value) override {}; + + void setGlobalFloat(const std::string& name, float value) override {}; + + std::vector getGlobals() const override { return {}; }; + + char getGlobalType(const std::string& name) const override { return ' '; }; + + std::string getActionBinding(const std::string& action) const override { return {}; }; + + std::string getActorName() const override { return {}; }; + + std::string getNPCRace() const override { return {}; }; + + std::string getNPCClass() const override { return {}; }; + + std::string getNPCFaction() const override { return {}; }; + + std::string getNPCRank() const override { return {}; }; + + std::string getPCName() const override { return {}; }; + + std::string getPCRace() const override { return {}; }; + + std::string getPCClass() const override { return {}; }; + + std::string getPCRank() const override { return {}; }; + + std::string getPCNextRank() const override { return {}; }; + + int getPCBounty() const override { return {}; }; + + std::string getCurrentCellName() const override { return {}; }; + + int getMemberShort(const std::string& id, const std::string& name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getShort(name); + return {}; + }; + + int getMemberLong(const std::string& id, const std::string& name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getLong(name); + return {}; + }; + + float getMemberFloat(const std::string& id, const std::string& name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getFloat(name); + return {}; + }; + + void setMemberShort(const std::string& id, const std::string& name, int value, bool global) override { mMembers[id].setShort(name, value); }; + + void setMemberLong(const std::string& id, const std::string& name, int value, bool global) override { mMembers[id].setLong(name, value); }; + + void setMemberFloat(const std::string& id, const std::string& name, float value, bool global) override { mMembers[id].setFloat(name, value); }; + }; + + struct CompiledScript + { + std::vector mByteCode; + Compiler::Locals mLocals; + + CompiledScript(const std::vector& code, const Compiler::Locals& locals) : mByteCode(code), mLocals(locals) {} + }; +} + +#endif \ No newline at end of file diff --git a/apps/openmw_test_suite/mwworld/test_store.cpp b/apps/openmw_test_suite/mwworld/test_store.cpp index f3b2bb3dcb..29240a1f7f 100644 --- a/apps/openmw_test_suite/mwworld/test_store.cpp +++ b/apps/openmw_test_suite/mwworld/test_store.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "apps/openmw/mwworld/esmstore.hpp" #include "apps/openmw/mwmechanics/spelllist.hpp" @@ -28,19 +29,14 @@ struct ContentFileTest : public ::testing::Test readContentFiles(); // load the content files - std::vector readerList; - readerList.resize(mContentFiles.size()); - int index=0; for (const auto & mContentFile : mContentFiles) { ESM::ESMReader lEsm; lEsm.setEncoder(nullptr); lEsm.setIndex(index); - lEsm.setGlobalReaderList(&readerList); lEsm.open(mContentFile.string()); - readerList[index] = lEsm; - mEsmStore.load(readerList[index], &dummyListener); + mEsmStore.load(lEsm, &dummyListener); ++index; } @@ -88,7 +84,10 @@ struct ContentFileTest : public ::testing::Test std::vector contentFiles = variables["content"].as().toStdStringVector(); for (auto & contentFile : contentFiles) - mContentFiles.push_back(collections.getPath(contentFile)); + { + if (!Misc::StringUtils::ciEndsWith(contentFile, ".omwscripts")) + mContentFiles.push_back(collections.getPath(contentFile)); + } } protected: @@ -250,9 +249,6 @@ TEST_F(StoreTest, delete_test) record.mId = recordId; ESM::ESMReader reader; - std::vector readerList; - readerList.push_back(reader); - reader.setGlobalReaderList(&readerList); // master file inserts a record Files::IStreamPtr file = getEsmFile(record, false); @@ -293,9 +289,6 @@ TEST_F(StoreTest, overwrite_test) record.mId = recordId; ESM::ESMReader reader; - std::vector readerList; - readerList.push_back(reader); - reader.setGlobalReaderList(&readerList); // master file inserts a record Files::IStreamPtr file = getEsmFile(record, false); diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp index a1f5d6091d..b37a8cd6c0 100644 --- a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp +++ b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp @@ -60,6 +60,19 @@ namespace { return isNear(lhs.getOrigin(), rhs.getOrigin()) && isNear(lhs.getBasis(), rhs.getBasis()); } + + struct WriteVec3f + { + osg::Vec3f mValue; + + friend std::ostream& operator <<(std::ostream& stream, const WriteVec3f& value) + { + return stream << "osg::Vec3f {" + << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue.x() << ", " + << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue.y() << ", " + << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue.z() << "}"; + } + }; } static std::ostream& operator <<(std::ostream& stream, const btVector3& value) @@ -122,6 +135,17 @@ static std::ostream& operator <<(std::ostream& stream, const TriangleMeshShape& return stream << "}}"; } +static bool operator ==(const BulletShape::CollisionBox& l, const BulletShape::CollisionBox& r) +{ + const auto tie = [] (const BulletShape::CollisionBox& v) { return std::tie(v.mExtents, v.mCenter); }; + return tie(l) == tie(r); +} + +static std::ostream& operator <<(std::ostream& stream, const BulletShape::CollisionBox& value) +{ + return stream << "CollisionBox {" << WriteVec3f {value.mExtents} << ", " << WriteVec3f {value.mCenter} << "}"; +} + } static std::ostream& operator <<(std::ostream& stream, const btCollisionShape& value) @@ -160,20 +184,18 @@ namespace Resource { static bool operator ==(const Resource::BulletShape& lhs, const Resource::BulletShape& rhs) { - return compareObjects(lhs.mCollisionShape, rhs.mCollisionShape) - && compareObjects(lhs.mAvoidCollisionShape, rhs.mAvoidCollisionShape) - && lhs.mCollisionBox.extents == rhs.mCollisionBox.extents - && lhs.mCollisionBox.center == rhs.mCollisionBox.center + return compareObjects(lhs.mCollisionShape.get(), rhs.mCollisionShape.get()) + && compareObjects(lhs.mAvoidCollisionShape.get(), rhs.mAvoidCollisionShape.get()) + && lhs.mCollisionBox == rhs.mCollisionBox && lhs.mAnimatedShapes == rhs.mAnimatedShapes; } static std::ostream& operator <<(std::ostream& stream, const Resource::BulletShape& value) { return stream << "Resource::BulletShape {" - << value.mCollisionShape << ", " - << value.mAvoidCollisionShape << ", " - << "osg::Vec3f {" << value.mCollisionBox.extents << "}" << ", " - << "osg::Vec3f {" << value.mCollisionBox.center << "}" << ", " + << value.mCollisionShape.get() << ", " + << value.mAvoidCollisionShape.get() << ", " + << value.mCollisionBox << ", " << value.mAnimatedShapes << "}"; } @@ -272,6 +294,12 @@ namespace value.recType = Nif::RC_NiTriShape; } + void init(Nif::NiTriStrips& value) + { + init(static_cast(value)); + value.recType = Nif::RC_NiTriStrips; + } + void init(Nif::NiSkinInstance& value) { value.data = Nif::NiSkinDataPtr(nullptr); @@ -330,6 +358,8 @@ namespace Nif::NiTriShape mNiTriShape; Nif::NiTriShapeData mNiTriShapeData2; Nif::NiTriShape mNiTriShape2; + Nif::NiTriStripsData mNiTriStripsData; + Nif::NiTriStrips mNiTriStrips; Nif::NiSkinInstance mNiSkinInstance; Nif::NiStringExtraData mNiStringExtraData; Nif::NiStringExtraData mNiStringExtraData2; @@ -361,6 +391,7 @@ namespace init(mNiNode3); init(mNiTriShape); init(mNiTriShape2); + init(mNiTriStrips); init(mNiSkinInstance); init(mNiStringExtraData); init(mNiStringExtraData2); @@ -375,6 +406,11 @@ namespace 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::NiGeometryDataPtr(&mNiTriShapeData2); + + mNiTriStripsData.recType = Nif::RC_NiTriStripsData; + mNiTriStripsData.vertices = {osg::Vec3f(0, 0, 0), osg::Vec3f(1, 0, 0), osg::Vec3f(1, 1, 0), osg::Vec3f(0, 1, 0)}; + mNiTriStripsData.strips = {{0, 1, 2, 3}}; + mNiTriStrips.data = Nif::NiGeometryDataPtr(&mNiTriStripsData); } }; @@ -389,6 +425,18 @@ namespace EXPECT_EQ(*result, expected); } + TEST_F(TestBulletNifLoader, should_ignore_nullptr_root) + { + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(nullptr)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + TEST_F(TestBulletNifLoader, for_default_root_nif_node_should_return_default) { EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -441,12 +489,12 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = 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()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -458,6 +506,7 @@ namespace mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNode.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNode)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -466,12 +515,12 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = 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()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -483,6 +532,7 @@ namespace mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNode.parent = &mNiNode; mNiNode.hasBounds = true; mNiNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; @@ -496,12 +546,12 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = 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()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -513,11 +563,13 @@ namespace mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNode.parent = &mNiNode; mNode2.hasBounds = true; mNode2.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode2.bounds.box.extents = osg::Vec3f(4, 5, 6); mNode2.bounds.box.center = osg::Vec3f(-4, -5, -6); + mNode2.parent = &mNiNode; mNiNode.hasBounds = true; mNiNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; @@ -531,12 +583,12 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = 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()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -547,12 +599,14 @@ namespace mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNode.parent = &mNiNode; mNode2.hasBounds = true; mNode2.flags |= Nif::NiNode::Flag_BBoxCollision; mNode2.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode2.bounds.box.extents = osg::Vec3f(4, 5, 6); mNode2.bounds.box.center = osg::Vec3f(-4, -5, -6); + mNode2.parent = &mNiNode; mNiNode.hasBounds = true; mNiNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; @@ -566,12 +620,12 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(4, 5, 6); - expected.mCollisionBox.center = osg::Vec3f(-4, -5, -6); + expected.mCollisionBox.mExtents = osg::Vec3f(4, 5, 6); + expected.mCollisionBox.mCenter = 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()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -589,8 +643,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); EXPECT_EQ(*result, expected); } @@ -605,7 +659,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -623,14 +677,15 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); EXPECT_EQ(*result, expected); } TEST_F(TestBulletNifLoader, for_tri_shape_child_node_should_return_shape_with_triangle_mesh_shape) { + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -641,7 +696,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -649,7 +704,9 @@ namespace TEST_F(TestBulletNifLoader, for_nested_tri_shape_child_should_return_shape_with_triangle_mesh_shape) { mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiNode2)})); + mNiNode2.parent = &mNiNode; mNiNode2.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); + mNiTriShape.parent = &mNiNode2; EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); @@ -659,13 +716,15 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } TEST_F(TestBulletNifLoader, for_two_tri_shape_children_should_return_shape_with_triangle_mesh_shape_with_all_meshes) { + mNiTriShape.parent = &mNiNode; + mNiTriShape2.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape), Nif::NodePtr(&mNiTriShape2) @@ -680,7 +739,7 @@ namespace triangles->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -688,6 +747,7 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_and_filename_starting_with_x_and_not_empty_skin_should_return_shape_with_triangle_mesh_shape) { mNiTriShape.skin = Nif::NiSkinInstancePtr(&mNiSkinInstance); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -698,7 +758,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -720,7 +780,7 @@ namespace std::unique_ptr shape(new btCompoundShape); shape->addChildShape(mResultTransform, mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -746,7 +806,7 @@ namespace std::unique_ptr shape(new btCompoundShape); shape->addChildShape(mResultTransform2, mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -756,9 +816,11 @@ namespace { copy(mTransform, mNiTriShape.trafo); mNiTriShape.trafo.scale = 3; + mNiTriShape.parent = &mNiNode; copy(mTransform, mNiTriShape2.trafo); mNiTriShape2.trafo.scale = 3; + mNiTriShape2.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape), @@ -784,7 +846,7 @@ namespace shape->addChildShape(mResultTransform, mesh.release()); shape->addChildShape(mResultTransform, mesh2.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -813,7 +875,7 @@ namespace std::unique_ptr shape(new btCompoundShape); shape->addChildShape(mResultTransform2, mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -825,6 +887,7 @@ namespace mController.flags |= Nif::NiNode::ControllerFlag_Active; copy(mTransform, mNiTriShape.trafo); mNiTriShape.trafo.scale = 3; + mNiTriShape.parent = &mNiNode; copy(mTransform, mNiTriShape2.trafo); mNiTriShape2.trafo.scale = 3; mNiTriShape2.parent = &mNiNode; @@ -841,7 +904,7 @@ namespace const auto result = mLoader.load(mNifFile); std::unique_ptr triangles(new btTriangleMesh(false)); - triangles->addTriangle(btVector3(1, 2, 3), btVector3(4, 2, 3), btVector3(4, 4.632747650146484375, 1.56172335147857666015625)); + triangles->addTriangle(btVector3(4, 8, 12), btVector3(16, 8, 12), btVector3(16, 18.5309906005859375, 6.246893405914306640625)); std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); mesh->setLocalScaling(btVector3(1, 1, 1)); @@ -854,7 +917,35 @@ namespace shape->addChildShape(mResultTransform2, mesh2.release()); shape->addChildShape(btTransform::getIdentity(), mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); + expected.mAnimatedShapes = {{-1, 0}}; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_add_static_mesh_to_existing_compound_mesh) + { + mNiTriShape.parent = &mNiNode; + mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(2)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getRoot(1)).WillOnce(Return(&mNiTriShape2)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("xtest.nif")); + const auto result = mLoader.load(mNifFile); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + + std::unique_ptr triangles2(new btTriangleMesh(false)); + triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); + + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles2.release(), true)); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -862,6 +953,7 @@ namespace TEST_F(TestBulletNifLoader, for_root_avoid_node_and_tri_shape_child_node_should_return_shape_with_null_collision_shape) { + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); mNiNode.recType = Nif::RC_AvoidNode; @@ -873,7 +965,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mAvoidCollisionShape = new Resource::TriangleMeshShape(triangles.release(), false); + expected.mAvoidCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), false)); EXPECT_EQ(*result, expected); } @@ -881,6 +973,7 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_empty_data_should_return_shape_with_null_collision_shape) { mNiTriShape.data = Nif::NiGeometryDataPtr(nullptr); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -897,6 +990,7 @@ namespace { auto data = static_cast(mNiTriShape.data.getPtr()); data->triangles.clear(); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -914,6 +1008,7 @@ namespace mNiStringExtraData.string = "NC___"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -932,6 +1027,7 @@ namespace mNiStringExtraData2.string = "NC___"; mNiStringExtraData2.recType = Nif::RC_NiStringExtraData; mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -949,6 +1045,7 @@ namespace mNiStringExtraData.string = "MRK"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -966,8 +1063,10 @@ namespace mNiStringExtraData.string = "MRK"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); + mNiTriShape.parent = &mNiNode2; mNiNode2.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); mNiNode2.recType = Nif::RC_RootCollisionNode; + mNiNode2.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiNode2)})); mNiNode.recType = Nif::RC_NiNode; @@ -979,7 +1078,137 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_ignore_tri_shape_data_with_mismatching_data_rec_type) + { + mNiTriShape.data = Nif::NiGeometryDataPtr(&mNiTriStripsData); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriShape)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_strips_root_node_should_return_shape_with_triangle_mesh_shape) + { + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + triangles->addTriangle(btVector3(1, 0, 0), btVector3(0, 1, 0), btVector3(1, 1, 0)); + Resource::BulletShape expected; + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_ignore_tri_strips_data_with_mismatching_data_rec_type) + { + mNiTriStrips.data = Nif::NiGeometryDataPtr(&mNiTriShapeData); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_ignore_tri_strips_data_with_empty_strips) + { + mNiTriStripsData.strips.clear(); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_static_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) + { + mNiTriStripsData.strips.front() = {0, 1}; + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_avoid_collision_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) + { + mNiTriShape.parent = &mNiNode; + mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); + mNiNode.recType = Nif::RC_AvoidNode; + mNiTriStripsData.strips.front() = {0, 1}; + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_animated_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) + { + mNiTriStripsData.strips.front() = {0, 1}; + mNiTriStrips.parent = &mNiNode; + mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriStrips)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("xtest.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_not_add_static_mesh_with_no_triangles_to_compound_shape) + { + mNiTriStripsData.strips.front() = {0, 1}; + mNiTriShape.parent = &mNiNode; + mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(2)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getRoot(1)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("xtest.nif")); + const auto result = mLoader.load(mNifFile); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); } diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 7b06b6702d..a3f77d86bf 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -29,7 +29,7 @@ endif (GIT_CHECKOUT) # source files add_component_dir (lua - luastate scriptscontainer utilpackage serialization omwscriptsparser + luastate scriptscontainer utilpackage serialization configuration ) add_component_dir (settings @@ -55,7 +55,7 @@ add_component_dir (shader add_component_dir (sceneutil clone attach visitor util statesetupdater controller skeleton riggeometry morphgeometry lightcontroller - lightmanager lightutil positionattitudetransform workqueue unrefqueue pathgridutil waterutil writescene serialize optimizer + lightmanager lightutil positionattitudetransform workqueue pathgridutil waterutil writescene serialize optimizer actorutil detourdebugdraw navmesh agentpath shadow mwshadowtechnique recastmesh shadowsbin osgacontroller rtt screencapture ) diff --git a/components/compiler/context.hpp b/components/compiler/context.hpp index 399e8125bb..d3caba7c53 100644 --- a/components/compiler/context.hpp +++ b/components/compiler/context.hpp @@ -42,9 +42,6 @@ namespace Compiler virtual bool isId (const std::string& name) const = 0; ///< Does \a name match an ID, that can be referenced? - - virtual bool isJournalId (const std::string& name) const = 0; - ///< Does \a name match a journal ID? }; } diff --git a/components/compiler/declarationparser.cpp b/components/compiler/declarationparser.cpp index 1c64aaadee..f29fe820c1 100644 --- a/components/compiler/declarationparser.cpp +++ b/components/compiler/declarationparser.cpp @@ -90,6 +90,16 @@ bool Compiler::DeclarationParser::parseSpecial (int code, const TokenLoc& loc, S return Parser::parseSpecial (code, loc, scanner); } +bool Compiler::DeclarationParser::parseInt(int value, const TokenLoc& loc, Scanner& scanner) +{ + if(mState == State_Name) + { + // Allow integers to be used as variable names + return parseName(loc.mLiteral, loc, scanner); + } + return Parser::parseInt(value, loc, scanner); +} + void Compiler::DeclarationParser::reset() { mState = State_Begin; diff --git a/components/compiler/declarationparser.hpp b/components/compiler/declarationparser.hpp index c04f1dc268..d08509fe50 100644 --- a/components/compiler/declarationparser.hpp +++ b/components/compiler/declarationparser.hpp @@ -35,6 +35,10 @@ namespace Compiler ///< Handle a special character token. /// \return fetch another token? + bool parseInt (int value, const TokenLoc& loc, Scanner& scanner) override; + ///< Handle an int token. + /// \return fetch another token? + void reset() override; }; diff --git a/components/compiler/exprparser.cpp b/components/compiler/exprparser.cpp index 2d525e6f8c..1aedc8dc59 100644 --- a/components/compiler/exprparser.cpp +++ b/components/compiler/exprparser.cpp @@ -632,7 +632,7 @@ namespace Compiler } int ExprParser::parseArguments (const std::string& arguments, Scanner& scanner, - std::vector& code, int ignoreKeyword) + std::vector& code, int ignoreKeyword, bool expectNames) { bool optional = false; int optionalCount = 0; @@ -717,6 +717,8 @@ namespace Compiler if (optional) parser.setOptional (true); + if(expectNames) + scanner.enableExpectName(); scanner.scan (parser); diff --git a/components/compiler/exprparser.hpp b/components/compiler/exprparser.hpp index 2f3eaa8a9f..42739658ec 100644 --- a/components/compiler/exprparser.hpp +++ b/components/compiler/exprparser.hpp @@ -96,7 +96,7 @@ namespace Compiler /// \return Type ('l': integer, 'f': float) int parseArguments (const std::string& arguments, Scanner& scanner, - std::vector& code, int ignoreKeyword = -1); + std::vector& code, int ignoreKeyword = -1, bool expectNames = false); ///< Parse sequence of arguments specified by \a arguments. /// \param arguments Uses ScriptArgs typedef /// \see Compiler::ScriptArgs diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index 3dfcadab10..64133bee84 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -553,7 +553,7 @@ namespace Compiler extensions.registerFunction("getpos",'f',"c",opcodeGetPos,opcodeGetPosExplicit); extensions.registerFunction("getstartingpos",'f',"c",opcodeGetStartingPos,opcodeGetStartingPosExplicit); extensions.registerInstruction("position","ffffz",opcodePosition,opcodePositionExplicit); - extensions.registerInstruction("positioncell","ffffcX",opcodePositionCell,opcodePositionCellExplicit); + extensions.registerInstruction("positioncell","ffffczz",opcodePositionCell,opcodePositionCellExplicit); extensions.registerInstruction("placeitemcell","ccffffX",opcodePlaceItemCell); extensions.registerInstruction("placeitem","cffffX",opcodePlaceItem); extensions.registerInstruction("placeatpc","clflX",opcodePlaceAtPc); diff --git a/components/compiler/lineparser.cpp b/components/compiler/lineparser.cpp index 77afaee8bd..ec90812ec7 100644 --- a/components/compiler/lineparser.cpp +++ b/components/compiler/lineparser.cpp @@ -67,6 +67,11 @@ namespace Compiler parseExpression (scanner, loc); return true; } + else if (mState == SetState) + { + // Allow ints to be used as variable names + return parseName(loc.mLiteral, loc, scanner); + } return Parser::parseInt (value, loc, scanner); } @@ -140,7 +145,7 @@ namespace Compiler if (!arguments.empty()) { mExprParser.reset(); - mExprParser.parseArguments (arguments, scanner, mCode); + mExprParser.parseArguments (arguments, scanner, mCode, -1, true); } mName = name; diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp index 1054e2e269..0e2b76cb23 100644 --- a/components/compiler/scanner.cpp +++ b/components/compiler/scanner.cpp @@ -130,7 +130,8 @@ namespace Compiler { bool cont = false; - if (scanInt (c, parser, cont)) + bool scanned = mExpectName ? scanName(c, parser, cont) : scanInt(c, parser, cont); + if (scanned) { mLoc.mLiteral.clear(); return cont; @@ -387,6 +388,8 @@ namespace Compiler bool Scanner::scanSpecial (MultiChar& c, Parser& parser, bool& cont) { + bool expectName = mExpectName; + mExpectName = false; int special = -1; if (c=='\n') @@ -541,15 +544,16 @@ namespace Compiler if (special==S_newline) mLoc.mLiteral = ""; - else if (mExpectName && (special == S_member || special == S_minus)) + else if (expectName && (special == S_member || special == S_minus)) { - mExpectName = false; bool tolerant = mTolerantNames; mTolerantNames = true; bool out = scanName(c, parser, cont); mTolerantNames = tolerant; return out; } + else if (expectName && special == S_comma) + mExpectName = true; TokenLoc loc (mLoc); mLoc.mLiteral.clear(); diff --git a/components/compiler/stringparser.cpp b/components/compiler/stringparser.cpp index d9c3c04947..4e0114e0a1 100644 --- a/components/compiler/stringparser.cpp +++ b/components/compiler/stringparser.cpp @@ -86,6 +86,12 @@ namespace Compiler return Parser::parseSpecial (code, loc, scanner); } + bool StringParser::parseInt (int value, const TokenLoc& loc, Scanner& scanner) + { + reportWarning("Treating integer argument as a string", loc); + return parseName(loc.mLiteral, loc, scanner); + } + void StringParser::append (std::vector& code) { std::copy (mCode.begin(), mCode.end(), std::back_inserter (code)); diff --git a/components/compiler/stringparser.hpp b/components/compiler/stringparser.hpp index 1976628360..07b61d8fda 100644 --- a/components/compiler/stringparser.hpp +++ b/components/compiler/stringparser.hpp @@ -43,6 +43,10 @@ namespace Compiler ///< Handle a special character token. /// \return fetch another token? + bool parseInt (int value, const TokenLoc& loc, Scanner& scanner) override; + ///< Handle an int token. + /// \return fetch another token? + void append (std::vector& code); ///< Append code for parsed string. diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index 690f968142..208b1315f3 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -9,9 +9,10 @@ #include -ContentSelectorModel::ContentModel::ContentModel(QObject *parent, QIcon warningIcon) : +ContentSelectorModel::ContentModel::ContentModel(QObject *parent, QIcon warningIcon, bool showOMWScripts) : QAbstractTableModel(parent), mWarningIcon(warningIcon), + mShowOMWScripts(showOMWScripts), mMimeType ("application/omwcontent"), mMimeTypes (QStringList() << mMimeType), mColumnCount (1), @@ -416,6 +417,8 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) QDir dir(path); QStringList filters; filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; + if (mShowOMWScripts) + filters << "*.omwscripts"; dir.setNameFilters(filters); for (const QString &path2 : dir.entryList()) @@ -425,6 +428,15 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) if (item(info.fileName())) continue; + if (info.fileName().endsWith(".omwscripts", Qt::CaseInsensitive)) + { + EsmFile *file = new EsmFile(path2); + file->setDate(info.lastModified()); + file->setFilePath(info.absoluteFilePath()); + addFile(file); + continue; + } + try { ESM::ESMReader fileReader; ToUTF8::Utf8Encoder encoder = diff --git a/components/contentselector/model/contentmodel.hpp b/components/contentselector/model/contentmodel.hpp index d245a0dcbf..f8130e3649 100644 --- a/components/contentselector/model/contentmodel.hpp +++ b/components/contentselector/model/contentmodel.hpp @@ -23,7 +23,7 @@ namespace ContentSelectorModel { Q_OBJECT public: - explicit ContentModel(QObject *parent, QIcon warningIcon); + explicit ContentModel(QObject *parent, QIcon warningIcon, bool showOMWScripts); ~ContentModel(); void setEncoding(const QString &encoding); @@ -84,6 +84,7 @@ namespace ContentSelectorModel QSet mPluginsWithLoadOrderError; QString mEncoding; QIcon mWarningIcon; + bool mShowOMWScripts; public: diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index d7996dfae3..f18e80dd0a 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -10,21 +10,21 @@ #include #include -ContentSelectorView::ContentSelector::ContentSelector(QWidget *parent) : +ContentSelectorView::ContentSelector::ContentSelector(QWidget *parent, bool showOMWScripts) : QObject(parent) { ui.setupUi(parent); ui.addonView->setDragDropMode(QAbstractItemView::InternalMove); - buildContentModel(); + buildContentModel(showOMWScripts); buildGameFileView(); buildAddonView(); } -void ContentSelectorView::ContentSelector::buildContentModel() +void ContentSelectorView::ContentSelector::buildContentModel(bool showOMWScripts) { QIcon warningIcon(ui.addonView->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(QSize(16, 15))); - mContentModel = new ContentSelectorModel::ContentModel(this, warningIcon); + mContentModel = new ContentSelectorModel::ContentModel(this, warningIcon, showOMWScripts); } void ContentSelectorView::ContentSelector::buildGameFileView() diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp index cda68fa1b7..4a9983c1bf 100644 --- a/components/contentselector/view/contentselector.hpp +++ b/components/contentselector/view/contentselector.hpp @@ -23,7 +23,7 @@ namespace ContentSelectorView public: - explicit ContentSelector(QWidget *parent = nullptr); + explicit ContentSelector(QWidget *parent = nullptr, bool showOMWScripts = false); QString currentFile() const; @@ -56,7 +56,7 @@ namespace ContentSelectorView Ui::ContentSelector ui; - void buildContentModel(); + void buildContentModel(bool showOMWScripts); void buildGameFileView(); void buildAddonView(); void buildContextMenu(); diff --git a/components/detournavigator/gettilespositions.hpp b/components/detournavigator/gettilespositions.hpp index 27c8f7a4ac..e8ba8beba9 100644 --- a/components/detournavigator/gettilespositions.hpp +++ b/components/detournavigator/gettilespositions.hpp @@ -46,13 +46,15 @@ namespace DetourNavigator btVector3 aabbMax; shape.getAabb(transform, aabbMin, aabbMax); - getTilesPositions(Misc::Convert::makeOsgVec3f(aabbMin), Misc::Convert::makeOsgVec3f(aabbMax), settings, std::forward(callback)); + getTilesPositions(Misc::Convert::toOsg(aabbMin), Misc::Convert::toOsg(aabbMax), settings, std::forward(callback)); } template void getTilesPositions(const int cellSize, const osg::Vec3f& shift, const Settings& settings, Callback&& callback) { + using Misc::Convert::toOsg; + const auto halfCellSize = cellSize / 2; const btTransform transform(btMatrix3x3::getIdentity(), Misc::Convert::toBullet(shift)); auto aabbMin = transform(btVector3(-halfCellSize, -halfCellSize, 0)); @@ -64,7 +66,7 @@ namespace DetourNavigator aabbMax.setX(std::max(aabbMin.x(), aabbMax.x())); aabbMax.setY(std::max(aabbMin.y(), aabbMax.y())); - getTilesPositions(Misc::Convert::makeOsgVec3f(aabbMin), Misc::Convert::makeOsgVec3f(aabbMax), settings, std::forward(callback)); + getTilesPositions(toOsg(aabbMin), toOsg(aabbMax), settings, std::forward(callback)); } } diff --git a/components/detournavigator/navigatorimpl.cpp b/components/detournavigator/navigatorimpl.cpp index f29ae1bb95..44b42b22c2 100644 --- a/components/detournavigator/navigatorimpl.cpp +++ b/components/detournavigator/navigatorimpl.cpp @@ -34,9 +34,9 @@ namespace DetourNavigator bool NavigatorImpl::addObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) { - CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->getCollisionShape()}; + CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->mCollisionShape}; bool result = mNavMeshManager.addObject(id, collisionShape, transform, AreaType_ground); - if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->getAvoidCollisionShape()) + if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->mAvoidCollisionShape.get()) { const ObjectId avoidId(avoidShape); CollisionShape avoidCollisionShape {shapes.mShapeInstance, *avoidShape}; @@ -64,13 +64,13 @@ namespace DetourNavigator bool NavigatorImpl::updateObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) { - const CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->getCollisionShape()}; + const CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->mCollisionShape}; bool result = mNavMeshManager.updateObject(id, collisionShape, transform, AreaType_ground); - if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->getAvoidCollisionShape()) + if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->mAvoidCollisionShape.get()) { const ObjectId avoidId(avoidShape); - const CollisionShape collisionShape {shapes.mShapeInstance, *avoidShape}; - if (mNavMeshManager.updateObject(avoidId, collisionShape, transform, AreaType_null)) + const CollisionShape avoidCollisionShape {shapes.mShapeInstance, *avoidShape}; + if (mNavMeshManager.updateObject(avoidId, avoidCollisionShape, transform, AreaType_null)) { updateAvoidShapeId(id, avoidId); result = true; diff --git a/components/detournavigator/preparednavmeshdatatuple.hpp b/components/detournavigator/preparednavmeshdatatuple.hpp index 8ff1267370..bcca0ace37 100644 --- a/components/detournavigator/preparednavmeshdatatuple.hpp +++ b/components/detournavigator/preparednavmeshdatatuple.hpp @@ -14,11 +14,11 @@ namespace DetourNavigator constexpr auto makeTuple(const rcPolyMesh& v) noexcept { return std::tuple( - Span(v.verts, getVertsLength(v)), - Span(v.polys, getPolysLength(v)), - Span(v.regs, getRegsLength(v)), - Span(v.flags, getFlagsLength(v)), - Span(v.areas, getAreasLength(v)), + Span(v.verts, static_cast(getVertsLength(v))), + Span(v.polys, static_cast(getPolysLength(v))), + Span(v.regs, static_cast(getRegsLength(v))), + Span(v.flags, static_cast(getFlagsLength(v))), + Span(v.areas, static_cast(getAreasLength(v))), ArrayRef(v.bmin), ArrayRef(v.bmax), v.cs, @@ -31,9 +31,9 @@ namespace DetourNavigator constexpr auto makeTuple(const rcPolyMeshDetail& v) noexcept { return std::tuple( - Span(v.meshes, getMeshesLength(v)), - Span(v.verts, getVertsLength(v)), - Span(v.tris, getTrisLength(v)) + Span(v.meshes, static_cast(getMeshesLength(v))), + Span(v.verts, static_cast(getVertsLength(v))), + Span(v.tris, static_cast(getTrisLength(v))) ); } diff --git a/components/detournavigator/recastmeshbuilder.cpp b/components/detournavigator/recastmeshbuilder.cpp index 73b731c247..8f860d2eb1 100644 --- a/components/detournavigator/recastmeshbuilder.cpp +++ b/components/detournavigator/recastmeshbuilder.cpp @@ -30,7 +30,7 @@ namespace DetourNavigator RecastMeshTriangle result; result.mAreaType = areaType; for (std::size_t i = 0; i < 3; ++i) - result.mVertices[i] = Misc::Convert::makeOsgVec3f(vertices[i]); + result.mVertices[i] = Misc::Convert::toOsg(vertices[i]); return result; } diff --git a/components/detournavigator/recastmeshobject.cpp b/components/detournavigator/recastmeshobject.cpp index 8b4bc2fd6f..31aa13a208 100644 --- a/components/detournavigator/recastmeshobject.cpp +++ b/components/detournavigator/recastmeshobject.cpp @@ -11,7 +11,7 @@ namespace DetourNavigator namespace { bool updateCompoundObject(const btCompoundShape& shape, const AreaType areaType, - std::vector& children) + std::vector& children) { assert(static_cast(shape.getNumChildShapes()) == children.size()); bool result = false; @@ -23,39 +23,33 @@ namespace DetourNavigator return result; } - std::vector makeChildrenObjects(const osg::ref_ptr& holder, - const btCompoundShape& shape, const AreaType areaType) + std::vector makeChildrenObjects(const btCompoundShape& shape, const AreaType areaType) { - std::vector result; + std::vector result; for (int i = 0, num = shape.getNumChildShapes(); i < num; ++i) - { - const CollisionShape collisionShape {holder, *shape.getChildShape(i)}; - result.emplace_back(collisionShape, shape.getChildTransform(i), areaType); - } + result.emplace_back(*shape.getChildShape(i), shape.getChildTransform(i), areaType); return result; } - std::vector makeChildrenObjects(const osg::ref_ptr& holder, - const btCollisionShape& shape, const AreaType areaType) + std::vector makeChildrenObjects(const btCollisionShape& shape, const AreaType areaType) { if (shape.isCompound()) - return makeChildrenObjects(holder, static_cast(shape), areaType); - return std::vector(); + return makeChildrenObjects(static_cast(shape), areaType); + return {}; } } - RecastMeshObject::RecastMeshObject(const CollisionShape& shape, const btTransform& transform, + ChildRecastMeshObject::ChildRecastMeshObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType) - : mHolder(shape.getHolder()) - , mShape(shape.getShape()) + : mShape(shape) , mTransform(transform) , mAreaType(areaType) - , mLocalScaling(mShape.get().getLocalScaling()) - , mChildren(makeChildrenObjects(mHolder, mShape.get(), mAreaType)) + , mLocalScaling(shape.getLocalScaling()) + , mChildren(makeChildrenObjects(shape, mAreaType)) { } - bool RecastMeshObject::update(const btTransform& transform, const AreaType areaType) + bool ChildRecastMeshObject::update(const btTransform& transform, const AreaType areaType) { bool result = false; if (!(mTransform == transform)) @@ -78,4 +72,11 @@ namespace DetourNavigator || result; return result; } + + RecastMeshObject::RecastMeshObject(const CollisionShape& shape, const btTransform& transform, + const AreaType areaType) + : mHolder(shape.getHolder()) + , mImpl(shape.getShape(), transform, areaType) + { + } } diff --git a/components/detournavigator/recastmeshobject.hpp b/components/detournavigator/recastmeshobject.hpp index 0c50c2f346..e833ee37e3 100644 --- a/components/detournavigator/recastmeshobject.hpp +++ b/components/detournavigator/recastmeshobject.hpp @@ -32,40 +32,45 @@ namespace DetourNavigator std::reference_wrapper mShape; }; + class ChildRecastMeshObject + { + public: + ChildRecastMeshObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType); + + bool update(const btTransform& transform, const AreaType areaType); + + const btCollisionShape& getShape() const { return mShape; } + + const btTransform& getTransform() const { return mTransform; } + + AreaType getAreaType() const { return mAreaType; } + + private: + std::reference_wrapper mShape; + btTransform mTransform; + AreaType mAreaType; + btVector3 mLocalScaling; + std::vector mChildren; + }; + class RecastMeshObject { public: RecastMeshObject(const CollisionShape& shape, const btTransform& transform, const AreaType areaType); - bool update(const btTransform& transform, const AreaType areaType); + bool update(const btTransform& transform, const AreaType areaType) { return mImpl.update(transform, areaType); } - const osg::ref_ptr& getHolder() const - { - return mHolder; - } + const osg::ref_ptr& getHolder() const { return mHolder; } - const btCollisionShape& getShape() const - { - return mShape; - } + const btCollisionShape& getShape() const { return mImpl.getShape(); } - const btTransform& getTransform() const - { - return mTransform; - } + const btTransform& getTransform() const { return mImpl.getTransform(); } - AreaType getAreaType() const - { - return mAreaType; - } + AreaType getAreaType() const { return mImpl.getAreaType(); } private: osg::ref_ptr mHolder; - std::reference_wrapper mShape; - btTransform mTransform; - AreaType mAreaType; - btVector3 mLocalScaling; - std::vector mChildren; + ChildRecastMeshObject mImpl; }; } diff --git a/components/detournavigator/serialization/binaryreader.hpp b/components/detournavigator/serialization/binaryreader.hpp new file mode 100644 index 0000000000..0d75c3ac99 --- /dev/null +++ b/components/detournavigator/serialization/binaryreader.hpp @@ -0,0 +1,62 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYREADER_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYREADER_H + +#include +#include +#include +#include +#include + +namespace DetourNavigator::Serialization +{ + class BinaryReader + { + public: + explicit BinaryReader(const std::byte* pos, const std::byte* end) + : mPos(pos), mEnd(end) + { + assert(mPos <= mEnd); + } + + BinaryReader(const BinaryReader&) = delete; + + template + void operator()(Format&& format, T& value) + { + if constexpr (std::is_arithmetic_v) + { + if (mEnd - mPos < static_cast(sizeof(value))) + throw std::runtime_error("Not enough data"); + std::memcpy(&value, mPos, sizeof(value)); + mPos += sizeof(value); + } + else + { + format(*this, value); + } + } + + template + auto operator()(Format&& format, T* data, std::size_t count) + { + if constexpr (std::is_arithmetic_v) + { + if (mEnd - mPos < static_cast(count * sizeof(T))) + throw std::runtime_error("Not enough data"); + const std::size_t size = sizeof(T) * count; + std::memcpy(data, mPos, size); + mPos += size; + } + else + { + format(*this, data, count); + } + } + + private: + const std::byte* mPos; + const std::byte* const mEnd; + }; +} + +#endif diff --git a/components/detournavigator/serialization/binarywriter.hpp b/components/detournavigator/serialization/binarywriter.hpp new file mode 100644 index 0000000000..5e710d85d5 --- /dev/null +++ b/components/detournavigator/serialization/binarywriter.hpp @@ -0,0 +1,62 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYWRITER_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYWRITER_H + +#include +#include +#include +#include +#include + +namespace DetourNavigator::Serialization +{ + struct BinaryWriter + { + public: + explicit BinaryWriter(std::byte* dest, const std::byte* end) + : mDest(dest), mEnd(end) + { + assert(mDest <= mEnd); + } + + BinaryWriter(const BinaryWriter&) = delete; + + template + void operator()(Format&& format, const T& value) + { + if constexpr (std::is_arithmetic_v) + { + if (mEnd - mDest < static_cast(sizeof(value))) + throw std::runtime_error("Not enough space"); + std::memcpy(mDest, &value, sizeof(value)); + mDest += sizeof(value); + } + else + { + format(*this, value); + } + } + + template + auto operator()(Format&& format, const T* data, std::size_t count) + { + if constexpr (std::is_arithmetic_v) + { + const std::size_t size = sizeof(T) * count; + if (mEnd - mDest < static_cast(size)) + throw std::runtime_error("Not enough space"); + std::memcpy(mDest, data, size); + mDest += size; + } + else + { + format(*this, data, count); + } + } + + private: + std::byte* mDest; + const std::byte* const mEnd; + }; +} + +#endif diff --git a/components/detournavigator/serialization/format.hpp b/components/detournavigator/serialization/format.hpp new file mode 100644 index 0000000000..d07ab9da6f --- /dev/null +++ b/components/detournavigator/serialization/format.hpp @@ -0,0 +1,80 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H + +#include +#include +#include +#include +#include +#include + +namespace DetourNavigator::Serialization +{ + enum class Mode + { + Read, + Write, + }; + + template + struct IsContiguousContainer : std::false_type {}; + + template + struct IsContiguousContainer> : std::true_type {}; + + template + constexpr bool isContiguousContainer = IsContiguousContainer>::value; + + template + struct Format + { + template + void operator()(Visitor&& visitor, T* data, std::size_t size) const + { + if constexpr (std::is_arithmetic_v) + { + visitor(self(), data, size); + } + else if constexpr (std::is_enum_v) + { + if constexpr (mode == Mode::Write) + visitor(self(), reinterpret_cast*>(data), size); + else + { + static_assert(mode == Mode::Read); + visitor(self(), reinterpret_cast*>(data), size); + } + } + else + { + std::for_each(data, data + size, [&] (auto& v) { visitor(self(), v); }); + } + } + + template + void operator()(Visitor&& visitor, T(& data)[size]) const + { + self()(std::forward(visitor), data, size); + } + + template + auto operator()(Visitor&& visitor, T&& value) const + -> std::enable_if_t> + { + if constexpr (mode == Mode::Write) + visitor(self(), value.size()); + else + { + static_assert(mode == Mode::Read); + std::size_t size = 0; + visitor(self(), size); + value.resize(size); + } + self()(std::forward(visitor), value.data(), value.size()); + } + + const Derived& self() const { return static_cast(*this); } + }; +} + +#endif diff --git a/components/detournavigator/serialization/sizeaccumulator.hpp b/components/detournavigator/serialization/sizeaccumulator.hpp new file mode 100644 index 0000000000..28bdb5c1cb --- /dev/null +++ b/components/detournavigator/serialization/sizeaccumulator.hpp @@ -0,0 +1,41 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_SIZEACCUMULATOR_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_SIZEACCUMULATOR_H + +#include +#include + +namespace DetourNavigator::Serialization +{ + class SizeAccumulator + { + public: + SizeAccumulator() = default; + + SizeAccumulator(const SizeAccumulator&) = delete; + + std::size_t value() const { return mValue; } + + template + void operator()(Format&& format, const T& value) + { + if constexpr (std::is_arithmetic_v) + mValue += sizeof(T); + else + format(*this, value); + } + + template + auto operator()(Format&& format, const T* data, std::size_t count) + { + if constexpr (std::is_arithmetic_v) + mValue += count * sizeof(T); + else + format(*this, data, count); + } + + private: + std::size_t mValue = 0; + }; +} + +#endif diff --git a/components/esm/activespells.hpp b/components/esm/activespells.hpp index 8b5f1f1946..a79366f9c2 100644 --- a/components/esm/activespells.hpp +++ b/components/esm/activespells.hpp @@ -22,7 +22,9 @@ namespace ESM Flag_None = 0, Flag_Applied = 1 << 0, Flag_Remove = 1 << 1, - Flag_Ignore_Resistances = 1 << 2 + Flag_Ignore_Resistances = 1 << 2, + Flag_Ignore_Reflect = 1 << 3, + Flag_Ignore_SpellAbsorption = 1 << 4 }; int mEffectId; diff --git a/components/esm/defs.hpp b/components/esm/defs.hpp index 7f2fe19cc5..254e66ec3a 100644 --- a/components/esm/defs.hpp +++ b/components/esm/defs.hpp @@ -165,6 +165,7 @@ enum RecNameInts // format 1 REC_FILT = FourCC<'F','I','L','T'>::value, REC_DBGP = FourCC<'D','B','G','P'>::value, ///< only used in project files + REC_LUAL = FourCC<'L','U','A','L'>::value, // LuaScriptsCfg // format 16 - Lua scripts in saved games REC_LUAM = FourCC<'L','U','A','M'>::value, // LuaManager data diff --git a/components/esm/esmreader.cpp b/components/esm/esmreader.cpp index dbf713315b..316748b53a 100644 --- a/components/esm/esmreader.cpp +++ b/components/esm/esmreader.cpp @@ -1,5 +1,8 @@ #include "esmreader.hpp" +#include +#include + #include namespace ESM @@ -17,7 +20,6 @@ ESM_Context ESMReader::getContext() ESMReader::ESMReader() : mRecordFlags(0) , mBuffer(50*1024) - , mGlobalReaderList(nullptr) , mEncoder(nullptr) , mFileSize(0) { @@ -55,6 +57,29 @@ void ESMReader::clearCtx() mCtx.subName.clear(); } +void ESMReader::resolveParentFileIndices(const std::vector& allPlugins) +{ + mCtx.parentFileIndices.clear(); + const std::vector &masters = getGameFiles(); + for (size_t j = 0; j < masters.size(); j++) { + const Header::MasterData &mast = masters[j]; + std::string fname = mast.name; + int index = getIndex(); + for (int i = 0; i < getIndex(); i++) { + const ESMReader& reader = allPlugins.at(i); + if (reader.getFileSize() == 0) + continue; // Content file in non-ESM format + const std::string candidate = reader.getName(); + std::string fnamecandidate = boost::filesystem::path(candidate).filename().string(); + if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) { + index = i; + break; + } + } + mCtx.parentFileIndices.push_back(index); + } +} + void ESMReader::openRaw(Files::IStreamPtr _esm, const std::string& name) { close(); diff --git a/components/esm/esmreader.hpp b/components/esm/esmreader.hpp index a438dca0cd..92f2a6673b 100644 --- a/components/esm/esmreader.hpp +++ b/components/esm/esmreader.hpp @@ -80,13 +80,15 @@ public: // to the individual load() methods. This hack allows to pass this reference // indirectly to the load() method. void setIndex(const int index) { mCtx.index = index;} - int getIndex() {return mCtx.index;} + int getIndex() const {return mCtx.index;} - void setGlobalReaderList(std::vector *list) {mGlobalReaderList = list;} - std::vector *getGlobalReaderList() {return mGlobalReaderList;} - - void addParentFileIndex(int index) { mCtx.parentFileIndices.push_back(index); } + // Assign parent esX files by tracking their indices in the global list of + // all files/readers used by the engine. This is required for correct adjustRefNum() results + // as required for handling moved, deleted and edited CellRefs. + /// @note Does not validate. + void resolveParentFileIndices(const std::vector& files); const std::vector& getParentFileIndices() const { return mCtx.parentFileIndices; } + bool isValidParentFileIndex(int i) const { return i != getIndex(); } /************************************************************************* * @@ -279,7 +281,6 @@ private: Header mHeader; - std::vector *mGlobalReaderList; ToUTF8::Utf8Encoder* mEncoder; size_t mFileSize; diff --git a/components/esm/loadland.cpp b/components/esm/loadland.cpp index d7dcd47c69..e97ad6b759 100644 --- a/components/esm/loadland.cpp +++ b/components/esm/loadland.cpp @@ -15,7 +15,6 @@ namespace ESM : mFlags(0) , mX(0) , mY(0) - , mPlugin(0) , mDataTypes(0) , mLandData(nullptr) { @@ -40,8 +39,6 @@ namespace ESM { isDeleted = false; - mPlugin = esm.getIndex(); - bool hasLocation = false; bool isLoaded = false; while (!isLoaded && esm.hasMoreSubs()) @@ -172,7 +169,7 @@ namespace ESM { float height = mLandData->mHeights[int(row * vertMult) * ESM::Land::LAND_SIZE + int(col * vertMult)]; height /= height > 0 ? 128.f : 16.f; - height = std::min(max, std::max(min, height)); + height = std::clamp(height, min, max); wnam[row * LAND_GLOBAL_MAP_LOD_SIZE_SQRT + col] = static_cast(height); } } @@ -192,7 +189,7 @@ namespace ESM void Land::blank() { - mPlugin = 0; + setPlugin(0); std::fill(std::begin(mWnam), std::end(mWnam), 0); @@ -326,7 +323,7 @@ namespace ESM } Land::Land (const Land& land) - : mFlags (land.mFlags), mX (land.mX), mY (land.mY), mPlugin (land.mPlugin), + : mFlags (land.mFlags), mX (land.mX), mY (land.mY), mContext (land.mContext), mDataTypes (land.mDataTypes), mLandData (land.mLandData ? new LandData (*land.mLandData) : nullptr) { @@ -345,7 +342,6 @@ namespace ESM std::swap (mFlags, land.mFlags); std::swap (mX, land.mX); std::swap (mY, land.mY); - std::swap (mPlugin, land.mPlugin); std::swap (mContext, land.mContext); std::swap (mDataTypes, land.mDataTypes); std::swap (mLandData, land.mLandData); diff --git a/components/esm/loadland.hpp b/components/esm/loadland.hpp index 67dd2e76a2..610dd28fb8 100644 --- a/components/esm/loadland.hpp +++ b/components/esm/loadland.hpp @@ -29,7 +29,10 @@ struct Land int mFlags; // Only first four bits seem to be used, don't know what // they mean. int mX, mY; // Map coordinates. - int mPlugin; // Plugin index, used to reference the correct material palette. + + // Plugin index, used to reference the correct material palette. + int getPlugin() const { return mContext.index; } + void setPlugin(int index) { mContext.index = index; } // File context. This allows the ESM reader to be 'reset' to this // location later when we are ready to load the full data set. diff --git a/components/esm/luascripts.cpp b/components/esm/luascripts.cpp index 1dd45ab2b1..c831cbbbfc 100644 --- a/components/esm/luascripts.cpp +++ b/components/esm/luascripts.cpp @@ -5,13 +5,15 @@ // List of all records, that are related to Lua. // -// Record: -// LUAM - MWLua::LuaManager +// Records: +// LUAL - LuaScriptsCfg - list of all scripts (in content files) +// LUAM - MWLua::LuaManager (in saves) // // Subrecords: +// LUAF - LuaScriptCfg::mFlags // LUAW - Start of MWLua::WorldView data // LUAE - Start of MWLua::LocalEvent or MWLua::GlobalEvent (eventName) -// LUAS - Start LuaUtil::ScriptsContainer data (scriptName) +// LUAS - VFS path to a Lua script // LUAD - Serialized Lua variable // LUAT - MWLua::ScriptsContainer::Timer // LUAC - Name of a timer callback (string) @@ -32,11 +34,33 @@ std::string ESM::loadLuaBinaryData(ESMReader& esm) { esm.getSubHeader(); data.resize(esm.getSubSize()); - esm.getExact(data.data(), data.size()); + esm.getExact(data.data(), static_cast(data.size())); } return data; } +void ESM::LuaScriptsCfg::load(ESMReader& esm) +{ + while (esm.isNextSub("LUAS")) + { + std::string name = esm.getHString(); + uint64_t flags; + esm.getHNT(flags, "LUAF"); + std::string data = loadLuaBinaryData(esm); + mScripts.push_back({std::move(name), std::move(data), flags}); + } +} + +void ESM::LuaScriptsCfg::save(ESMWriter& esm) const +{ + for (const LuaScriptCfg& script : mScripts) + { + esm.writeHNString("LUAS", script.mScriptPath); + esm.writeHNT("LUAF", script.mFlags); + saveLuaBinaryData(esm, script.mInitializationData); + } +} + void ESM::LuaScripts::load(ESMReader& esm) { while (esm.isNextSub("LUAS")) @@ -63,8 +87,7 @@ void ESM::LuaScripts::save(ESMWriter& esm) const for (const LuaScript& script : mScripts) { esm.writeHNString("LUAS", script.mScriptPath); - if (!script.mData.empty()) - saveLuaBinaryData(esm, script.mData); + saveLuaBinaryData(esm, script.mData); for (const LuaTimer& timer : script.mTimers) { esm.startSubRecord("LUAT"); diff --git a/components/esm/luascripts.hpp b/components/esm/luascripts.hpp index f268f41536..e6f7113c16 100644 --- a/components/esm/luascripts.hpp +++ b/components/esm/luascripts.hpp @@ -9,7 +9,44 @@ namespace ESM class ESMReader; class ESMWriter; - // Storage structure for LuaUtil::ScriptsContainer. This is not a top-level record. + // LuaScriptCfg, LuaScriptsCfg are used in content files. + + struct LuaScriptCfg + { + using Flags = uint64_t; + static constexpr Flags sGlobal = 1ull << 0; + static constexpr Flags sCustom = 1ull << 1; // local; can be attached/detached by a global script + static constexpr Flags sPlayer = 1ull << 2; // auto attach to players + // auto attach for other classes: + static constexpr Flags sActivator = 1ull << 3; + static constexpr Flags sArmor = 1ull << 4; + static constexpr Flags sBook = 1ull << 5; + static constexpr Flags sClothing = 1ull << 6; + static constexpr Flags sContainer = 1ull << 7; + static constexpr Flags sCreature = 1ull << 8; + static constexpr Flags sDoor = 1ull << 9; + static constexpr Flags sIngredient = 1ull << 10; + static constexpr Flags sLight = 1ull << 11; + static constexpr Flags sMiscItem = 1ull << 12; + static constexpr Flags sNPC = 1ull << 13; + static constexpr Flags sPotion = 1ull << 14; + static constexpr Flags sWeapon = 1ull << 15; + + std::string mScriptPath; + std::string mInitializationData; // Serialized Lua table. It is a binary data. Can contain '\0'. + Flags mFlags; // bitwise OR of Flags. + }; + + struct LuaScriptsCfg + { + std::vector mScripts; + + void load(ESMReader &esm); + void save(ESMWriter &esm) const; + }; + + // LuaTimer, LuaScript, LuaScripts are used in saved game files. + // Storage structure for LuaUtil::ScriptsContainer. These are not top-level records. // Used either for global scripts or for local scripts on a specific object. struct LuaTimer @@ -37,11 +74,11 @@ namespace ESM { std::vector mScripts; - void load (ESMReader &esm); - void save (ESMWriter &esm) const; + void load(ESMReader &esm); + void save(ESMWriter &esm) const; }; - // Saves binary string `data` (can contain '\0') as record LUAD. + // Saves binary string `data` (can contain '\0') as LUAD record. void saveLuaBinaryData(ESM::ESMWriter& esm, const std::string& data); // Loads LUAD as binary string. If next subrecord is not LUAD, then returns an empty string. diff --git a/components/esmloader/load.cpp b/components/esmloader/load.cpp index 789e7619b6..9879f33274 100644 --- a/components/esmloader/load.cpp +++ b/components/esmloader/load.cpp @@ -214,7 +214,6 @@ namespace EsmLoader ESM::ESMReader& reader = readers[i]; reader.setEncoder(encoder); reader.setIndex(static_cast(i)); - reader.setGlobalReaderList(&readers); reader.open(collection.getPath(file).string()); loadEsm(query, readers[i], result); diff --git a/components/esmterrain/storage.hpp b/components/esmterrain/storage.hpp index 68e71574ee..107255a7af 100644 --- a/components/esmterrain/storage.hpp +++ b/components/esmterrain/storage.hpp @@ -36,11 +36,7 @@ namespace ESMTerrain return nullptr; return &mData; } - - inline int getPlugin() const - { - return mLand->mPlugin; - } + inline int getPlugin() const { return mLand->getPlugin(); } private: const ESM::Land* mLand; diff --git a/components/files/escape.cpp b/components/files/escape.cpp index 8b11504d34..fcbcc04a16 100644 --- a/components/files/escape.cpp +++ b/components/files/escape.cpp @@ -29,10 +29,10 @@ namespace Files std::string temp = str; static const char hash[] = { escape_hash_filter::sEscape, escape_hash_filter::sHashIdentifier }; - Misc::StringUtils::replaceAll(temp, hash, "#", 2, 1); + Misc::StringUtils::replaceAll(temp, std::string_view(hash, 2), "#"); static const char escape[] = { escape_hash_filter::sEscape, escape_hash_filter::sEscapeIdentifier }; - Misc::StringUtils::replaceAll(temp, escape, "@", 2, 1); + Misc::StringUtils::replaceAll(temp, std::string_view(escape, 2), "@"); return temp; } diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp index da43cc38ec..76f554bec7 100644 --- a/components/fontloader/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -145,7 +145,7 @@ namespace Gui FontLoader::FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, const std::string& userDataPath, float scalingFactor) : mVFS(vfs) , mUserDataPath(userDataPath) - , mFontHeight(16) + , mFontHeight(std::clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20)) , mScalingFactor(scalingFactor) { if (encoding == ToUTF8::WINDOWS_1252) @@ -153,9 +153,6 @@ namespace Gui else mEncoding = encoding; - int fontSize = Settings::Manager::getInt("font size", "GUI"); - mFontHeight = std::min(std::max(12, fontSize), 20); - MyGUI::ResourceManager::getInstance().unregisterLoadXmlDelegate("Resource"); MyGUI::ResourceManager::getInstance().registerLoadXmlDelegate("Resource") = MyGUI::newDelegate(this, &FontLoader::loadFontFromXml); } @@ -549,7 +546,7 @@ namespace Gui // to allow to configure font size via config file, without need to edit XML files. // Also we should take UI scaling factor in account. int resolution = Settings::Manager::getInt("ttf resolution", "GUI"); - resolution = std::min(960, std::max(48, resolution)) * mScalingFactor; + resolution = std::clamp(resolution, 48, 960) * mScalingFactor; MyGUI::xml::ElementPtr resolutionNode = resourceNode->createChild("Property"); resolutionNode->addAttribute("key", "Resolution"); @@ -591,7 +588,7 @@ namespace Gui // setup separate fonts with different Resolution to fit these windows. // These fonts have an internal prefix. int resolution = Settings::Manager::getInt("ttf resolution", "GUI"); - resolution = std::min(960, std::max(48, resolution)); + resolution = std::clamp(resolution, 48, 960); float currentX = Settings::Manager::getInt("resolution x", "Video"); float currentY = Settings::Manager::getInt("resolution y", "Video"); diff --git a/components/lua/configuration.cpp b/components/lua/configuration.cpp new file mode 100644 index 0000000000..4598ed2508 --- /dev/null +++ b/components/lua/configuration.cpp @@ -0,0 +1,165 @@ +#include "configuration.hpp" + +#include +#include +#include +#include + +#include + +namespace LuaUtil +{ + + namespace + { + const std::map> flagsByName{ + {"GLOBAL", ESM::LuaScriptCfg::sGlobal}, + {"CUSTOM", ESM::LuaScriptCfg::sCustom}, + {"PLAYER", ESM::LuaScriptCfg::sPlayer}, + {"ACTIVATOR", ESM::LuaScriptCfg::sActivator}, + {"ARMOR", ESM::LuaScriptCfg::sArmor}, + {"BOOK", ESM::LuaScriptCfg::sBook}, + {"CLOTHING", ESM::LuaScriptCfg::sClothing}, + {"CONTAINER", ESM::LuaScriptCfg::sContainer}, + {"CREATURE", ESM::LuaScriptCfg::sCreature}, + {"DOOR", ESM::LuaScriptCfg::sDoor}, + {"INGREDIENT", ESM::LuaScriptCfg::sIngredient}, + {"LIGHT", ESM::LuaScriptCfg::sLight}, + {"MISC_ITEM", ESM::LuaScriptCfg::sMiscItem}, + {"NPC", ESM::LuaScriptCfg::sNPC}, + {"POTION", ESM::LuaScriptCfg::sPotion}, + {"WEAPON", ESM::LuaScriptCfg::sWeapon}, + }; + } + + const std::vector ScriptsConfiguration::sEmpty; + + void ScriptsConfiguration::init(ESM::LuaScriptsCfg cfg) + { + mScripts.clear(); + mScriptsByFlag.clear(); + mPathToIndex.clear(); + + // Find duplicates; only the last occurrence will be used. + // Search for duplicates is case insensitive. + std::vector skip(cfg.mScripts.size(), false); + for (int i = cfg.mScripts.size() - 1; i >= 0; --i) + { + auto [_, inserted] = mPathToIndex.insert_or_assign( + Misc::StringUtils::lowerCase(cfg.mScripts[i].mScriptPath), -1); + if (!inserted || cfg.mScripts[i].mFlags == 0) + skip[i] = true; + } + mPathToIndex.clear(); + int index = 0; + for (size_t i = 0; i < cfg.mScripts.size(); ++i) + { + if (skip[i]) + continue; + ESM::LuaScriptCfg& s = cfg.mScripts[i]; + mPathToIndex[s.mScriptPath] = index; // Stored paths are case sensitive. + ESM::LuaScriptCfg::Flags flags = s.mFlags; + ESM::LuaScriptCfg::Flags flag = 1; + while (flags != 0) + { + if (flags & flag) + mScriptsByFlag[flag].push_back(index); + flags &= ~flag; + flag = flag << 1; + } + mScripts.push_back(std::move(s)); + index++; + } + } + + std::optional ScriptsConfiguration::findId(std::string_view path) const + { + auto it = mPathToIndex.find(path); + if (it != mPathToIndex.end()) + return it->second; + else + return std::nullopt; + } + + const std::vector& ScriptsConfiguration::getListByFlag(ESM::LuaScriptCfg::Flags type) const + { + assert(std::bitset<64>(type).count() <= 1); + auto it = mScriptsByFlag.find(type); + if (it != mScriptsByFlag.end()) + return it->second; + else + return sEmpty; + } + + void parseOMWScripts(ESM::LuaScriptsCfg& cfg, std::string_view data) + { + while (!data.empty()) + { + // Get next line + std::string_view line = data.substr(0, data.find('\n')); + data = data.substr(std::min(line.size() + 1, data.size())); + if (!line.empty() && line.back() == '\r') + line = line.substr(0, line.size() - 1); + + while (!line.empty() && std::isspace(line[0])) + line = line.substr(1); + if (line.empty() || line[0] == '#') // Skip empty lines and comments + continue; + while (!line.empty() && std::isspace(line.back())) + line = line.substr(0, line.size() - 1); + + if (!Misc::StringUtils::ciEndsWith(line, ".lua")) + throw std::runtime_error(Misc::StringUtils::format( + "Lua script should have suffix '.lua', got: %s", std::string(line.substr(0, 300)))); + + // Split flags and script path + size_t semicolonPos = line.find(':'); + if (semicolonPos == std::string::npos) + throw std::runtime_error(Misc::StringUtils::format("No flags found in: %s", std::string(line))); + std::string_view flagsStr = line.substr(0, semicolonPos); + std::string_view scriptPath = line.substr(semicolonPos + 1); + while (std::isspace(scriptPath[0])) + scriptPath = scriptPath.substr(1); + + // Parse flags + ESM::LuaScriptCfg::Flags flags = 0; + size_t flagsPos = 0; + while (true) + { + while (flagsPos < flagsStr.size() && (std::isspace(flagsStr[flagsPos]) || flagsStr[flagsPos] == ',')) + flagsPos++; + size_t startPos = flagsPos; + while (flagsPos < flagsStr.size() && !std::isspace(flagsStr[flagsPos]) && flagsStr[flagsPos] != ',') + flagsPos++; + if (startPos == flagsPos) + break; + std::string_view flagName = flagsStr.substr(startPos, flagsPos - startPos); + auto it = flagsByName.find(flagName); + if (it != flagsByName.end()) + flags |= it->second; + else + throw std::runtime_error(Misc::StringUtils::format("Unknown flag '%s' in: %s", + std::string(flagName), std::string(line))); + } + if ((flags & ESM::LuaScriptCfg::sGlobal) && flags != ESM::LuaScriptCfg::sGlobal) + throw std::runtime_error("Global script can not have local flags"); + + cfg.mScripts.push_back(ESM::LuaScriptCfg{std::string(scriptPath), "", flags}); + } + } + + std::string scriptCfgToString(const ESM::LuaScriptCfg& script) + { + std::stringstream ss; + for (const auto& [flagName, flag] : flagsByName) + { + if (script.mFlags & flag) + ss << flagName << " "; + } + ss << ": " << script.mScriptPath; + if (!script.mInitializationData.empty()) + ss << " (with data, " << script.mInitializationData.size() << " bytes)"; + return ss.str(); + } + +} diff --git a/components/lua/configuration.hpp b/components/lua/configuration.hpp new file mode 100644 index 0000000000..32eddf399c --- /dev/null +++ b/components/lua/configuration.hpp @@ -0,0 +1,37 @@ +#ifndef COMPONENTS_LUA_CONFIGURATION_H +#define COMPONENTS_LUA_CONFIGURATION_H + +#include +#include + +#include + +namespace LuaUtil +{ + + class ScriptsConfiguration + { + public: + void init(ESM::LuaScriptsCfg); + + size_t size() const { return mScripts.size(); } + const ESM::LuaScriptCfg& operator[](int id) const { return mScripts[id]; } + + std::optional findId(std::string_view path) const; + const std::vector& getListByFlag(ESM::LuaScriptCfg::Flags type) const; + + private: + std::vector mScripts; + std::map> mPathToIndex; + std::map> mScriptsByFlag; + static const std::vector sEmpty; + }; + + // Parse ESM::LuaScriptsCfg from text and add to `cfg`. + void parseOMWScripts(ESM::LuaScriptsCfg& cfg, std::string_view data); + + std::string scriptCfgToString(const ESM::LuaScriptCfg& script); + +} + +#endif // COMPONENTS_LUA_CONFIGURATION_H diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp index 8e4719dba4..e78f7bed06 100644 --- a/components/lua/luastate.cpp +++ b/components/lua/luastate.cpp @@ -22,7 +22,7 @@ namespace LuaUtil "type", "unpack", "xpcall", "rawequal", "rawget", "rawset", "getmetatable", "setmetatable"}; static const std::string safePackages[] = {"coroutine", "math", "string", "table"}; - LuaState::LuaState(const VFS::Manager* vfs) : mVFS(vfs) + LuaState::LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf) : mConf(conf), mVFS(vfs) { mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, sol::lib::string, sol::lib::table, sol::lib::debug); @@ -95,12 +95,11 @@ namespace LuaUtil return res; } - void LuaState::addCommonPackage(const std::string& packageName, const sol::object& package) + void LuaState::addCommonPackage(std::string packageName, sol::object package) { - if (package.is()) - mCommonPackages[packageName] = package; - else - mCommonPackages[packageName] = makeReadOnly(package); + if (!package.is()) + package = makeReadOnly(std::move(package)); + mCommonPackages.emplace(std::move(packageName), std::move(package)); } sol::protected_function_result LuaState::runInNewSandbox( @@ -148,7 +147,7 @@ namespace LuaUtil return std::move(res); } - sol::protected_function LuaState::loadScript(const std::string& path) + sol::function LuaState::loadScript(const std::string& path) { auto iter = mCompiledScripts.find(path); if (iter != mCompiledScripts.end()) diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp index 8982b49b36..7ac5af0b1b 100644 --- a/components/lua/luastate.hpp +++ b/components/lua/luastate.hpp @@ -7,6 +7,8 @@ #include +#include "configuration.hpp" + namespace LuaUtil { @@ -22,12 +24,12 @@ namespace LuaUtil // - Access to common read-only resources from different sandboxes; // - Replace standard `require` with a safe version that allows to search // Lua libraries (only source, no dll's) in the virtual filesystem; - // - Make `print` to add the script name to the every message and - // write to Log rather than directly to stdout; + // - Make `print` to add the script name to every message and + // write to the Log rather than directly to stdout; class LuaState { public: - explicit LuaState(const VFS::Manager* vfs); + explicit LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf); ~LuaState(); // Returns underlying sol::state. @@ -40,7 +42,7 @@ namespace LuaUtil // The package can be either a sol::table with an API or a sol::function. If it is a function, // it will be evaluated (once per sandbox) the first time when requested. If the package // is a table, then `makeReadOnly` is applied to it automatically (but not to other tables it contains). - void addCommonPackage(const std::string& packageName, const sol::object& package); + void addCommonPackage(std::string packageName, sol::object package); // Creates a new sandbox, runs a script, and returns the result // (the result is expected to be an interface of the script). @@ -58,14 +60,17 @@ namespace LuaUtil void dropScriptCache() { mCompiledScripts.clear(); } + const ScriptsConfiguration& getConfiguration() const { return *mConf; } + private: static sol::protected_function_result throwIfError(sol::protected_function_result&&); template - friend sol::protected_function_result call(sol::protected_function fn, Args&&... args); + friend sol::protected_function_result call(const sol::protected_function& fn, Args&&... args); - sol::protected_function loadScript(const std::string& path); + sol::function loadScript(const std::string& path); sol::state mLua; + const ScriptsConfiguration* mConf; sol::table mSandboxEnv; std::map mCompiledScripts; std::map mCommonPackages; @@ -75,7 +80,7 @@ namespace LuaUtil // Should be used for every call of every Lua function. // It is a workaround for a bug in `sol`. See https://github.com/ThePhD/sol2/issues/1078 template - sol::protected_function_result call(sol::protected_function fn, Args&&... args) + sol::protected_function_result call(const sol::protected_function& fn, Args&&... args) { try { @@ -101,7 +106,7 @@ namespace LuaUtil std::string toString(const sol::object&); // Makes a table read only (when accessed from Lua) by wrapping it with an empty userdata. - // Needed to forbid any changes in common resources that can accessed from different sandboxes. + // Needed to forbid any changes in common resources that can be accessed from different sandboxes. sol::table makeReadOnly(sol::table); sol::table getMutableFromReadOnly(const sol::userdata&); diff --git a/components/lua/omwscriptsparser.cpp b/components/lua/omwscriptsparser.cpp deleted file mode 100644 index bc73e013db..0000000000 --- a/components/lua/omwscriptsparser.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "omwscriptsparser.hpp" - -#include - -#include - -std::vector LuaUtil::parseOMWScriptsFiles(const VFS::Manager* vfs, const std::vector& scriptLists) -{ - auto endsWith = [](std::string_view s, std::string_view suffix) - { - return s.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), s.rbegin()); - }; - std::vector res; - for (const std::string& scriptListFile : scriptLists) - { - if (!endsWith(scriptListFile, ".omwscripts")) - { - Log(Debug::Error) << "Script list should have suffix '.omwscripts', got: '" << scriptListFile << "'"; - continue; - } - std::string content(std::istreambuf_iterator(*vfs->get(scriptListFile)), {}); - std::string_view view(content); - while (!view.empty()) - { - size_t pos = 0; - while (pos < view.size() && view[pos] != '\n') - pos++; - std::string_view line = view.substr(0, pos); - view = view.substr(std::min(pos + 1, view.size())); - if (!line.empty() && line.back() == '\r') - line = line.substr(0, pos - 1); - // Lines starting with '#' are comments. - // TODO: Maybe make the parser more robust. It is a bit inconsistent that 'path/#to/file.lua' - // is a valid path, but '#path/to/file.lua' is considered as a comment and ignored. - if (line.empty() || line[0] == '#') - continue; - if (endsWith(line, ".lua")) - res.push_back(std::string(line)); - else - Log(Debug::Error) << "Lua script should have suffix '.lua', got: '" << line.substr(0, 300) << "'"; - } - } - return res; -} diff --git a/components/lua/omwscriptsparser.hpp b/components/lua/omwscriptsparser.hpp deleted file mode 100644 index 1da9f123b2..0000000000 --- a/components/lua/omwscriptsparser.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef COMPONENTS_LUA_OMWSCRIPTSPARSER_H -#define COMPONENTS_LUA_OMWSCRIPTSPARSER_H - -#include - -namespace LuaUtil -{ - - // Parses list of `*.omwscripts` files. - std::vector parseOMWScriptsFiles(const VFS::Manager* vfs, const std::vector& scriptLists); - -} - -#endif // COMPONENTS_LUA_OMWSCRIPTSPARSER_H diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index 703381a453..517ad5f788 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -10,162 +10,249 @@ namespace LuaUtil static constexpr std::string_view INTERFACE_NAME = "interfaceName"; static constexpr std::string_view INTERFACE = "interface"; + static constexpr std::string_view HANDLER_INIT = "onInit"; static constexpr std::string_view HANDLER_SAVE = "onSave"; static constexpr std::string_view HANDLER_LOAD = "onLoad"; + static constexpr std::string_view HANDLER_INTERFACE_OVERRIDE = "onInterfaceOverride"; - static constexpr std::string_view REGISTERED_TIMER_CALLBACKS = "_timers"; - static constexpr std::string_view TEMPORARY_TIMER_CALLBACKS = "_temp_timers"; - - std::string ScriptsContainer::ScriptId::toString() const - { - std::string res = mContainer->mNamePrefix; - res.push_back('['); - res.append(mPath); - res.push_back(']'); - return res; - } - - ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix) : mNamePrefix(namePrefix), mLua(*lua) + ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix, ESM::LuaScriptCfg::Flags autoStartMode) + : mNamePrefix(namePrefix), mLua(*lua), mAutoStartMode(autoStartMode) { registerEngineHandlers({&mUpdateHandlers}); mPublicInterfaces = sol::table(lua->sol(), sol::create); addPackage("openmw.interfaces", mPublicInterfaces); } - void ScriptsContainer::addPackage(const std::string& packageName, sol::object package) + void ScriptsContainer::printError(int scriptId, std::string_view msg, const std::exception& e) { - API[packageName] = makeReadOnly(std::move(package)); + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(scriptId) << "] " << msg << ": " << e.what(); } - bool ScriptsContainer::addNewScript(const std::string& path) + void ScriptsContainer::addPackage(std::string packageName, sol::object package) { - if (mScripts.count(path) != 0) + mAPI.emplace(std::move(packageName), makeReadOnly(std::move(package))); + } + + bool ScriptsContainer::addCustomScript(int scriptId) + { + assert(mLua.getConfiguration()[scriptId].mFlags & ESM::LuaScriptCfg::sCustom); + std::optional onInit, onLoad; + bool ok = addScript(scriptId, onInit, onLoad); + if (ok && onInit) + callOnInit(scriptId, *onInit); + return ok; + } + + void ScriptsContainer::addAutoStartedScripts() + { + for (int scriptId : mLua.getConfiguration().getListByFlag(mAutoStartMode)) + { + std::optional onInit, onLoad; + bool ok = addScript(scriptId, onInit, onLoad); + if (ok && onInit) + callOnInit(scriptId, *onInit); + } + } + + bool ScriptsContainer::addScript(int scriptId, std::optional& onInit, std::optional& onLoad) + { + assert(scriptId >= 0 && scriptId < static_cast(mLua.getConfiguration().size())); + if (mScripts.count(scriptId) != 0) return false; // already present + const std::string& path = scriptPath(scriptId); + std::string debugName = mNamePrefix; + debugName.push_back('['); + debugName.append(path); + debugName.push_back(']'); + + Script& script = mScripts[scriptId]; + script.mHiddenData = mLua.newTable(); + script.mHiddenData[sScriptIdKey] = ScriptId{this, scriptId}; + script.mHiddenData[sScriptDebugNameKey] = debugName; + script.mPath = path; + try { - sol::table hiddenData(mLua.sol(), sol::create); - hiddenData[ScriptId::KEY] = ScriptId{this, path}; - hiddenData[REGISTERED_TIMER_CALLBACKS] = mLua.newTable(); - hiddenData[TEMPORARY_TIMER_CALLBACKS] = mLua.newTable(); - mScripts[path].mHiddenData = hiddenData; - sol::object script = mLua.runInNewSandbox(path, mNamePrefix, API, hiddenData); - std::string interfaceName = ""; - sol::object publicInterface = sol::nil; - if (script != sol::nil) + sol::object scriptOutput = mLua.runInNewSandbox(path, mNamePrefix, mAPI, script.mHiddenData); + if (scriptOutput == sol::nil) + return true; + sol::object engineHandlers = sol::nil, eventHandlers = sol::nil; + for (const auto& [key, value] : sol::table(scriptOutput)) { - for (auto& [key, value] : sol::table(script)) + std::string_view sectionName = key.as(); + if (sectionName == ENGINE_HANDLERS) + engineHandlers = value; + else if (sectionName == EVENT_HANDLERS) + eventHandlers = value; + else if (sectionName == INTERFACE_NAME) + script.mInterfaceName = value.as(); + else if (sectionName == INTERFACE) + script.mInterface = value.as(); + else + Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << debugName; + } + if (engineHandlers != sol::nil) + { + for (const auto& [key, fn] : sol::table(engineHandlers)) { - std::string_view sectionName = key.as(); - if (sectionName == ENGINE_HANDLERS) - parseEngineHandlers(value, path); - else if (sectionName == EVENT_HANDLERS) - parseEventHandlers(value, path); - else if (sectionName == INTERFACE_NAME) - interfaceName = value.as(); - else if (sectionName == INTERFACE) - publicInterface = value.as(); + std::string_view handlerName = key.as(); + if (handlerName == HANDLER_INIT) + onInit = sol::function(fn); + else if (handlerName == HANDLER_LOAD) + onLoad = sol::function(fn); + else if (handlerName == HANDLER_SAVE) + script.mOnSave = sol::function(fn); + else if (handlerName == HANDLER_INTERFACE_OVERRIDE) + script.mOnOverride = sol::function(fn); else - Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << mNamePrefix << "[" << path << "]"; + { + auto it = mEngineHandlers.find(handlerName); + if (it == mEngineHandlers.end()) + Log(Debug::Error) << "Not supported handler '" << handlerName << "' in " << debugName; + else + insertHandler(it->second->mList, scriptId, fn); + } } } - if (interfaceName.empty() != (publicInterface == sol::nil)) - Log(Debug::Error) << mNamePrefix << "[" << path << "]: 'interfaceName' should always be used together with 'interface'"; - else if (!interfaceName.empty()) - script.as()[INTERFACE] = mPublicInterfaces[interfaceName] = makeReadOnly(publicInterface); - mScriptOrder.push_back(path); - mScripts[path].mInterface = std::move(script); + if (eventHandlers != sol::nil) + { + for (const auto& [key, fn] : sol::table(eventHandlers)) + { + std::string_view eventName = key.as(); + auto it = mEventHandlers.find(eventName); + if (it == mEventHandlers.end()) + it = mEventHandlers.emplace(std::string(eventName), EventHandlerList()).first; + insertHandler(it->second, scriptId, fn); + } + } + + if (script.mInterfaceName.empty() == script.mInterface.has_value()) + { + Log(Debug::Error) << debugName << ": 'interfaceName' should always be used together with 'interface'"; + script.mInterfaceName.clear(); + script.mInterface = sol::nil; + } + else if (script.mInterface) + { + script.mInterface = makeReadOnly(*script.mInterface); + insertInterface(scriptId, script); + } + return true; } catch (std::exception& e) { - mScripts.erase(path); - Log(Debug::Error) << "Can't start " << mNamePrefix << "[" << path << "]; " << e.what(); + mScripts[scriptId].mHiddenData[sScriptIdKey] = sol::nil; + mScripts.erase(scriptId); + Log(Debug::Error) << "Can't start " << debugName << "; " << e.what(); return false; } } - bool ScriptsContainer::removeScript(const std::string& path) + void ScriptsContainer::removeScript(int scriptId) { - auto scriptIter = mScripts.find(path); + auto scriptIter = mScripts.find(scriptId); if (scriptIter == mScripts.end()) - return false; // no such script - scriptIter->second.mHiddenData[ScriptId::KEY] = sol::nil; - sol::object& script = scriptIter->second.mInterface; - if (getFieldOrNil(script, INTERFACE_NAME) != sol::nil) - { - std::string_view interfaceName = getFieldOrNil(script, INTERFACE_NAME).as(); - if (mPublicInterfaces[interfaceName] == getFieldOrNil(script, INTERFACE)) - { - mPublicInterfaces[interfaceName] = sol::nil; - auto prevIt = mScriptOrder.rbegin(); - while (*prevIt != path) - prevIt++; - prevIt++; - while (prevIt != mScriptOrder.rend()) - { - sol::object& prevScript = mScripts[*(prevIt++)].mInterface; - sol::object prevInterfaceName = getFieldOrNil(prevScript, INTERFACE_NAME); - if (prevInterfaceName != sol::nil && prevInterfaceName.as() == interfaceName) - { - mPublicInterfaces[interfaceName] = getFieldOrNil(prevScript, INTERFACE); - break; - } - } - } - } - sol::object engineHandlers = getFieldOrNil(script, ENGINE_HANDLERS); - if (engineHandlers != sol::nil) - { - for (auto& [key, value] : sol::table(engineHandlers)) - { - std::string_view handlerName = key.as(); - auto handlerIter = mEngineHandlers.find(handlerName); - if (handlerIter == mEngineHandlers.end()) - continue; - std::vector& list = handlerIter->second->mList; - list.erase(std::find(list.begin(), list.end(), value.as())); - } - } - sol::object eventHandlers = getFieldOrNil(script, EVENT_HANDLERS); - if (eventHandlers != sol::nil) - { - for (auto& [key, value] : sol::table(eventHandlers)) - { - EventHandlerList& list = mEventHandlers.find(key.as())->second; - list.erase(std::find(list.begin(), list.end(), value.as())); - } - } + return; // no such script + Script& script = scriptIter->second; + if (script.mInterface) + removeInterface(scriptId, script); + script.mHiddenData[sScriptIdKey] = sol::nil; mScripts.erase(scriptIter); - mScriptOrder.erase(std::find(mScriptOrder.begin(), mScriptOrder.end(), path)); - return true; + for (auto& [_, handlers] : mEngineHandlers) + removeHandler(handlers->mList, scriptId); + for (auto& [_, handlers] : mEventHandlers) + removeHandler(handlers, scriptId); } - void ScriptsContainer::parseEventHandlers(sol::table handlers, std::string_view scriptPath) + void ScriptsContainer::insertInterface(int scriptId, const Script& script) { - for (auto& [key, value] : handlers) + assert(script.mInterface); + const Script* prev = nullptr; + const Script* next = nullptr; + int nextId = 0; + for (const auto& [otherId, otherScript] : mScripts) { - std::string_view eventName = key.as(); - auto it = mEventHandlers.find(eventName); - if (it == mEventHandlers.end()) - it = mEventHandlers.insert({std::string(eventName), EventHandlerList()}).first; - it->second.push_back(value); - } - } - - void ScriptsContainer::parseEngineHandlers(sol::table handlers, std::string_view scriptPath) - { - for (auto& [key, value] : handlers) - { - std::string_view handlerName = key.as(); - if (handlerName == HANDLER_LOAD || handlerName == HANDLER_SAVE) - continue; // save and load are handled separately - auto it = mEngineHandlers.find(handlerName); - if (it == mEngineHandlers.end()) - Log(Debug::Error) << "Not supported handler '" << handlerName << "' in " << mNamePrefix << "[" << scriptPath << "]"; + if (scriptId == otherId || script.mInterfaceName != otherScript.mInterfaceName) + continue; + if (otherId < scriptId) + prev = &otherScript; else - it->second->mList.push_back(value); + { + next = &otherScript; + nextId = otherId; + break; + } } + if (prev && script.mOnOverride) + { + try { LuaUtil::call(*script.mOnOverride, *prev->mInterface); } + catch (std::exception& e) { printError(scriptId, "onInterfaceOverride failed", e); } + } + if (next && next->mOnOverride) + { + try { LuaUtil::call(*next->mOnOverride, *script.mInterface); } + catch (std::exception& e) { printError(nextId, "onInterfaceOverride failed", e); } + } + if (next == nullptr) + mPublicInterfaces[script.mInterfaceName] = *script.mInterface; + } + + void ScriptsContainer::removeInterface(int scriptId, const Script& script) + { + assert(script.mInterface); + const Script* prev = nullptr; + const Script* next = nullptr; + int nextId = 0; + for (const auto& [otherId, otherScript] : mScripts) + { + if (scriptId == otherId || script.mInterfaceName != otherScript.mInterfaceName) + continue; + if (otherId < scriptId) + prev = &otherScript; + else + { + next = &otherScript; + nextId = otherId; + break; + } + } + if (next) + { + if (next->mOnOverride) + { + sol::object prevInterface = sol::nil; + if (prev) + prevInterface = *prev->mInterface; + try { LuaUtil::call(*next->mOnOverride, prevInterface); } + catch (std::exception& e) { printError(nextId, "onInterfaceOverride failed", e); } + } + } + else if (prev) + mPublicInterfaces[script.mInterfaceName] = *prev->mInterface; + else + mPublicInterfaces[script.mInterfaceName] = sol::nil; + } + + void ScriptsContainer::insertHandler(std::vector& list, int scriptId, sol::function fn) + { + list.emplace_back(); + int pos = list.size() - 1; + while (pos > 0 && list[pos - 1].mScriptId > scriptId) + { + list[pos] = std::move(list[pos - 1]); + pos--; + } + list[pos].mScriptId = scriptId; + list[pos].mFn = std::move(fn); + } + + void ScriptsContainer::removeHandler(std::vector& list, int scriptId) + { + list.erase(std::remove_if(list.begin(), list.end(), + [scriptId](const Handler& h){ return h.mScriptId == scriptId; }), + list.end()); } void ScriptsContainer::receiveEvent(std::string_view eventName, std::string_view eventData) @@ -191,13 +278,14 @@ namespace LuaUtil { try { - sol::object res = LuaUtil::call(list[i], data); + sol::object res = LuaUtil::call(list[i].mFn, data); if (res != sol::nil && !res.as()) break; // Skip other handlers if 'false' was returned. } catch (std::exception& e) { - Log(Debug::Error) << mNamePrefix << " eventHandler[" << eventName << "] failed. " << e.what(); + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(list[i].mScriptId) + << "] eventHandler[" << eventName << "] failed. " << e.what(); } } } @@ -208,9 +296,19 @@ namespace LuaUtil mEngineHandlers[h->mName] = h; } + void ScriptsContainer::callOnInit(int scriptId, const sol::function& onInit) + { + try + { + const std::string& data = mLua.getConfiguration()[scriptId].mInitializationData; + LuaUtil::call(onInit, deserialize(mLua.sol(), data, mSerializer)); + } + catch (std::exception& e) { printError(scriptId, "onInit failed", e); } + } + void ScriptsContainer::save(ESM::LuaScripts& data) { - std::map> timers; + std::map> timers; auto saveTimerFn = [&](const Timer& timer, TimeUnit timeUnit) { if (!timer.mSerializable) @@ -220,78 +318,87 @@ namespace LuaUtil savedTimer.mUnit = timeUnit; savedTimer.mCallbackName = std::get(timer.mCallback); savedTimer.mCallbackArgument = timer.mSerializedArg; - if (timers.count(timer.mScript) == 0) - timers[timer.mScript] = {}; - timers[timer.mScript].push_back(std::move(savedTimer)); + timers[timer.mScriptId].push_back(std::move(savedTimer)); }; for (const Timer& timer : mSecondsTimersQueue) saveTimerFn(timer, TimeUnit::SECONDS); for (const Timer& timer : mHoursTimersQueue) saveTimerFn(timer, TimeUnit::HOURS); data.mScripts.clear(); - for (const std::string& path : mScriptOrder) + for (auto& [scriptId, script] : mScripts) { ESM::LuaScript savedScript; - savedScript.mScriptPath = path; - sol::object handler = getFieldOrNil(mScripts[path].mInterface, ENGINE_HANDLERS, HANDLER_SAVE); - if (handler != sol::nil) + // Note: We can not use `scriptPath(scriptId)` here because `save` can be called during + // evaluating "reloadlua" command when ScriptsConfiguration is already changed. + savedScript.mScriptPath = script.mPath; + if (script.mOnSave) { try { - sol::object state = LuaUtil::call(handler); + sol::object state = LuaUtil::call(*script.mOnSave); savedScript.mData = serialize(state, mSerializer); } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << path << "] onSave failed: " << e.what(); - } + catch (std::exception& e) { printError(scriptId, "onSave failed", e); } } - auto timersIt = timers.find(path); + auto timersIt = timers.find(scriptId); if (timersIt != timers.end()) savedScript.mTimers = std::move(timersIt->second); data.mScripts.push_back(std::move(savedScript)); } } - void ScriptsContainer::load(const ESM::LuaScripts& data, bool resetScriptList) + void ScriptsContainer::load(const ESM::LuaScripts& data) { - std::map scriptsWithoutSavedData; - if (resetScriptList) + removeAllScripts(); + const ScriptsConfiguration& cfg = mLua.getConfiguration(); + + std::map scripts; + for (int scriptId : mLua.getConfiguration().getListByFlag(mAutoStartMode)) + scripts[scriptId] = nullptr; + for (const ESM::LuaScript& s : data.mScripts) { - removeAllScripts(); - for (const ESM::LuaScript& script : data.mScripts) - addNewScript(script.mScriptPath); - } - else - scriptsWithoutSavedData = mScripts; - mSecondsTimersQueue.clear(); - mHoursTimersQueue.clear(); - for (const ESM::LuaScript& script : data.mScripts) - { - auto iter = mScripts.find(script.mScriptPath); - if (iter == mScripts.end()) + std::optional scriptId = cfg.findId(s.mScriptPath); + if (!scriptId) + { + Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; script not registered"; continue; - scriptsWithoutSavedData.erase(iter->first); - iter->second.mHiddenData.get(TEMPORARY_TIMER_CALLBACKS).clear(); - try + } + if (!(cfg[*scriptId].mFlags & (ESM::LuaScriptCfg::sCustom | mAutoStartMode))) { - sol::object handler = getFieldOrNil(iter->second.mInterface, ENGINE_HANDLERS, HANDLER_LOAD); - if (handler != sol::nil) + Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; this script is not allowed here"; + continue; + } + scripts[*scriptId] = &s; + } + + for (const auto& [scriptId, savedScript] : scripts) + { + std::optional onInit, onLoad; + if (!addScript(scriptId, onInit, onLoad)) + continue; + if (savedScript == nullptr) + { + if (onInit) + callOnInit(scriptId, *onInit); + continue; + } + if (onLoad) + { + try { - sol::object state = deserialize(mLua.sol(), script.mData, mSerializer); - LuaUtil::call(handler, state); + sol::object state = deserialize(mLua.sol(), savedScript->mData, mSerializer); + sol::object initializationData = + deserialize(mLua.sol(), mLua.getConfiguration()[scriptId].mInitializationData, mSerializer); + LuaUtil::call(*onLoad, state, initializationData); } + catch (std::exception& e) { printError(scriptId, "onLoad failed", e); } } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] onLoad failed: " << e.what(); - } - for (const ESM::LuaTimer& savedTimer : script.mTimers) + for (const ESM::LuaTimer& savedTimer : savedScript->mTimers) { Timer timer; timer.mCallback = savedTimer.mCallbackName; timer.mSerializable = true; - timer.mScript = script.mScriptPath; + timer.mScriptId = scriptId; timer.mTime = savedTimer.mTime; try @@ -306,24 +413,10 @@ namespace LuaUtil else mSecondsTimersQueue.push_back(std::move(timer)); } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] can not load timer: " << e.what(); - } - } - } - for (auto& [path, script] : scriptsWithoutSavedData) - { - script.mHiddenData.get(TEMPORARY_TIMER_CALLBACKS).clear(); - sol::object handler = getFieldOrNil(script.mInterface, ENGINE_HANDLERS, HANDLER_LOAD); - if (handler == sol::nil) - continue; - try { LuaUtil::call(handler); } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << path << "] onLoad failed: " << e.what(); + catch (std::exception& e) { printError(scriptId, "can not load timer", e); } } } + std::make_heap(mSecondsTimersQueue.begin(), mSecondsTimersQueue.end()); std::make_heap(mHoursTimersQueue.begin(), mHoursTimersQueue.end()); } @@ -331,15 +424,16 @@ namespace LuaUtil ScriptsContainer::~ScriptsContainer() { for (auto& [_, script] : mScripts) - script.mHiddenData[ScriptId::KEY] = sol::nil; + script.mHiddenData[sScriptIdKey] = sol::nil; } + // Note: shouldn't be called from destructor because mEngineHandlers has pointers on + // external objects that are already removed during child class destruction. void ScriptsContainer::removeAllScripts() { for (auto& [_, script] : mScripts) - script.mHiddenData[ScriptId::KEY] = sol::nil; + script.mHiddenData[sScriptIdKey] = sol::nil; mScripts.clear(); - mScriptOrder.clear(); for (auto& [_, handlers] : mEngineHandlers) handlers->mList.clear(); mEventHandlers.clear(); @@ -351,17 +445,17 @@ namespace LuaUtil mPublicInterfaces[sol::meta_function::index] = mPublicInterfaces; } - sol::table ScriptsContainer::getHiddenData(const std::string& scriptPath) + ScriptsContainer::Script& ScriptsContainer::getScript(int scriptId) { - auto it = mScripts.find(scriptPath); + auto it = mScripts.find(scriptId); if (it == mScripts.end()) - throw std::logic_error("ScriptsContainer::getHiddenData: script doesn't exist"); - return it->second.mHiddenData; + throw std::logic_error("Script doesn't exist"); + return it->second; } - void ScriptsContainer::registerTimerCallback(const std::string& scriptPath, std::string_view callbackName, sol::function callback) + void ScriptsContainer::registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback) { - getHiddenData(scriptPath)[REGISTERED_TIMER_CALLBACKS][callbackName] = std::move(callback); + getScript(scriptId).mRegisteredCallbacks.emplace(std::string(callbackName), std::move(callback)); } void ScriptsContainer::insertTimer(std::vector& timerQueue, Timer&& t) @@ -370,12 +464,12 @@ namespace LuaUtil std::push_heap(timerQueue.begin(), timerQueue.end()); } - void ScriptsContainer::setupSerializableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, + void ScriptsContainer::setupSerializableTimer(TimeUnit timeUnit, double time, int scriptId, std::string_view callbackName, sol::object callbackArg) { Timer t; t.mCallback = std::string(callbackName); - t.mScript = scriptPath; + t.mScriptId = scriptId; t.mSerializable = true; t.mTime = time; t.mArg = callbackArg; @@ -383,15 +477,15 @@ namespace LuaUtil insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); } - void ScriptsContainer::setupUnsavableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, sol::function callback) + void ScriptsContainer::setupUnsavableTimer(TimeUnit timeUnit, double time, int scriptId, sol::function callback) { Timer t; - t.mScript = scriptPath; + t.mScriptId = scriptId; t.mSerializable = false; t.mTime = time; t.mCallback = mTemporaryCallbackCounter; - getHiddenData(scriptPath)[TEMPORARY_TIMER_CALLBACKS][mTemporaryCallbackCounter] = std::move(callback); + getScript(t.mScriptId).mTemporaryCallbacks.emplace(mTemporaryCallbackCounter, std::move(callback)); mTemporaryCallbackCounter++; insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); @@ -401,30 +495,23 @@ namespace LuaUtil { try { - sol::table data = getHiddenData(t.mScript); + Script& script = getScript(t.mScriptId); if (t.mSerializable) { const std::string& callbackName = std::get(t.mCallback); - sol::object callback = data[REGISTERED_TIMER_CALLBACKS][callbackName]; - if (!callback.is()) + auto it = script.mRegisteredCallbacks.find(callbackName); + if (it == script.mRegisteredCallbacks.end()) throw std::logic_error("Callback '" + callbackName + "' doesn't exist"); - LuaUtil::call(callback, t.mArg); + LuaUtil::call(it->second, t.mArg); } else { int64_t id = std::get(t.mCallback); - sol::table callbacks = data[TEMPORARY_TIMER_CALLBACKS]; - sol::object callback = callbacks[id]; - if (!callback.is()) - throw std::logic_error("Temporary timer callback doesn't exist"); - LuaUtil::call(callback); - callbacks[id] = sol::nil; + LuaUtil::call(script.mTemporaryCallbacks.at(id)); + script.mTemporaryCallbacks.erase(id); } } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << t.mScript << "] callTimer failed: " << e.what(); - } + catch (std::exception& e) { printError(t.mScriptId, "callTimer failed", e); } } void ScriptsContainer::updateTimerQueue(std::vector& timerQueue, double time) @@ -443,4 +530,13 @@ namespace LuaUtil updateTimerQueue(mHoursTimersQueue, gameHours); } + void Callback::operator()(sol::object arg) const + { + if (mHiddenData[ScriptsContainer::sScriptIdKey] != sol::nil) + LuaUtil::call(mFunc, std::move(arg)); + else + Log(Debug::Debug) << "Ignored callback to the removed script " + << mHiddenData.get(ScriptsContainer::sScriptDebugNameKey); + } + } diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 69aa18e940..e934868d08 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -17,7 +17,7 @@ namespace LuaUtil // ScriptsContainer is a base class for all scripts containers (LocalScripts, // GlobalScripts, PlayerScripts, etc). Each script runs in a separate sandbox. // Scripts from different containers can interact to each other only via events. -// Scripts within one container can interact via interfaces (not implemented yet). +// Scripts within one container can interact via interfaces. // All scripts from one container have the same set of API packages available. // // Each script should return a table in a specific format that describes its @@ -42,11 +42,12 @@ namespace LuaUtil // -- An error is printed if unknown handler is specified. // engineHandlers = { // onUpdate = update, +// onInit = function(initData) ... end, -- used when the script is just created (not loaded) // onSave = function() return ... end, -// onLoad = function(state) ... end, -- "state" is the data that was earlier returned by onSave +// onLoad = function(state, initData) ... end, -- "state" is the data that was earlier returned by onSave // -// -- Works only if ScriptsContainer::registerEngineHandler is overloaded in a child class -// -- and explicitly supports 'onSomethingElse' +// -- Works only if a child class has passed a EngineHandlerList +// -- for 'onSomethingElse' to ScriptsContainer::registerEngineHandlers. // onSomethingElse = function() print("something else") end // }, // @@ -59,36 +60,44 @@ namespace LuaUtil class ScriptsContainer { public: + // ScriptId of each script is stored with this key in Script::mHiddenData. + // Removed from mHiddenData when the script if removed. + constexpr static std::string_view sScriptIdKey = "_id"; + + // Debug identifier of each script is stored with this key in Script::mHiddenData. + // Present in mHiddenData even after removal of the script from ScriptsContainer. + constexpr static std::string_view sScriptDebugNameKey = "_name"; + struct ScriptId { - // ScriptId is stored in hidden data (see getHiddenData) with this key. - constexpr static std::string_view KEY = "_id"; - ScriptsContainer* mContainer; - std::string mPath; - - std::string toString() const; + int mIndex; // index in LuaUtil::ScriptsConfiguration }; using TimeUnit = ESM::LuaTimer::TimeUnit; // `namePrefix` is a common prefix for all scripts in the container. Used in logs for error messages and `print` output. - ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix); + // `autoStartMode` specifies the list of scripts that should be autostarted in this container; the list itself is + // stored in ScriptsConfiguration: lua->getConfiguration().getListByFlag(autoStartMode). + ScriptsContainer(LuaState* lua, std::string_view namePrefix, ESM::LuaScriptCfg::Flags autoStartMode = 0); + ScriptsContainer(const ScriptsContainer&) = delete; ScriptsContainer(ScriptsContainer&&) = delete; virtual ~ScriptsContainer(); + ESM::LuaScriptCfg::Flags getAutoStartMode() const { return mAutoStartMode; } + // Adds package that will be available (via `require`) for all scripts in the container. // Automatically applies LuaUtil::makeReadOnly to the package. - void addPackage(const std::string& packageName, sol::object package); + void addPackage(std::string packageName, sol::object package); - // Finds a file with given path in the virtual file system, starts as a new script, and adds it to the container. - // Returns `true` if the script was successfully added. Otherwise prints an error message and returns `false`. - // `false` can be returned if either file not found or has syntax errors or such script already exists in the container. - bool addNewScript(const std::string& path); + // Gets script with given id from ScriptsConfiguration, finds the source in the virtual file system, starts as a new script, + // adds it to the container, and calls onInit for this script. Returns `true` if the script was successfully added. + // The script should have CUSTOM flag. If the flag is not set, or file not found, or has syntax errors, returns false. + // If such script already exists in the container, then also returns false. + bool addCustomScript(int scriptId); - // Removes script. Returns `true` if it was successfully removed. - bool removeScript(const std::string& path); - void removeAllScripts(); + bool hasScript(int scriptId) const { return mScripts.count(scriptId) != 0; } + void removeScript(int scriptId); // Processes timers. gameSeconds and gameHours are time (in seconds and in game hours) passed from the game start. void processTimers(double gameSeconds, double gameHours); @@ -107,22 +116,22 @@ namespace LuaUtil // only built-in types and types from util package can be serialized. void setSerializer(const UserdataSerializer* serializer) { mSerializer = serializer; } + // Starts scripts according to `autoStartMode` and calls `onInit` for them. Not needed if `load` is used. + void addAutoStartedScripts(); + + // Removes all scripts including the auto started. + void removeAllScripts(); + // Calls engineHandler "onSave" for every script and saves the list of the scripts with serialized data to ESM::LuaScripts. void save(ESM::LuaScripts&); - // Calls engineHandler "onLoad" for every script with given data. - // If resetScriptList=true, then removes all currently active scripts and runs the scripts that were saved in ESM::LuaScripts. - // If resetScriptList=false, then list of running scripts is not changed, only engineHandlers "onLoad" are called. - void load(const ESM::LuaScripts&, bool resetScriptList); - - // Returns the hidden data of a script. - // Each script has a corresponding "hidden data" - a lua table that is not accessible from the script itself, - // but can be used by built-in packages. It contains ScriptId and can contain any arbitrary data. - sol::table getHiddenData(const std::string& scriptPath); + // Removes all scripts; starts scripts according to `autoStartMode` and + // loads the savedScripts. Runs "onLoad" for each script. + void load(const ESM::LuaScripts& savedScripts); // Callbacks for serializable timers should be registered in advance. // The script with the given path should already present in the container. - void registerTimerCallback(const std::string& scriptPath, std::string_view callbackName, sol::function callback); + void registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback); // Sets up a timer, that can be automatically saved and loaded. // timeUnit - game seconds (TimeUnit::Seconds) or game hours (TimeUnit::Hours). @@ -130,18 +139,24 @@ namespace LuaUtil // scriptPath - script path in VFS is used as script id. The script with the given path should already present in the container. // callbackName - callback (should be registered in advance) for this timer. // callbackArg - parameter for the callback (should be serializable). - void setupSerializableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, + void setupSerializableTimer(TimeUnit timeUnit, double time, int scriptId, std::string_view callbackName, sol::object callbackArg); // Creates a timer. `callback` is an arbitrary Lua function. This type of timers is called "unsavable" // because it can not be stored in saves. I.e. loading a saved game will not fully restore the state. - void setupUnsavableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, sol::function callback); + void setupUnsavableTimer(TimeUnit timeUnit, double time, int scriptId, sol::function callback); protected: + struct Handler + { + int mScriptId; + sol::function mFn; + }; + struct EngineHandlerList { std::string_view mName; - std::vector mList; + std::vector mList; // "name" must be string literal explicit EngineHandlerList(std::string_view name) : mName(name) {} @@ -151,12 +166,13 @@ namespace LuaUtil template void callEngineHandlers(EngineHandlerList& handlers, const Args&... args) { - for (sol::protected_function& handler : handlers.mList) + for (Handler& handler : handlers.mList) { - try { LuaUtil::call(handler, args...); } + try { LuaUtil::call(handler.mFn, args...); } catch (std::exception& e) { - Log(Debug::Error) << mNamePrefix << " " << handlers.mName << " failed. " << e.what(); + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(handler.mScriptId) << "] " + << handlers.mName << " failed. " << e.what(); } } } @@ -171,34 +187,50 @@ namespace LuaUtil private: struct Script { - sol::object mInterface; // returned value of the script (sol::table or nil) + std::optional mOnSave; + std::optional mOnOverride; + std::optional mInterface; + std::string mInterfaceName; sol::table mHiddenData; + std::map mRegisteredCallbacks; + std::map mTemporaryCallbacks; + std::string mPath; }; struct Timer { double mTime; bool mSerializable; - std::string mScript; + int mScriptId; std::variant mCallback; // string if serializable, integer otherwise sol::object mArg; std::string mSerializedArg; bool operator<(const Timer& t) const { return mTime > t.mTime; } }; - using EventHandlerList = std::vector; + using EventHandlerList = std::vector; - void parseEngineHandlers(sol::table handlers, std::string_view scriptPath); - void parseEventHandlers(sol::table handlers, std::string_view scriptPath); + // Add to container without calling onInit/onLoad. + bool addScript(int scriptId, std::optional& onInit, std::optional& onLoad); + // Returns script by id (throws an exception if doesn't exist) + Script& getScript(int scriptId); + + void printError(int scriptId, std::string_view msg, const std::exception& e); + const std::string& scriptPath(int scriptId) const { return mLua.getConfiguration()[scriptId].mScriptPath; } + void callOnInit(int scriptId, const sol::function& onInit); void callTimer(const Timer& t); void updateTimerQueue(std::vector& timerQueue, double time); static void insertTimer(std::vector& timerQueue, Timer&& t); + static void insertHandler(std::vector& list, int scriptId, sol::function fn); + static void removeHandler(std::vector& list, int scriptId); + void insertInterface(int scriptId, const Script& script); + void removeInterface(int scriptId, const Script& script); + ESM::LuaScriptCfg::Flags mAutoStartMode; const UserdataSerializer* mSerializer = nullptr; - std::map API; + std::map mAPI; - std::vector mScriptOrder; - std::map mScripts; + std::map mScripts; sol::table mPublicInterfaces; EngineHandlerList mUpdateHandlers{"onUpdate"}; @@ -210,6 +242,17 @@ namespace LuaUtil int64_t mTemporaryCallbackCounter = 0; }; + // Wrapper for a single-argument Lua function. + // Holds information about the script the function belongs to. + // Needed to prevent callback calls if the script was removed. + struct Callback + { + sol::function mFunc; + sol::table mHiddenData; // same object as Script::mHiddenData in ScriptsContainer + + void operator()(sol::object arg) const; + }; + } #endif // COMPONENTS_LUA_SCRIPTSCONTAINER_H diff --git a/components/misc/algorithm.hpp b/components/misc/algorithm.hpp index 4d70afa86c..54ac74e97e 100644 --- a/components/misc/algorithm.hpp +++ b/components/misc/algorithm.hpp @@ -4,6 +4,8 @@ #include #include +#include "stringops.hpp" + namespace Misc { template @@ -31,6 +33,30 @@ namespace Misc } return begin; } + + /// Performs a binary search on a sorted container for a string that 'key' starts with + template + static Iterator partialBinarySearch(Iterator begin, Iterator end, const T& key) + { + const Iterator notFound = end; + + while(begin < end) + { + const Iterator middle = begin + (std::distance(begin, end) / 2); + + int comp = Misc::StringUtils::ciCompareLen((*middle), key, (*middle).size()); + + if(comp == 0) + return middle; + else if(comp > 0) + end = middle; + else + begin = middle + 1; + } + + return notFound; + } + } #endif diff --git a/components/misc/convert.hpp b/components/misc/convert.hpp index 81270c0c0b..6f4a55cfcc 100644 --- a/components/misc/convert.hpp +++ b/components/misc/convert.hpp @@ -19,11 +19,6 @@ namespace Convert return osg::Vec3f(values[0], values[1], values[2]); } - inline osg::Vec3f makeOsgVec3f(const btVector3& value) - { - return osg::Vec3f(value.x(), value.y(), value.z()); - } - inline osg::Vec3f makeOsgVec3f(const ESM::Pathgrid::Point& value) { return osg::Vec3f(value.mX, value.mY, value.mZ); diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index 4d54baafc6..610c7a790c 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -142,5 +142,5 @@ std::string Misc::ResourceHelpers::correctActorModelPath(const std::string &resP bool Misc::ResourceHelpers::isHiddenMarker(std::string_view id) { - return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker"; + return Misc::StringUtils::ciEqual(id, "prisonmarker") || Misc::StringUtils::ciEqual(id, "divinemarker") || Misc::StringUtils::ciEqual(id, "templemarker") || Misc::StringUtils::ciEqual(id, "northmarker"); } diff --git a/components/misc/stringops.hpp b/components/misc/stringops.hpp index 0863522356..12633db826 100644 --- a/components/misc/stringops.hpp +++ b/components/misc/stringops.hpp @@ -6,8 +6,7 @@ #include #include #include - -#include "utf8stream.hpp" +#include namespace Misc { @@ -24,6 +23,7 @@ class StringUtils template static T argument(T value) noexcept { + static_assert(!std::is_same_v, "std::string_view is not supported"); return value; } @@ -43,70 +43,6 @@ public: return (c >= 'A' && c <= 'Z') ? c + 'a' - 'A' : c; } - static Utf8Stream::UnicodeChar toLowerUtf8(Utf8Stream::UnicodeChar ch) - { - // Russian alphabet - if (ch >= 0x0410 && ch < 0x0430) - return ch + 0x20; - - // Cyrillic IO character - if (ch == 0x0401) - return ch + 0x50; - - // Latin alphabet - if (ch >= 0x41 && ch < 0x60) - return ch + 0x20; - - // Deutch characters - if (ch == 0xc4 || ch == 0xd6 || ch == 0xdc) - return ch + 0x20; - if (ch == 0x1e9e) - return 0xdf; - - // TODO: probably we will need to support characters from other languages - - return ch; - } - - static std::string lowerCaseUtf8(const std::string& str) - { - if (str.empty()) - return str; - - // Decode string as utf8 characters, convert to lower case and pack them to string - std::string out; - Utf8Stream stream (str.c_str()); - while (!stream.eof ()) - { - Utf8Stream::UnicodeChar character = toLowerUtf8(stream.peek()); - - if (character <= 0x7f) - out.append(1, static_cast(character)); - else if (character <= 0x7ff) - { - out.append(1, static_cast(0xc0 | ((character >> 6) & 0x1f))); - out.append(1, static_cast(0x80 | (character & 0x3f))); - } - else if (character <= 0xffff) - { - out.append(1, static_cast(0xe0 | ((character >> 12) & 0x0f))); - out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); - out.append(1, static_cast(0x80 | (character & 0x3f))); - } - else - { - out.append(1, static_cast(0xf0 | ((character >> 18) & 0x07))); - out.append(1, static_cast(0x80 | ((character >> 12) & 0x3f))); - out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); - out.append(1, static_cast(0x80 | (character & 0x3f))); - } - - stream.consume(); - } - - return out; - } - static bool ciLess(const std::string &x, const std::string &y) { return std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end(), ci()); } @@ -182,6 +118,21 @@ public: return out; } + struct CiEqual + { + bool operator()(const std::string& left, const std::string& right) const + { + return ciEqual(left, right); + } + }; + struct CiHash + { + std::size_t operator()(std::string str) const + { + lowerCaseInPlace(str); + return std::hash{}(str); + } + }; struct CiComp { bool operator()(const std::string& left, const std::string& right) const @@ -190,55 +141,21 @@ public: } }; - - /// Performs a binary search on a sorted container for a string that 'key' starts with - template - static Iterator partialBinarySearch(Iterator begin, Iterator end, const T& key) - { - const Iterator notFound = end; - - while(begin < end) - { - const Iterator middle = begin + (std::distance(begin, end) / 2); - - int comp = Misc::StringUtils::ciCompareLen((*middle), key, (*middle).size()); - - if(comp == 0) - return middle; - else if(comp > 0) - end = middle; - else - begin = middle + 1; - } - - return notFound; - } - /** @brief Replaces all occurrences of a string in another string. * * @param str The string to operate on. * @param what The string to replace. * @param with The replacement string. - * @param whatLen The length of the string to replace. - * @param withLen The length of the replacement string. - * * @return A reference to the string passed in @p str. */ - static std::string &replaceAll(std::string &str, const char *what, const char *with, - std::size_t whatLen=std::string::npos, std::size_t withLen=std::string::npos) + static std::string &replaceAll(std::string &str, std::string_view what, std::string_view with) { - if (whatLen == std::string::npos) - whatLen = strlen(what); - - if (withLen == std::string::npos) - withLen = strlen(with); - std::size_t found; std::size_t offset = 0; - while((found = str.find(what, offset, whatLen)) != std::string::npos) + while((found = str.find(what, offset)) != std::string::npos) { - str.replace(found, whatLen, with, withLen); - offset = found + withLen; + str.replace(found, what.size(), with); + offset = found + with.size(); } return str; } @@ -294,28 +211,19 @@ public: cont.push_back(str.substr(previous, current - previous)); } - // TODO: use the std::string_view once we will use the C++17. - // It should allow us to avoid data copying while we still will support both string and literal arguments. - - static inline void replaceAll(std::string& data, const std::string& toSearch, const std::string& replaceStr) + static inline void replaceLast(std::string& str, const std::string& substr, const std::string& with) { - size_t pos = data.find(toSearch); - - while( pos != std::string::npos) - { - data.replace(pos, toSearch.size(), replaceStr); - pos = data.find(toSearch, pos + replaceStr.size()); - } + size_t pos = str.rfind(substr); + if (pos == std::string::npos) + return; + str.replace(pos, substr.size(), with); } - static inline void replaceLast(std::string& str, const std::string& substr, const std::string& with) - { - size_t pos = str.rfind(substr); - if (pos == std::string::npos) - return; - - str.replace(pos, substr.size(), with); - } + static inline bool ciEndsWith(std::string_view s, std::string_view suffix) + { + return s.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), s.rbegin(), + [](char l, char r) { return toLower(l) == toLower(r); }); + }; }; } diff --git a/components/misc/utf8stream.hpp b/components/misc/utf8stream.hpp index e499d15e60..9dc8aa8208 100644 --- a/components/misc/utf8stream.hpp +++ b/components/misc/utf8stream.hpp @@ -2,6 +2,7 @@ #define MISC_UTF8ITER_HPP #include +#include #include class Utf8Stream @@ -87,6 +88,70 @@ public: return std::make_pair (chr, cur); } + static UnicodeChar toLowerUtf8(UnicodeChar ch) + { + // Russian alphabet + if (ch >= 0x0410 && ch < 0x0430) + return ch + 0x20; + + // Cyrillic IO character + if (ch == 0x0401) + return ch + 0x50; + + // Latin alphabet + if (ch >= 0x41 && ch < 0x60) + return ch + 0x20; + + // German characters + if (ch == 0xc4 || ch == 0xd6 || ch == 0xdc) + return ch + 0x20; + if (ch == 0x1e9e) + return 0xdf; + + // TODO: probably we will need to support characters from other languages + + return ch; + } + + static std::string lowerCaseUtf8(const std::string& str) + { + if (str.empty()) + return str; + + // Decode string as utf8 characters, convert to lower case and pack them to string + std::string out; + Utf8Stream stream (str.c_str()); + while (!stream.eof ()) + { + UnicodeChar character = toLowerUtf8(stream.peek()); + + if (character <= 0x7f) + out.append(1, static_cast(character)); + else if (character <= 0x7ff) + { + out.append(1, static_cast(0xc0 | ((character >> 6) & 0x1f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + else if (character <= 0xffff) + { + out.append(1, static_cast(0xe0 | ((character >> 12) & 0x0f))); + out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + else + { + out.append(1, static_cast(0xf0 | ((character >> 18) & 0x07))); + out.append(1, static_cast(0x80 | ((character >> 12) & 0x3f))); + out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + + stream.consume(); + } + + return out; + } + private: static std::pair octet_count (unsigned char octet) diff --git a/components/myguiplatform/myguirendermanager.cpp b/components/myguiplatform/myguirendermanager.cpp index 3061b329c2..43b176c795 100644 --- a/components/myguiplatform/myguirendermanager.cpp +++ b/components/myguiplatform/myguirendermanager.cpp @@ -4,10 +4,8 @@ #include #include -#include #include #include -#include #include @@ -17,8 +15,6 @@ #include #include -#include - #include "myguicompat.h" #include "myguitexture.hpp" @@ -465,23 +461,17 @@ void RenderManager::doRender(MyGUI::IVertexBuffer *buffer, MyGUI::ITexture *text batch.mVertexBuffer = static_cast(buffer)->getVertexBuffer(); batch.mArray = static_cast(buffer)->getVertexArray(); static_cast(buffer)->markUsed(); - bool premultipliedAlpha = false; - if (texture) + + if (OSGTexture* osgtexture = static_cast(texture)) { - batch.mTexture = static_cast(texture)->getTexture(); + batch.mTexture = osgtexture->getTexture(); if (batch.mTexture->getDataVariance() == osg::Object::DYNAMIC) mDrawable->setDataVariance(osg::Object::DYNAMIC); // only for this frame, reset in begin() - batch.mTexture->getUserValue("premultiplied alpha", premultipliedAlpha); + if (!mInjectState && osgtexture->getInjectState()) + batch.mStateSet = osgtexture->getInjectState(); } if (mInjectState) batch.mStateSet = mInjectState; - else if (premultipliedAlpha) - { - // This is hacky, but MyGUI made it impossible to use a custom layer for a nested node, so state couldn't be injected 'properly' - osg::ref_ptr stateSet = new osg::StateSet(); - stateSet->setAttribute(new osg::BlendFunc(osg::BlendFunc::ONE, osg::BlendFunc::ONE_MINUS_SRC_ALPHA)); - batch.mStateSet = stateSet; - } mDrawable->addBatch(batch); } diff --git a/components/myguiplatform/myguitexture.cpp b/components/myguiplatform/myguitexture.cpp index ce7332cc7e..d0e4e9a86e 100644 --- a/components/myguiplatform/myguitexture.cpp +++ b/components/myguiplatform/myguitexture.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -21,9 +22,10 @@ namespace osgMyGUI { } - OSGTexture::OSGTexture(osg::Texture2D *texture) + OSGTexture::OSGTexture(osg::Texture2D *texture, osg::StateSet *injectState) : mImageManager(nullptr) , mTexture(texture) + , mInjectState(injectState) , mFormat(MyGUI::PixelFormat::Unknow) , mUsage(MyGUI::TextureUsage::Default) , mNumElemBytes(0) diff --git a/components/myguiplatform/myguitexture.hpp b/components/myguiplatform/myguitexture.hpp index a34f1b7628..e8b49eab04 100644 --- a/components/myguiplatform/myguitexture.hpp +++ b/components/myguiplatform/myguitexture.hpp @@ -15,6 +15,7 @@ namespace osg { class Image; class Texture2D; + class StateSet; } namespace Resource @@ -31,6 +32,7 @@ namespace osgMyGUI osg::ref_ptr mLockedImage; osg::ref_ptr mTexture; + osg::ref_ptr mInjectState; MyGUI::PixelFormat mFormat; MyGUI::TextureUsage mUsage; size_t mNumElemBytes; @@ -40,9 +42,11 @@ namespace osgMyGUI public: OSGTexture(const std::string &name, Resource::ImageManager* imageManager); - OSGTexture(osg::Texture2D* texture); + OSGTexture(osg::Texture2D* texture, osg::StateSet* injectState = nullptr); virtual ~OSGTexture(); + osg::StateSet* getInjectState() { return mInjectState; } + const std::string& getName() const override { return mName; } void createManual(int width, int height, MyGUI::TextureUsage usage, MyGUI::PixelFormat format) override; diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index 6ae1759395..6678d8ff74 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -1,6 +1,8 @@ #include "bulletnifloader.hpp" +#include #include +#include #include #include @@ -18,11 +20,11 @@ namespace { -osg::Matrixf getWorldTransform(const Nif::Node *node) +osg::Matrixf getWorldTransform(const Nif::Node& node) { - if(node->parent != nullptr) - return node->trafo.toMatrix() * getWorldTransform(node->parent); - return node->trafo.toMatrix(); + if(node.parent != nullptr) + return node.trafo.toMatrix() * getWorldTransform(*node.parent); + return node.trafo.toMatrix(); } bool pathFileNameStartsWithX(const std::string& path) @@ -99,12 +101,56 @@ void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriStripsData& data, co } } -void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiGeometry* geometry, const osg::Matrixf &transform = osg::Matrixf()) +template +auto handleNiGeometry(const Nif::NiGeometry& geometry, Function&& function) + -> decltype(function(static_cast(geometry.data.get()))) { - 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); + if (geometry.recType == Nif::RC_NiTriShape || geometry.recType == Nif::RC_BSLODTriShape) + { + if (geometry.data->recType != Nif::RC_NiTriShapeData) + return {}; + + auto data = static_cast(geometry.data.getPtr()); + if (data->triangles.empty()) + return {}; + + return function(static_cast(*data)); + } + + if (geometry.recType == Nif::RC_NiTriStrips) + { + if (geometry.data->recType != Nif::RC_NiTriStripsData) + return {}; + + auto data = static_cast(geometry.data.getPtr()); + if (data->strips.empty()) + return {}; + + return function(static_cast(*data)); + } + + return {}; +} + +std::monostate fillTriangleMesh(std::unique_ptr& mesh, const Nif::NiGeometry& geometry, const osg::Matrixf &transform) +{ + return handleNiGeometry(geometry, [&] (const auto& data) + { + if (mesh == nullptr) + mesh.reset(new btTriangleMesh(false)); + fillTriangleMesh(*mesh, data, transform); + return std::monostate {}; + }); +} + +std::unique_ptr makeChildMesh(const Nif::NiGeometry& geometry) +{ + return handleNiGeometry(geometry, [&] (const auto& data) + { + std::unique_ptr mesh(new btTriangleMesh); + fillTriangleMesh(*mesh, data, osg::Matrixf()); + return mesh; + }); } } @@ -141,10 +187,10 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) // Try to find a valid bounding box first. If one's found for any root node, use that. for (const Nif::Node* node : roots) { - if (findBoundingBox(node, filename)) + if (findBoundingBox(*node, filename)) { - const btVector3 extents = Misc::Convert::toBullet(mShape->mCollisionBox.extents); - const btVector3 center = Misc::Convert::toBullet(mShape->mCollisionBox.center); + const btVector3 extents = Misc::Convert::toBullet(mShape->mCollisionBox.mExtents); + const btVector3 center = Misc::Convert::toBullet(mShape->mCollisionBox.mCenter); std::unique_ptr compound (new btCompoundShape); std::unique_ptr boxShape(new btBoxShape(extents)); btTransform transform = btTransform::getIdentity(); @@ -152,7 +198,7 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) compound->addChildShape(transform, boxShape.get()); boxShape.release(); - mShape->mCollisionShape = compound.release(); + mShape->mCollisionShape.reset(compound.release()); return mShape; } } @@ -164,13 +210,13 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) // from the collision data present in every root node. for (const Nif::Node* node : roots) { - bool autogenerated = hasAutoGeneratedCollision(node); - handleNode(filename, node, 0, autogenerated, isAnimated, autogenerated); + bool autogenerated = hasAutoGeneratedCollision(*node); + handleNode(filename, *node, 0, autogenerated, isAnimated, autogenerated); } if (mCompoundShape) { - if (mStaticMesh) + if (mStaticMesh != nullptr && mStaticMesh->getNumTriangles() > 0) { btTransform trans; trans.setIdentity(); @@ -179,17 +225,17 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) child.release(); mStaticMesh.release(); } - mShape->mCollisionShape = mCompoundShape.release(); + mShape->mCollisionShape = std::move(mCompoundShape); } - else if (mStaticMesh) + else if (mStaticMesh != nullptr && mStaticMesh->getNumTriangles() > 0) { - mShape->mCollisionShape = new Resource::TriangleMeshShape(mStaticMesh.get(), true); + mShape->mCollisionShape.reset(new Resource::TriangleMeshShape(mStaticMesh.get(), true)); mStaticMesh.release(); } - if (mAvoidStaticMesh) + if (mAvoidStaticMesh != nullptr && mAvoidStaticMesh->getNumTriangles() > 0) { - mShape->mAvoidCollisionShape = new Resource::TriangleMeshShape(mAvoidStaticMesh.get(), false); + mShape->mAvoidCollisionShape.reset(new Resource::TriangleMeshShape(mAvoidStaticMesh.get(), false)); mAvoidStaticMesh.release(); } @@ -198,41 +244,40 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) // Find a boundingBox in the node hierarchy. // Return: use bounding box for collision? -bool BulletNifLoader::findBoundingBox(const Nif::Node* node, const std::string& filename) +bool BulletNifLoader::findBoundingBox(const Nif::Node& node, const std::string& filename) { - if (node->hasBounds) + if (node.hasBounds) { - unsigned int type = node->bounds.type; + unsigned int type = node.bounds.type; switch (type) { case Nif::NiBoundingVolume::Type::BOX_BV: - mShape->mCollisionBox.extents = node->bounds.box.extents; - mShape->mCollisionBox.center = node->bounds.box.center; + mShape->mCollisionBox.mExtents = node.bounds.box.extents; + mShape->mCollisionBox.mCenter = node.bounds.box.center; break; default: { std::stringstream warning; - warning << "Unsupported NiBoundingVolume type " << type << " in node " << node->recIndex; + warning << "Unsupported NiBoundingVolume type " << type << " in node " << node.recIndex; warning << " in file " << filename; warn(warning.str()); } } - if (node->flags & Nif::NiNode::Flag_BBoxCollision) + if (node.flags & Nif::NiNode::Flag_BBoxCollision) { return true; } } - const Nif::NiNode *ninode = dynamic_cast(node); - if(ninode) + if (const Nif::NiNode *ninode = dynamic_cast(&node)) { const Nif::NodeList &list = ninode->children; for(size_t i = 0;i < list.length();i++) { if(!list[i].empty()) { - if (findBoundingBox(list[i].getPtr(), filename)) + if (findBoundingBox(list[i].get(), filename)) return true; } } @@ -240,10 +285,9 @@ bool BulletNifLoader::findBoundingBox(const Nif::Node* node, const std::string& return false; } -bool BulletNifLoader::hasAutoGeneratedCollision(const Nif::Node* rootNode) +bool BulletNifLoader::hasAutoGeneratedCollision(const Nif::Node& rootNode) { - const Nif::NiNode *ninode = dynamic_cast(rootNode); - if(ninode) + if (const Nif::NiNode* ninode = dynamic_cast(&rootNode)) { const Nif::NodeList &list = ninode->children; for(size_t i = 0;i < list.length();i++) @@ -258,32 +302,32 @@ bool BulletNifLoader::hasAutoGeneratedCollision(const Nif::Node* rootNode) return true; } -void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node *node, int flags, +void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node& node, int flags, bool isCollisionNode, bool isAnimated, bool autogenerated, bool avoid) { // TODO: allow on-the fly collision switching via toggling this flag - if (node->recType == Nif::RC_NiCollisionSwitch && !(node->flags & Nif::NiNode::Flag_ActiveCollision)) + if (node.recType == Nif::RC_NiCollisionSwitch && !(node.flags & Nif::NiNode::Flag_ActiveCollision)) return; // Accumulate the flags from all the child nodes. This works for all // the flags we currently use, at least. - flags |= node->flags; + flags |= node.flags; - if (!node->controller.empty() && node->controller->recType == Nif::RC_NiKeyframeController - && (node->controller->flags & Nif::NiNode::ControllerFlag_Active)) + if (!node.controller.empty() && node.controller->recType == Nif::RC_NiKeyframeController + && (node.controller->flags & Nif::NiNode::ControllerFlag_Active)) isAnimated = true; - isCollisionNode = isCollisionNode || (node->recType == Nif::RC_RootCollisionNode); + isCollisionNode = isCollisionNode || (node.recType == Nif::RC_RootCollisionNode); // Don't collide with AvoidNode shapes - avoid = avoid || (node->recType == Nif::RC_AvoidNode); + avoid = avoid || (node.recType == Nif::RC_AvoidNode); // We encountered a RootCollisionNode inside autogenerated mesh. It is not right. - if (node->recType == Nif::RC_RootCollisionNode && autogenerated) + if (node.recType == Nif::RC_RootCollisionNode && autogenerated) Log(Debug::Info) << "RootCollisionNode is not attached to the root node in " << fileName << ". Treating it as a common NiTriShape."; // Check for extra data - for (Nif::ExtraPtr e = node->extra; !e.empty(); e = e->next) + for (Nif::ExtraPtr e = node.extra; !e.empty(); e = e->next) { if (e->recType == Nif::RC_NiStringExtraData) { @@ -310,108 +354,79 @@ 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 - || node->recType == Nif::RC_BSLODTriShape)) + 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); } } // For NiNodes, loop through children - const Nif::NiNode *ninode = dynamic_cast(node); - if(ninode) + if (const Nif::NiNode *ninode = dynamic_cast(&node)) { const Nif::NodeList &list = ninode->children; for(size_t i = 0;i < list.length();i++) { - if(!list[i].empty()) - handleNode(fileName, list[i].getPtr(), flags, isCollisionNode, isAnimated, autogenerated, avoid); + if (list[i].empty()) + continue; + + assert(list[i].get().parent == &node); + handleNode(fileName, list[i].get(), flags, isCollisionNode, isAnimated, autogenerated, avoid); } } } -void BulletNifLoader::handleNiTriShape(const Nif::Node *nifNode, int flags, const osg::Matrixf &transform, +void BulletNifLoader::handleNiTriShape(const Nif::Node& nifNode, int flags, const osg::Matrixf &transform, bool isAnimated, bool avoid) { - assert(nifNode != nullptr); - // If the object was marked "NCO" earlier, it shouldn't collide with // anything. So don't do anything. if ((flags & 0x800)) return; - auto niGeometry = static_cast(nifNode); - if (niGeometry->data.empty() || niGeometry->data->vertices.empty()) + handleNiTriShape(static_cast(nifNode), transform, isAnimated, avoid); +} + +void BulletNifLoader::handleNiTriShape(const Nif::NiGeometry& niGeometry, const osg::Matrixf &transform, + bool isAnimated, bool avoid) +{ + if (niGeometry.data.empty() || niGeometry.data->vertices.empty()) return; - if (niGeometry->recType == Nif::RC_NiTriShape || niGeometry->recType == Nif::RC_BSLODTriShape) - { - if (niGeometry->data->recType != Nif::RC_NiTriShapeData) - return; - - auto data = static_cast(niGeometry->data.getPtr()); - if (data->triangles.empty()) - return; - } - else if (niGeometry->recType == Nif::RC_NiTriStrips) - { - if (niGeometry->data->recType != Nif::RC_NiTriStripsData) - return; - - auto data = static_cast(niGeometry->data.getPtr()); - if (data->strips.empty()) - return; - } - - if (!niGeometry->skin.empty()) + if (!niGeometry.skin.empty()) isAnimated = false; if (isAnimated) { + std::unique_ptr childMesh = makeChildMesh(niGeometry); + if (childMesh == nullptr || childMesh->getNumTriangles() == 0) + return; + if (!mCompoundShape) mCompoundShape.reset(new btCompoundShape); - std::unique_ptr childMesh(new btTriangleMesh); - - fillTriangleMesh(*childMesh, niGeometry); - std::unique_ptr childShape(new Resource::TriangleMeshShape(childMesh.get(), true)); childMesh.release(); - float scale = nifNode->trafo.scale; - const Nif::Node* parent = nifNode; - while (parent->parent) - { - parent = parent->parent; + float scale = niGeometry.trafo.scale; + for (const Nif::Node* parent = niGeometry.parent; parent != nullptr; parent = parent->parent) scale *= parent->trafo.scale; - } osg::Quat q = transform.getRotate(); osg::Vec3f v = transform.getTrans(); childShape->setLocalScaling(btVector3(scale, scale, scale)); btTransform trans(btQuaternion(q.x(), q.y(), q.z(), q.w()), btVector3(v.x(), v.y(), v.z())); - mShape->mAnimatedShapes.emplace(nifNode->recIndex, mCompoundShape->getNumChildShapes()); + mShape->mAnimatedShapes.emplace(niGeometry.recIndex, mCompoundShape->getNumChildShapes()); mCompoundShape->addChildShape(trans, childShape.get()); childShape.release(); } else if (avoid) - { - if (!mAvoidStaticMesh) - mAvoidStaticMesh.reset(new btTriangleMesh(false)); - - fillTriangleMesh(*mAvoidStaticMesh, niGeometry, transform); - } + fillTriangleMesh(mAvoidStaticMesh, niGeometry, transform); else - { - if (!mStaticMesh) - mStaticMesh.reset(new btTriangleMesh(false)); - - // Static shape, just transform all vertices into position - fillTriangleMesh(*mStaticMesh, niGeometry, transform); - } + fillTriangleMesh(mStaticMesh, niGeometry, transform); } } // namespace NifBullet diff --git a/components/nifbullet/bulletnifloader.hpp b/components/nifbullet/bulletnifloader.hpp index 71c84566a0..e0fec338c6 100644 --- a/components/nifbullet/bulletnifloader.hpp +++ b/components/nifbullet/bulletnifloader.hpp @@ -27,6 +27,7 @@ namespace Nif struct Transformation; struct NiTriShape; struct NiTriStrips; + struct NiGeometry; } namespace NifBullet @@ -52,16 +53,18 @@ public: osg::ref_ptr load(const Nif::File& file); private: - bool findBoundingBox(const Nif::Node* node, const std::string& filename); + bool findBoundingBox(const Nif::Node& node, const std::string& filename); - void handleNode(const std::string& fileName, Nif::Node const *node, int flags, bool isCollisionNode, + void handleNode(const std::string& fileName, const Nif::Node& node, int flags, bool isCollisionNode, bool isAnimated=false, bool autogenerated=false, bool avoid=false); - bool hasAutoGeneratedCollision(const Nif::Node *rootNode); + bool hasAutoGeneratedCollision(const Nif::Node& rootNode); - void handleNiTriShape(const Nif::Node *nifNode, int flags, const osg::Matrixf& transform, bool isAnimated, bool avoid); + void handleNiTriShape(const Nif::Node& nifNode, int flags, const osg::Matrixf& transform, bool isAnimated, bool avoid); - std::unique_ptr mCompoundShape; + void handleNiTriShape(const Nif::NiGeometry& nifNode, const osg::Matrixf& transform, bool isAnimated, bool avoid); + + std::unique_ptr mCompoundShape; std::unique_ptr mStaticMesh; diff --git a/components/nifosg/controller.cpp b/components/nifosg/controller.cpp index ddded82156..956fe2e489 100644 --- a/components/nifosg/controller.cpp +++ b/components/nifosg/controller.cpp @@ -57,7 +57,7 @@ float ControllerFunction::calculate(float value) const } case Constant: default: - return std::min(mStopTime, std::max(mStartTime, time)); + return std::clamp(time, mStartTime, mStopTime); } } diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp index c6311fd5fc..5d88dda1f1 100644 --- a/components/nifosg/controller.hpp +++ b/components/nifosg/controller.hpp @@ -248,6 +248,7 @@ namespace NifOsg META_Object(NifOsg, KeyframeController) osg::Vec3f getTranslation(float time) const override; + osg::Callback* getAsCallback() override { return this; } void operator() (NifOsg::MatrixTransform*, osg::NodeVisitor*); diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp index 798a6778e6..52d639d272 100644 --- a/components/resource/bulletshape.cpp +++ b/components/resource/bulletshape.cpp @@ -10,86 +10,71 @@ namespace Resource { - -BulletShape::BulletShape() - : mCollisionShape(nullptr) - , mAvoidCollisionShape(nullptr) +namespace { - -} - -BulletShape::BulletShape(const BulletShape ©, const osg::CopyOp ©op) - : mCollisionShape(duplicateCollisionShape(copy.mCollisionShape)) - , mAvoidCollisionShape(duplicateCollisionShape(copy.mAvoidCollisionShape)) - , mCollisionBox(copy.mCollisionBox) - , mAnimatedShapes(copy.mAnimatedShapes) -{ -} - -BulletShape::~BulletShape() -{ - deleteShape(mAvoidCollisionShape); - deleteShape(mCollisionShape); -} - -void BulletShape::deleteShape(btCollisionShape* shape) -{ - if(shape!=nullptr) + CollisionShapePtr duplicateCollisionShape(const btCollisionShape *shape) { - if(shape->isCompound()) + if (shape == nullptr) + return nullptr; + + if (shape->isCompound()) { - btCompoundShape* ms = static_cast(shape); - int a = ms->getNumChildShapes(); - for(int i=0; i getChildShape(i)); + const btCompoundShape *comp = static_cast(shape); + std::unique_ptr newShape(new btCompoundShape); + + for (int i = 0, n = comp->getNumChildShapes(); i < n; ++i) + { + auto child = duplicateCollisionShape(comp->getChildShape(i)); + const btTransform& trans = comp->getChildTransform(i); + newShape->addChildShape(trans, child.release()); + } + + return newShape; } + + if (shape->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE) + { + const btBvhTriangleMeshShape* trishape = static_cast(shape); + return CollisionShapePtr(new btScaledBvhTriangleMeshShape(const_cast(trishape), btVector3(1.f, 1.f, 1.f))); + } + + if (shape->getShapeType() == BOX_SHAPE_PROXYTYPE) + { + const btBoxShape* boxshape = static_cast(shape); + return CollisionShapePtr(new btBoxShape(*boxshape)); + } + + if (shape->getShapeType() == TERRAIN_SHAPE_PROXYTYPE) + return CollisionShapePtr(new btHeightfieldTerrainShape(static_cast(*shape))); + + throw std::logic_error(std::string("Unhandled Bullet shape duplication: ") + shape->getName()); + } + + void deleteShape(btCollisionShape* shape) + { + if (shape->isCompound()) + { + btCompoundShape* compound = static_cast(shape); + for (int i = 0, n = compound->getNumChildShapes(); i < n; i++) + if (btCollisionShape* child = compound->getChildShape(i)) + deleteShape(child); + } + delete shape; } } -btCollisionShape* BulletShape::duplicateCollisionShape(const btCollisionShape *shape) const +void DeleteCollisionShape::operator()(btCollisionShape* shape) const { - if(shape->isCompound()) - { - const btCompoundShape *comp = static_cast(shape); - btCompoundShape *newShape = new btCompoundShape; - - int numShapes = comp->getNumChildShapes(); - for(int i = 0;i < numShapes;++i) - { - btCollisionShape *child = duplicateCollisionShape(comp->getChildShape(i)); - const btTransform& trans = comp->getChildTransform(i); - newShape->addChildShape(trans, child); - } - - return newShape; - } - - if(const btBvhTriangleMeshShape* trishape = dynamic_cast(shape)) - { - btScaledBvhTriangleMeshShape* newShape = new btScaledBvhTriangleMeshShape(const_cast(trishape), btVector3(1.f, 1.f, 1.f)); - return newShape; - } - - if (const btBoxShape* boxshape = dynamic_cast(shape)) - { - return new btBoxShape(*boxshape); - } - - if (shape->getShapeType() == TERRAIN_SHAPE_PROXYTYPE) - return new btHeightfieldTerrainShape(static_cast(*shape)); - - throw std::logic_error(std::string("Unhandled Bullet shape duplication: ")+shape->getName()); + deleteShape(shape); } -btCollisionShape *BulletShape::getCollisionShape() const +BulletShape::BulletShape(const BulletShape ©, const osg::CopyOp ©op) + : mCollisionShape(duplicateCollisionShape(copy.mCollisionShape.get())) + , mAvoidCollisionShape(duplicateCollisionShape(copy.mAvoidCollisionShape.get())) + , mCollisionBox(copy.mCollisionBox) + , mAnimatedShapes(copy.mAnimatedShapes) { - return mCollisionShape; -} - -btCollisionShape *BulletShape::getAvoidCollisionShape() const -{ - return mAvoidCollisionShape; } void BulletShape::setLocalScaling(const btVector3& scale) @@ -99,30 +84,18 @@ void BulletShape::setLocalScaling(const btVector3& scale) mAvoidCollisionShape->setLocalScaling(scale); } -bool BulletShape::isAnimated() const +osg::ref_ptr makeInstance(osg::ref_ptr source) { - return !mAnimatedShapes.empty(); -} - -osg::ref_ptr BulletShape::makeInstance() const -{ - osg::ref_ptr instance (new BulletShapeInstance(this)); - return instance; + return {new BulletShapeInstance(std::move(source))}; } BulletShapeInstance::BulletShapeInstance(osg::ref_ptr source) - : BulletShape() - , mSource(source) + : mSource(std::move(source)) { - mCollisionBox = source->mCollisionBox; - - mAnimatedShapes = source->mAnimatedShapes; - - if (source->mCollisionShape) - mCollisionShape = duplicateCollisionShape(source->mCollisionShape); - - if (source->mAvoidCollisionShape) - mAvoidCollisionShape = duplicateCollisionShape(source->mAvoidCollisionShape); + mCollisionBox = mSource->mCollisionBox; + mAnimatedShapes = mSource->mAnimatedShapes; + mCollisionShape = duplicateCollisionShape(mSource->mCollisionShape.get()); + mAvoidCollisionShape = duplicateCollisionShape(mSource->mAvoidCollisionShape.get()); } } diff --git a/components/resource/bulletshape.hpp b/components/resource/bulletshape.hpp index 6ac8064cb3..7188165045 100644 --- a/components/resource/bulletshape.hpp +++ b/components/resource/bulletshape.hpp @@ -2,6 +2,7 @@ #define OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H #include +#include #include #include @@ -13,24 +14,28 @@ class btCollisionShape; namespace Resource { + struct DeleteCollisionShape + { + void operator()(btCollisionShape* shape) const; + }; + + using CollisionShapePtr = std::unique_ptr; - class BulletShapeInstance; class BulletShape : public osg::Object { public: - BulletShape(); + BulletShape() = default; BulletShape(const BulletShape& copy, const osg::CopyOp& copyop); - virtual ~BulletShape(); META_Object(Resource, BulletShape) - btCollisionShape* mCollisionShape; - btCollisionShape* mAvoidCollisionShape; + CollisionShapePtr mCollisionShape; + CollisionShapePtr mAvoidCollisionShape; struct CollisionBox { - osg::Vec3f extents; - osg::Vec3f center; + osg::Vec3f mExtents; + osg::Vec3f mCenter; }; // Used for actors and projectiles. mCollisionShape is used for actors only when we need to autogenerate collision box for creatures. // For now, use one file <-> one resource for simplicity. @@ -42,21 +47,9 @@ namespace Resource // we store the node's record index mapped to the child index of the shape in the btCompoundShape. std::map mAnimatedShapes; - osg::ref_ptr makeInstance() const; - - btCollisionShape* duplicateCollisionShape(const btCollisionShape* shape) const; - - btCollisionShape* getCollisionShape() const; - - btCollisionShape* getAvoidCollisionShape() const; - void setLocalScaling(const btVector3& scale); - bool isAnimated() const; - - private: - - void deleteShape(btCollisionShape* shape); + bool isAnimated() const { return !mAnimatedShapes.empty(); } }; @@ -71,6 +64,8 @@ namespace Resource osg::ref_ptr mSource; }; + osg::ref_ptr makeInstance(osg::ref_ptr source); + // Subclass btBhvTriangleMeshShape to auto-delete the meshInterface struct TriangleMeshShape : public btBvhTriangleMeshShape { diff --git a/components/resource/bulletshapemanager.cpp b/components/resource/bulletshapemanager.cpp index 5b6dce067c..39ceb4fe7e 100644 --- a/components/resource/bulletshapemanager.cpp +++ b/components/resource/bulletshapemanager.cpp @@ -83,16 +83,17 @@ public: return osg::ref_ptr(); osg::ref_ptr shape (new BulletShape); - btBvhTriangleMeshShape* triangleMeshShape = new TriangleMeshShape(mTriangleMesh.release(), true); + + auto triangleMeshShape = std::make_unique(mTriangleMesh.release(), true); btVector3 aabbMin = triangleMeshShape->getLocalAabbMin(); btVector3 aabbMax = triangleMeshShape->getLocalAabbMax(); - shape->mCollisionBox.extents[0] = (aabbMax[0] - aabbMin[0]) / 2.0f; - shape->mCollisionBox.extents[1] = (aabbMax[1] - aabbMin[1]) / 2.0f; - shape->mCollisionBox.extents[2] = (aabbMax[2] - aabbMin[2]) / 2.0f; - shape->mCollisionBox.center = osg::Vec3f( (aabbMax[0] + aabbMin[0]) / 2.0f, + shape->mCollisionBox.mExtents[0] = (aabbMax[0] - aabbMin[0]) / 2.0f; + shape->mCollisionBox.mExtents[1] = (aabbMax[1] - aabbMin[1]) / 2.0f; + shape->mCollisionBox.mExtents[2] = (aabbMax[2] - aabbMin[2]) / 2.0f; + shape->mCollisionBox.mCenter = osg::Vec3f( (aabbMax[0] + aabbMin[0]) / 2.0f, (aabbMax[1] + aabbMin[1]) / 2.0f, (aabbMax[2] + aabbMin[2]) / 2.0f ); - shape->mCollisionShape = triangleMeshShape; + shape->mCollisionShape.reset(triangleMeshShape.release()); return shape; } @@ -193,9 +194,8 @@ osg::ref_ptr BulletShapeManager::createInstance(const std:: { osg::ref_ptr shape = getShape(name); if (shape) - return shape->makeInstance(); - else - return osg::ref_ptr(); + return makeInstance(std::move(shape)); + return osg::ref_ptr(); } void BulletShapeManager::updateCache(double referenceTime) diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index c5ef957c3e..c1d567c0e1 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -20,6 +20,7 @@ #include #include +#include #include @@ -551,7 +552,7 @@ namespace Resource std::sort(reservedNames.begin(), reservedNames.end(), Misc::StringUtils::ciLess); } - std::vector::iterator it = Misc::StringUtils::partialBinarySearch(reservedNames.begin(), reservedNames.end(), name); + std::vector::iterator it = Misc::partialBinarySearch(reservedNames.begin(), reservedNames.end(), name); return it != reservedNames.end(); } @@ -693,19 +694,33 @@ namespace Resource } } - osg::ref_ptr SceneManager::createInstance(const std::string& name) + osg::ref_ptr SceneManager::getInstance(const std::string& name) { osg::ref_ptr scene = getTemplate(name); - return createInstance(scene); + return getInstance(scene); } - osg::ref_ptr SceneManager::createInstance(const osg::Node *base) + osg::ref_ptr SceneManager::cloneNode(const osg::Node* base) { - osg::ref_ptr cloned = static_cast(base->clone(SceneUtil::CopyOp())); - - // add a ref to the original template, to hint to the cache that it's still being used and should be kept in cache + SceneUtil::CopyOp copyop; + if (const osg::Drawable* drawable = base->asDrawable()) + { + if (drawable->asGeometry()) + { + Log(Debug::Warning) << "SceneManager::cloneNode: attempting to clone osg::Geometry. For safety reasons this will be expensive. Consider avoiding this call."; + copyop.setCopyFlags(copyop.getCopyFlags()|osg::CopyOp::DEEP_COPY_ARRAYS|osg::CopyOp::DEEP_COPY_PRIMITIVES); + } + } + osg::ref_ptr cloned = static_cast(base->clone(copyop)); + // add a ref to the original template to help verify the safety of shallow cloning operations + // in addition, if this node is managed by a cache, we hint to the cache that it's still being used and should be kept in cache cloned->getOrCreateUserDataContainer()->addUserObject(new TemplateRef(base)); + return cloned; + } + osg::ref_ptr SceneManager::getInstance(const osg::Node *base) + { + osg::ref_ptr cloned = cloneNode(base); // we can skip any scene graphs without update callbacks since we know that particle emitters will have an update callback set if (cloned->getNumChildrenRequiringUpdateTraversal() > 0) { @@ -716,11 +731,6 @@ namespace Resource return cloned; } - osg::ref_ptr SceneManager::getInstance(const std::string &name) - { - return createInstance(name); - } - osg::ref_ptr SceneManager::getInstance(const std::string &name, osg::Group* parentNode) { osg::ref_ptr cloned = getInstance(name); diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index b5d3e453a0..85e012071d 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -132,16 +132,22 @@ namespace Resource /// @note Thread safe. osg::ref_ptr getTemplate(const std::string& name, bool compile=true); - osg::ref_ptr createInstance(const std::string& name); + /// Clone osg::Node safely. + /// @note Thread safe. + static osg::ref_ptr cloneNode(const osg::Node* base); - osg::ref_ptr createInstance(const osg::Node* base); void shareState(osg::ref_ptr node); - /// Get an instance of the given scene template + + /// Clone osg::Node and adjust it according to SceneManager's settings. + /// @note Thread safe. + osg::ref_ptr getInstance(const osg::Node* base); + + /// Instance the given scene template. /// @see getTemplate /// @note Thread safe. osg::ref_ptr getInstance(const std::string& name); - /// Get an instance of the given scene template and immediately attach it to a parent node + /// Instance the given scene template and immediately attach it to a parent node /// @see getTemplate /// @note Not thread safe, unless parentNode is not part of the main scene graph yet. osg::ref_ptr getInstance(const std::string& name, osg::Group* parentNode); diff --git a/components/sceneutil/attach.cpp b/components/sceneutil/attach.cpp index 6690148c74..02c3456425 100644 --- a/components/sceneutil/attach.cpp +++ b/components/sceneutil/attach.cpp @@ -13,9 +13,9 @@ #include #include +#include #include "visitor.hpp" -#include "clone.hpp" namespace SceneUtil { @@ -49,10 +49,10 @@ namespace SceneUtil if (!filterMatches(drawable.getName())) return; - osg::Node* node = &drawable; + const osg::Node* node = &drawable; for (auto it = getNodePath().rbegin()+1; it != getNodePath().rend(); ++it) { - osg::Node* parent = *it; + const osg::Node* parent = *it; if (!filterMatches(parent->getName())) break; node = parent; @@ -60,11 +60,11 @@ namespace SceneUtil mToCopy.emplace(node); } - void doCopy() + void doCopy(Resource::SceneManager* sceneManager) { - for (const osg::ref_ptr& node : mToCopy) + for (const osg::ref_ptr& node : mToCopy) { - mParent->addChild(static_cast(node->clone(SceneUtil::CopyOp()))); + mParent->addChild(sceneManager->getInstance(node)); } mToCopy.clear(); } @@ -78,7 +78,7 @@ namespace SceneUtil || (lowerName.size() >= mFilter2.size() && lowerName.compare(0, mFilter2.size(), mFilter2) == 0); } - using NodeSet = std::set>; + using NodeSet = std::set>; NodeSet mToCopy; osg::ref_ptr mParent; @@ -100,7 +100,7 @@ namespace SceneUtil } } - osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node *master, const std::string &filter, osg::Group* attachNode) + osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node *master, const std::string &filter, osg::Group* attachNode, Resource::SceneManager* sceneManager, const osg::Quat* attitude) { if (dynamic_cast(toAttach.get())) { @@ -108,7 +108,9 @@ namespace SceneUtil CopyRigVisitor copyVisitor(handle, filter); const_cast(toAttach.get())->accept(copyVisitor); - copyVisitor.doCopy(); + copyVisitor.doCopy(sceneManager); + // add a ref to the original template to hint to the cache that it is still being used and should be kept in cache. + handle->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(toAttach)); if (handle->getNumChildren() == 1) { @@ -127,7 +129,7 @@ namespace SceneUtil } else { - osg::ref_ptr clonedToAttach = static_cast(toAttach->clone(SceneUtil::CopyOp())); + osg::ref_ptr clonedToAttach = sceneManager->getInstance(toAttach); FindByNameVisitor findBoneOffset("BoneOffset"); clonedToAttach->accept(findBoneOffset); @@ -142,8 +144,6 @@ namespace SceneUtil trans = new osg::PositionAttitudeTransform; trans->setPosition(boneOffset->getMatrix().getTrans()); - // The BoneOffset rotation seems to be incorrect - trans->setAttitude(osg::Quat(osg::DegreesToRadians(-90.f), osg::Vec3f(1,0,0))); // Now that we used it, get rid of the redundant node. if (boneOffset->getNumChildren() == 0 && boneOffset->getNumParents() == 1) @@ -170,6 +170,13 @@ namespace SceneUtil trans->setStateSet(frontFaceStateSet); } + if(attitude) + { + if (!trans) + trans = new osg::PositionAttitudeTransform; + trans->setAttitude(*attitude); + } + if (trans) { attachNode->addChild(trans); diff --git a/components/sceneutil/attach.hpp b/components/sceneutil/attach.hpp index 806fc53488..ed0299dece 100644 --- a/components/sceneutil/attach.hpp +++ b/components/sceneutil/attach.hpp @@ -9,6 +9,11 @@ namespace osg { class Node; class Group; + class Quat; +} +namespace Resource +{ + class SceneManager; } namespace SceneUtil @@ -19,7 +24,7 @@ namespace SceneUtil /// Otherwise, just attach all of the toAttach scenegraph to the attachment node on the master scenegraph, with no filtering. /// @note The master scene graph is expected to include a skeleton. /// @return A newly created node that is directly attached to the master scene graph - osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node* master, const std::string& filter, osg::Group* attachNode); + osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node* master, const std::string& filter, osg::Group* attachNode, Resource::SceneManager *sceneManager, const osg::Quat* attitude = nullptr); } diff --git a/components/sceneutil/clone.hpp b/components/sceneutil/clone.hpp index 1cf00c9e58..35240cbfba 100644 --- a/components/sceneutil/clone.hpp +++ b/components/sceneutil/clone.hpp @@ -18,6 +18,7 @@ namespace SceneUtil /// @par Defines the cloning behaviour we need: /// * Assigns updated ParticleSystem pointers on cloned emitters and programs. /// * Deep copies RigGeometry and MorphGeometry so they can animate without affecting clones. + /// @warning Avoid using this class directly. The safety of cloning operations depends on the copy flags and the objects involved. Consider using SceneManager::cloneNode for additional safety. /// @warning Do not use an object of this class for more than one copy operation. class CopyOp : public osg::CopyOp { diff --git a/components/sceneutil/keyframe.hpp b/components/sceneutil/keyframe.hpp index 5be6924a09..59a87ab08e 100644 --- a/components/sceneutil/keyframe.hpp +++ b/components/sceneutil/keyframe.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include #include @@ -11,18 +11,20 @@ namespace SceneUtil { - class KeyframeController : public SceneUtil::Controller, public virtual osg::Callback + /// @note Derived classes are expected to derive from osg::Callback and implement getAsCallback(). + class KeyframeController : public SceneUtil::Controller, public virtual osg::Object { public: KeyframeController() {} KeyframeController(const KeyframeController& copy, const osg::CopyOp& copyop) - : osg::Callback(copy, copyop) - , SceneUtil::Controller(copy) - {} - META_Object(SceneUtil, KeyframeController) + : osg::Object(copy, copyop) + , SceneUtil::Controller(copy) {} virtual osg::Vec3f getTranslation(float time) const { return osg::Vec3f(); } + + /// @note We could drop this function in favour of osg::Object::asCallback from OSG 3.6 on. + virtual osg::Callback* getAsCallback() = 0; }; /// Wrapper object containing an animation track as a ref-countable osg::Object. diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp index f38fd80d26..448f6ca916 100644 --- a/components/sceneutil/lightmanager.cpp +++ b/components/sceneutil/lightmanager.cpp @@ -1,6 +1,9 @@ #include "lightmanager.hpp" #include +#include +#include +#include #include #include @@ -1158,29 +1161,39 @@ namespace SceneUtil return mSun; } + size_t LightManager::HashLightIdList::operator()(const LightIdList& lightIdList) const + { + size_t hash = 0; + for (size_t i = 0; i < lightIdList.size(); ++i) + Misc::hashCombine(hash, lightIdList[i]); + return hash; + } + osg::ref_ptr LightManager::getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix) { // possible optimization: return a StateSet containing all requested lights plus some extra lights (if a suitable one exists) - size_t hash = 0; - for (size_t i = 0; i < lightList.size(); ++i) + + if (getLightingMethod() == LightingMethod::SingleUBO) { - auto id = lightList[i]->mLightSource->getId(); - Misc::hashCombine(hash, id); + for (size_t i = 0; i < lightList.size(); ++i) + { + auto id = lightList[i]->mLightSource->getId(); + if (getLightIndexMap(frameNum).find(id) != getLightIndexMap(frameNum).end()) + continue; - if (getLightingMethod() != LightingMethod::SingleUBO) - continue; - - if (getLightIndexMap(frameNum).find(id) != getLightIndexMap(frameNum).end()) - continue; - - int index = getLightIndexMap(frameNum).size() + 1; - updateGPUPointLight(index, lightList[i]->mLightSource, frameNum, viewMatrix); - getLightIndexMap(frameNum).emplace(lightList[i]->mLightSource->getId(), index); + int index = getLightIndexMap(frameNum).size() + 1; + updateGPUPointLight(index, lightList[i]->mLightSource, frameNum, viewMatrix); + getLightIndexMap(frameNum).emplace(id, index); + } } auto& stateSetCache = mStateSetCache[frameNum%2]; - auto found = stateSetCache.find(hash); + LightIdList lightIdList; + lightIdList.reserve(lightList.size()); + std::transform(lightList.begin(), lightList.end(), std::back_inserter(lightIdList), [] (const LightSourceViewBound* l) { return l->mLightSource->getId(); }); + + auto found = stateSetCache.find(lightIdList); if (found != stateSetCache.end()) { mStateSetGenerator->update(found->second, lightList, frameNum); @@ -1188,7 +1201,7 @@ namespace SceneUtil } auto stateset = mStateSetGenerator->generate(lightList, frameNum); - stateSetCache.emplace(hash, stateset); + stateSetCache.emplace(lightIdList, stateset); return stateset; } diff --git a/components/sceneutil/lightmanager.hpp b/components/sceneutil/lightmanager.hpp index b518a4723c..4a7dc7dbe7 100644 --- a/components/sceneutil/lightmanager.hpp +++ b/components/sceneutil/lightmanager.hpp @@ -207,8 +207,12 @@ namespace SceneUtil using LightSourceViewBoundCollection = std::vector; std::map, LightSourceViewBoundCollection> mLightsInViewSpace; - // < Light list hash , StateSet > - using LightStateSetMap = std::map>; + using LightIdList = std::vector; + struct HashLightIdList + { + size_t operator()(const LightIdList&) const; + }; + using LightStateSetMap = std::unordered_map, HashLightIdList>; LightStateSetMap mStateSetCache[2]; std::vector> mDummies; diff --git a/components/sceneutil/lightutil.cpp b/components/sceneutil/lightutil.cpp index 6a1a1376ec..2a5a945558 100644 --- a/components/sceneutil/lightutil.cpp +++ b/components/sceneutil/lightutil.cpp @@ -79,6 +79,7 @@ namespace SceneUtil // PositionAttitudeTransform seems to be slightly faster than MatrixTransform osg::ref_ptr trans(new SceneUtil::PositionAttitudeTransform); trans->setPosition(computeBound.getBoundingBox().center()); + trans->setNodeMask(lightMask); node->addChild(trans); diff --git a/components/sceneutil/morphgeometry.cpp b/components/sceneutil/morphgeometry.cpp index 78be559989..59adbffffe 100644 --- a/components/sceneutil/morphgeometry.cpp +++ b/components/sceneutil/morphgeometry.cpp @@ -1,6 +1,7 @@ #include "morphgeometry.hpp" #include +#include #include @@ -27,11 +28,19 @@ MorphGeometry::MorphGeometry(const MorphGeometry ©, const osg::CopyOp ©o void MorphGeometry::setSourceGeometry(osg::ref_ptr sourceGeom) { + for (unsigned int i=0; i<2; ++i) + mGeometry[i] = nullptr; + mSourceGeometry = sourceGeom; for (unsigned int i=0; i<2; ++i) { + // DO NOT COPY AND PASTE THIS CODE. Cloning osg::Geometry without also cloning its contained Arrays is generally unsafe. + // In this specific case the operation is safe under the following two assumptions: + // - When Arrays are removed or replaced in the cloned geometry, the original Arrays in their place must outlive the cloned geometry regardless. (ensured by TemplateRef) + // - Arrays that we add or replace in the cloned geometry must be explicitely forbidden from reusing BufferObjects of the original geometry. (ensured by vbo below) mGeometry[i] = new osg::Geometry(*mSourceGeometry, osg::CopyOp::SHALLOW_COPY); + mGeometry[i]->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(mSourceGeometry)); const osg::Geometry& from = *mSourceGeometry; osg::Geometry& to = *mGeometry[i]; diff --git a/components/sceneutil/nodecallback.hpp b/components/sceneutil/nodecallback.hpp index 6f0140d64c..96e3ae229e 100644 --- a/components/sceneutil/nodecallback.hpp +++ b/components/sceneutil/nodecallback.hpp @@ -13,13 +13,12 @@ namespace SceneUtil { template -class NodeCallback : public virtual osg::Callback +class NodeCallback : public osg::Callback { public: NodeCallback(){} NodeCallback(const NodeCallback& nc,const osg::CopyOp& copyop): osg::Callback(nc, copyop) {} - META_Object(SceneUtil, NodeCallback) bool run(osg::Object* object, osg::Object* data) override { diff --git a/components/sceneutil/osgacontroller.hpp b/components/sceneutil/osgacontroller.hpp index 26212a3b99..893b8b1ebe 100644 --- a/components/sceneutil/osgacontroller.hpp +++ b/components/sceneutil/osgacontroller.hpp @@ -45,6 +45,8 @@ namespace SceneUtil META_Object(SceneUtil, OsgAnimationController) + osg::Callback* getAsCallback() override { return this; } + /// @brief Handles the location of the instance osg::Vec3f getTranslation(float time) const override; diff --git a/components/sceneutil/riggeometry.cpp b/components/sceneutil/riggeometry.cpp index ec00efa535..84b31f4afc 100644 --- a/components/sceneutil/riggeometry.cpp +++ b/components/sceneutil/riggeometry.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "skeleton.hpp" @@ -60,12 +61,22 @@ RigGeometry::RigGeometry(const RigGeometry ©, const osg::CopyOp ©op) void RigGeometry::setSourceGeometry(osg::ref_ptr sourceGeometry) { + for (unsigned int i=0; i<2; ++i) + mGeometry[i] = nullptr; + mSourceGeometry = sourceGeometry; for (unsigned int i=0; i<2; ++i) { const osg::Geometry& from = *sourceGeometry; + + // DO NOT COPY AND PASTE THIS CODE. Cloning osg::Geometry without also cloning its contained Arrays is generally unsafe. + // In this specific case the operation is safe under the following two assumptions: + // - When Arrays are removed or replaced in the cloned geometry, the original Arrays in their place must outlive the cloned geometry regardless. (ensured by mSourceGeometry) + // - Arrays that we add or replace in the cloned geometry must be explicitely forbidden from reusing BufferObjects of the original geometry. (ensured by vbo below) mGeometry[i] = new osg::Geometry(from, osg::CopyOp::SHALLOW_COPY); + mGeometry[i]->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(mSourceGeometry)); + osg::Geometry& to = *mGeometry[i]; to.setSupportsDisplayList(false); to.setUseVertexBufferObjects(true); diff --git a/components/sceneutil/riggeometry.hpp b/components/sceneutil/riggeometry.hpp index e01583399e..25ae5a3243 100644 --- a/components/sceneutil/riggeometry.hpp +++ b/components/sceneutil/riggeometry.hpp @@ -9,6 +9,13 @@ namespace SceneUtil class Skeleton; class Bone; + // TODO: This class has a lot of issues. + // - We require too many workarounds to ensure safety. + // - mSourceGeometry should be const, but can not be const because of a use case in shadervisitor.cpp. + // - We create useless mGeometry clones in template RigGeometries. + // - We do not support compileGLObjects. + // - We duplicate some code in MorphGeometry. + /// @brief Mesh skinning implementation. /// @note A RigGeometry may be attached directly to a Skeleton, or somewhere below a Skeleton. /// Note though that the RigGeometry ignores any transforms below the Skeleton, so the attachment point is not that important. diff --git a/components/sceneutil/serialize.cpp b/components/sceneutil/serialize.cpp index 703b63af7d..9da0d6a40e 100644 --- a/components/sceneutil/serialize.cpp +++ b/components/sceneutil/serialize.cpp @@ -128,10 +128,10 @@ void registerSerializers() "SceneUtil::UpdateRigBounds", "SceneUtil::UpdateRigGeometry", "SceneUtil::LightSource", - "SceneUtil::StateSetUpdater", "SceneUtil::DisableLight", "SceneUtil::MWShadowTechnique", "SceneUtil::TextKeyMapHolder", + "Shader::AddedState", "Shader::RemovedAlphaFunc", "NifOsg::LightManagerStateAttribute", "NifOsg::FlipController", diff --git a/components/sceneutil/shadow.cpp b/components/sceneutil/shadow.cpp index 8c3758e980..dfe4cf1507 100644 --- a/components/sceneutil/shadow.cpp +++ b/components/sceneutil/shadow.cpp @@ -27,8 +27,7 @@ namespace SceneUtil mShadowSettings->setLightNum(0); mShadowSettings->setReceivesShadowTraversalMask(~0u); - int numberOfShadowMapsPerLight = Settings::Manager::getInt("number of shadow maps", "Shadows"); - numberOfShadowMapsPerLight = std::max(1, std::min(numberOfShadowMapsPerLight, 8)); + const int numberOfShadowMapsPerLight = std::clamp(Settings::Manager::getInt("number of shadow maps", "Shadows"), 1, 8); mShadowSettings->setNumShadowMapsPerLight(numberOfShadowMapsPerLight); mShadowSettings->setBaseShadowTextureUnit(8 - numberOfShadowMapsPerLight); @@ -36,7 +35,7 @@ namespace SceneUtil const float maximumShadowMapDistance = Settings::Manager::getFloat("maximum shadow map distance", "Shadows"); if (maximumShadowMapDistance > 0) { - const float shadowFadeStart = std::min(std::max(0.f, Settings::Manager::getFloat("shadow fade start", "Shadows")), 1.f); + const float shadowFadeStart = std::clamp(Settings::Manager::getFloat("shadow fade start", "Shadows"), 0.f, 1.f); mShadowSettings->setMaximumShadowMapDistance(maximumShadowMapDistance); mShadowTechnique->setShadowFadeStart(maximumShadowMapDistance * shadowFadeStart); } @@ -78,8 +77,7 @@ namespace SceneUtil if (!Settings::Manager::getBool("enable shadows", "Shadows")) return; - int numberOfShadowMapsPerLight = Settings::Manager::getInt("number of shadow maps", "Shadows"); - numberOfShadowMapsPerLight = std::max(1, std::min(numberOfShadowMapsPerLight, 8)); + const int numberOfShadowMapsPerLight = std::clamp(Settings::Manager::getInt("number of shadow maps", "Shadows"), 1, 8); int baseShadowTextureUnit = 8 - numberOfShadowMapsPerLight; diff --git a/components/sceneutil/statesetupdater.hpp b/components/sceneutil/statesetupdater.hpp index 35be9cb434..cc2e248457 100644 --- a/components/sceneutil/statesetupdater.hpp +++ b/components/sceneutil/statesetupdater.hpp @@ -34,8 +34,6 @@ namespace SceneUtil StateSetUpdater(); StateSetUpdater(const StateSetUpdater& copy, const osg::CopyOp& copyop); - META_Object(SceneUtil, StateSetUpdater) - void operator()(osg::Node* node, osg::NodeVisitor* nv); /// Apply state - to override in derived classes diff --git a/components/sceneutil/visitor.cpp b/components/sceneutil/visitor.cpp index fde87c66ba..ea09445678 100644 --- a/components/sceneutil/visitor.cpp +++ b/components/sceneutil/visitor.cpp @@ -51,18 +51,17 @@ namespace SceneUtil void NodeMapVisitor::apply(osg::MatrixTransform& trans) { - // Take transformation for first found node in file - std::string originalNodeName = Misc::StringUtils::lowerCase(trans.getName()); + // Choose first found node in file if (trans.libraryName() == std::string("osgAnimation")) { + std::string nodeName = trans.getName(); // Convert underscores to whitespaces as a workaround for Collada (OpenMW's animation system uses whitespace-separated names) - std::replace(originalNodeName.begin(), originalNodeName.end(), '_', ' '); + std::replace(nodeName.begin(), nodeName.end(), '_', ' '); + mMap.emplace(nodeName, &trans); } - - const std::string nodeName = originalNodeName; - - mMap.emplace(nodeName, &trans); + else + mMap.emplace(trans.getName(), &trans); traverse(trans); } diff --git a/components/sceneutil/visitor.hpp b/components/sceneutil/visitor.hpp index fcf3c1f944..45aa408b9e 100644 --- a/components/sceneutil/visitor.hpp +++ b/components/sceneutil/visitor.hpp @@ -4,6 +4,10 @@ #include #include +#include + +#include + // Commonly used scene graph visitors namespace SceneUtil { @@ -49,7 +53,7 @@ namespace SceneUtil class NodeMapVisitor : public osg::NodeVisitor { public: - typedef std::map > NodeMap; + typedef std::unordered_map, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> NodeMap; NodeMapVisitor(NodeMap& map) : osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN) diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index 6709ee842e..9877ab1863 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -516,6 +516,14 @@ namespace Shader // We could fall back to a texture size uniform if EXT_gpu_shader4 is missing } + bool simpleLighting = false; + node.getUserValue("simpleLighting", simpleLighting); + if (simpleLighting) + { + defineMap["forcePPL"] = "1"; + defineMap["endLight"] = "0"; + } + if (writableStateSet->getMode(GL_ALPHA_TEST) != osg::StateAttribute::INHERIT && !previousAddedState->hasMode(GL_ALPHA_TEST)) removedState->setMode(GL_ALPHA_TEST, writableStateSet->getMode(GL_ALPHA_TEST)); // This disables the deprecated fixed-function alpha test diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp index 0282eb8de1..7fc0895846 100644 --- a/components/terrain/quadtreeworld.cpp +++ b/components/terrain/quadtreeworld.cpp @@ -278,6 +278,7 @@ QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resour , mViewDistance(std::numeric_limits::max()) , mMinSize(1/8.f) , mDebugTerrainChunks(debugChunks) + , mRevalidateDistance(0.f) { mChunkManager->setCompositeMapSize(compMapResolution); mChunkManager->setCompositeMapLevel(compMapLevel); @@ -346,22 +347,25 @@ unsigned int getLodFlags(QuadTreeNode* node, int ourLod, int vertexLodMod, const return lodFlags; } -void loadRenderingNode(ViewData::Entry& entry, ViewData* vd, int vertexLodMod, float cellWorldSize, const osg::Vec4i &gridbounds, const std::vector& chunkManagers, bool compile, float reuseDistance) +void QuadTreeWorld::loadRenderingNode(ViewDataEntry& entry, ViewData* vd, float cellWorldSize, const osg::Vec4i &gridbounds, bool compile, float reuseDistance) { if (!vd->hasChanged() && entry.mRenderingNode) return; - int ourLod = getVertexLod(entry.mNode, vertexLodMod); + int ourLod = getVertexLod(entry.mNode, mVertexLodMod); if (vd->hasChanged()) { // have to recompute the lodFlags in case a neighbour has changed LOD. - unsigned int lodFlags = getLodFlags(entry.mNode, ourLod, vertexLodMod, vd); + unsigned int lodFlags = getLodFlags(entry.mNode, ourLod, mVertexLodMod, vd); if (lodFlags != entry.mLodFlags) { entry.mRenderingNode = nullptr; entry.mLodFlags = lodFlags; } + // have to revalidate chunks within a custom view distance. + if (mRevalidateDistance && entry.mNode->distance(vd->getViewPoint()) <= mRevalidateDistance + reuseDistance) + entry.mRenderingNode = nullptr; } if (!entry.mRenderingNode) @@ -372,9 +376,9 @@ void loadRenderingNode(ViewData::Entry& entry, ViewData* vd, int vertexLodMod, f const osg::Vec2f& center = entry.mNode->getCenter(); bool activeGrid = (center.x() > gridbounds.x() && center.y() > gridbounds.y() && center.x() < gridbounds.z() && center.y() < gridbounds.w()); - for (QuadTreeWorld::ChunkManager* m : chunkManagers) + for (QuadTreeWorld::ChunkManager* m : mChunkManagers) { - if (m->getViewDistance() && entry.mNode->distance(vd->getViewPoint()) > m->getViewDistance() + reuseDistance) + if (mRevalidateDistance && m->getViewDistance() && entry.mNode->distance(vd->getViewPoint()) > m->getViewDistance() + reuseDistance) continue; osg::ref_ptr n = m->getChunk(entry.mNode->getSize(), entry.mNode->getCenter(), ourLod, entry.mLodFlags, activeGrid, vd->getViewPoint(), compile); if (n) pat->addChild(n); @@ -398,7 +402,7 @@ void updateWaterCullingView(HeightCullCallback* callback, ViewData* vd, osgUtil: static bool debug = getenv("OPENMW_WATER_CULLING_DEBUG") != nullptr; for (unsigned int i=0; igetNumEntries(); ++i) { - ViewData::Entry& entry = vd->getEntry(i); + ViewDataEntry& entry = vd->getEntry(i); osg::BoundingBox bb = static_cast(entry.mRenderingNode->asGroup()->getChild(0))->getWaterBoundingBox(); if (!bb.valid()) continue; @@ -457,15 +461,15 @@ void QuadTreeWorld::accept(osg::NodeVisitor &nv) for (unsigned int i=0; igetNumEntries(); ++i) { - ViewData::Entry& entry = vd->getEntry(i); - loadRenderingNode(entry, vd, mVertexLodMod, cellWorldSize, mActiveGrid, mChunkManagers, false, mViewDataMap->getReuseDistance()); + ViewDataEntry& entry = vd->getEntry(i); + loadRenderingNode(entry, vd, cellWorldSize, mActiveGrid, false, mViewDataMap->getReuseDistance()); entry.mRenderingNode->accept(nv); } if (mHeightCullCallback && isCullVisitor) updateWaterCullingView(mHeightCullCallback, vd, static_cast(&nv), mStorage->getCellWorldSize(), !isGridEmpty()); - vd->markUnchanged(); + vd->setChanged(false); double referenceTime = nv.getFrameStamp() ? nv.getFrameStamp()->getReferenceTime() : 0.0; if (referenceTime != 0.0) @@ -540,9 +544,9 @@ void QuadTreeWorld::preload(View *view, const osg::Vec3f &viewPoint, const osg:: const float reuseDistance = std::max(mViewDataMap->getReuseDistance(), std::abs(distanceModifier)); for (unsigned int i=startEntry; igetNumEntries() && !abort; ++i) { - ViewData::Entry& entry = vd->getEntry(i); + ViewDataEntry& entry = vd->getEntry(i); - loadRenderingNode(entry, vd, mVertexLodMod, cellWorldSize, grid, mChunkManagers, true, reuseDistance); + loadRenderingNode(entry, vd, cellWorldSize, grid, true, reuseDistance); if (pass==0) reporter.addProgress(entry.mNode->getSize()); entry.mNode = nullptr; // Clear node lest we break the neighbours search for the next pass } @@ -579,6 +583,8 @@ void QuadTreeWorld::addChunkManager(QuadTreeWorld::ChunkManager* m) { mChunkManagers.push_back(m); mTerrainRoot->setNodeMask(mTerrainRoot->getNodeMask()|m->getNodeMask()); + if (m->getViewDistance()) + mRevalidateDistance = std::max(m->getViewDistance(), mRevalidateDistance); } void QuadTreeWorld::rebuildViews() @@ -586,4 +592,12 @@ void QuadTreeWorld::rebuildViews() mViewDataMap->rebuildViews(); } +void QuadTreeWorld::setViewDistance(float viewDistance) +{ + if (mViewDistance == viewDistance) + return; + mViewDistance = viewDistance; + mViewDataMap->rebuildViews(); +} + } diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp index 3bd606d6c6..9d21d65fc5 100644 --- a/components/terrain/quadtreeworld.hpp +++ b/components/terrain/quadtreeworld.hpp @@ -16,6 +16,9 @@ namespace Terrain { class RootNode; class ViewDataMap; + class ViewData; + struct ViewDataEntry; + class DebugChunkManager; /// @brief Terrain implementation that loads cells into a Quad Tree, with geometry LOD and texture LOD. @@ -30,7 +33,7 @@ namespace Terrain void enable(bool enabled) override; - void setViewDistance(float distance) override { mViewDistance = distance; } + void setViewDistance(float distance) override; void cacheCell(View *view, int x, int y) override {} /// @note Not thread safe. @@ -60,6 +63,7 @@ namespace Terrain private: void ensureQuadTreeBuilt(); + void loadRenderingNode(ViewDataEntry& entry, ViewData* vd, float cellWorldSize, const osg::Vec4i &gridbounds, bool compile, float reuseDistance); osg::ref_ptr mRootNode; @@ -75,6 +79,7 @@ namespace Terrain float mMinSize; bool mDebugTerrainChunks; std::unique_ptr mDebugChunkManager; + float mRevalidateDistance; }; } diff --git a/components/terrain/viewdata.cpp b/components/terrain/viewdata.cpp index 3ebc99f1df..ae23f034a8 100644 --- a/components/terrain/viewdata.cpp +++ b/components/terrain/viewdata.cpp @@ -12,12 +12,10 @@ ViewData::ViewData() , mHasViewPoint(false) , mWorldUpdateRevision(0) { - } ViewData::~ViewData() { - } void ViewData::copyFrom(const ViewData& other) @@ -38,42 +36,19 @@ void ViewData::add(QuadTreeNode *node) if (index+1 > mEntries.size()) mEntries.resize(index+1); - Entry& entry = mEntries[index]; + ViewDataEntry& entry = mEntries[index]; if (entry.set(node)) mChanged = true; } -unsigned int ViewData::getNumEntries() const -{ - return mNumEntries; -} - -ViewData::Entry &ViewData::getEntry(unsigned int i) -{ - return mEntries[i]; -} - -bool ViewData::hasChanged() const -{ - return mChanged; -} - -bool ViewData::hasViewPoint() const -{ - return mHasViewPoint; -} - void ViewData::setViewPoint(const osg::Vec3f &viewPoint) { mViewPoint = viewPoint; mHasViewPoint = true; } -const osg::Vec3f& ViewData::getViewPoint() const -{ - return mViewPoint; -} - +// NOTE: As a performance optimisation, we cache mRenderingNodes from previous frames here. +// If this cache becomes invalid (e.g. through mWorldUpdateRevision), we need to use clear() instead of reset(). void ViewData::reset() { // clear any unused entries @@ -108,14 +83,13 @@ bool ViewData::contains(QuadTreeNode *node) const return false; } -ViewData::Entry::Entry() +ViewDataEntry::ViewDataEntry() : mNode(nullptr) , mLodFlags(0) { - } -bool ViewData::Entry::set(QuadTreeNode *node) +bool ViewDataEntry::set(QuadTreeNode *node) { if (node == mNode) return false; @@ -164,9 +138,14 @@ ViewData *ViewDataMap::getViewData(osg::Object *viewer, const osg::Vec3f& viewPo } else if (!mostSuitableView) { + if (vd->getWorldUpdateRevision() != mWorldUpdateRevision) + { + vd->setWorldUpdateRevision(mWorldUpdateRevision); + vd->clear(); + } vd->setViewPoint(viewPoint); vd->setActiveGrid(activeGrid); - vd->setWorldUpdateRevision(mWorldUpdateRevision); + vd->setChanged(true); needsUpdate = true; } } diff --git a/components/terrain/viewdata.hpp b/components/terrain/viewdata.hpp index 5d814251ea..b7dbc977b1 100644 --- a/components/terrain/viewdata.hpp +++ b/components/terrain/viewdata.hpp @@ -13,6 +13,18 @@ namespace Terrain class QuadTreeNode; + struct ViewDataEntry + { + ViewDataEntry(); + + bool set(QuadTreeNode* node); + + QuadTreeNode* mNode; + + unsigned int mLodFlags; + osg::ref_ptr mRenderingNode; + }; + class ViewData : public View { public: @@ -31,33 +43,22 @@ namespace Terrain void copyFrom(const ViewData& other); - struct Entry - { - Entry(); - - bool set(QuadTreeNode* node); - - QuadTreeNode* mNode; - - unsigned int mLodFlags; - osg::ref_ptr mRenderingNode; - }; - - unsigned int getNumEntries() const; - - Entry& getEntry(unsigned int i); + unsigned int getNumEntries() const { return mNumEntries; } + ViewDataEntry& getEntry(unsigned int i) { return mEntries[i]; } double getLastUsageTimeStamp() const { return mLastUsageTimeStamp; } void setLastUsageTimeStamp(double timeStamp) { mLastUsageTimeStamp = timeStamp; } - /// @return Have any nodes changed since the last frame - bool hasChanged() const; - void markUnchanged() { mChanged = false; } + /// Indicates at least one mNode of mEntries has changed or the view point has moved beyond mReuseDistance. + /// @note Such changes may necessitate a revalidation of cached mRenderingNodes elsewhere depending + /// on the parameters that affect the creation of mRenderingNode. + bool hasChanged() const { return mChanged; } + void setChanged(bool changed) { mChanged = changed; } - bool hasViewPoint() const; + bool hasViewPoint() const { return mHasViewPoint; } void setViewPoint(const osg::Vec3f& viewPoint); - const osg::Vec3f& getViewPoint() const; + const osg::Vec3f& getViewPoint() const { return mViewPoint; } void setActiveGrid(const osg::Vec4i &grid) { if (grid != mActiveGrid) {mActiveGrid = grid;mEntries.clear();mNumEntries=0;} } const osg::Vec4i &getActiveGrid() const { return mActiveGrid;} @@ -66,7 +67,7 @@ namespace Terrain void setWorldUpdateRevision(int updateRevision) { mWorldUpdateRevision = updateRevision; } private: - std::vector mEntries; + std::vector mEntries; unsigned int mNumEntries; double mLastUsageTimeStamp; bool mChanged; diff --git a/components/widgets/fontwrapper.hpp b/components/widgets/fontwrapper.hpp index daa69f9202..16ebba3587 100644 --- a/components/widgets/fontwrapper.hpp +++ b/components/widgets/fontwrapper.hpp @@ -31,15 +31,11 @@ namespace Gui } private: - static int clamp(const int& value, const int& lowBound, const int& highBound) - { - return std::min(std::max(lowBound, value), highBound); - } std::string getFontSize() { // Note: we can not use the FontLoader here, so there is a code duplication a bit. - static const std::string fontSize = std::to_string(clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20)); + static const std::string fontSize = std::to_string(std::clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20)); return fontSize; } }; diff --git a/components/widgets/numericeditbox.cpp b/components/widgets/numericeditbox.cpp index e8ba226f70..c6ff9628ee 100644 --- a/components/widgets/numericeditbox.cpp +++ b/components/widgets/numericeditbox.cpp @@ -31,7 +31,7 @@ namespace Gui try { mValue = std::stoi(newCaption); - int capped = std::min(mMaxValue, std::max(mValue, mMinValue)); + int capped = std::clamp(mValue, mMinValue, mMaxValue); if (capped != mValue) { mValue = capped; diff --git a/docs/requirements.txt b/docs/requirements.txt index 288d462d0d..ac82149f5d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ parse_cmake -sphinx>=1.7.0 +sphinx==1.8.5 +docutils==0.17.1 diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 538cfd4c6d..2af7946536 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -105,10 +105,10 @@ If you are running macOS, you can also download Morrowind through Steam: #. Launch the Steam client and let it download. You can then find ``Morrowind.esm`` at ``~/Library/Application Support/Steam/steamapps/common/The Elder Scrolls III - Morrowind/Data Files/`` -Linux ----- -Debian/Ubuntu - using "Steam Proton" & "OpenMW launcher". ----- +Linux +----- +Debian/Ubuntu - using "Steam Proton" & "OpenMW launcher". +--------------------------------------------------------- #. Install Steam from "Ubuntu Software" Center #. Enable Proton (basically WINE under the hood). This is done in the Steam client menu drop down. Select, "Steam | Settings" then in the "SteamPlay" section check the box next to "enable steam play for all other titles" #. Now Morrowind should be selectable in your game list (as long as you own it). You can install it like any other game, choose to install it and remember the directory path of the location you pick. diff --git a/docs/source/reference/documentationHowTo.rst b/docs/source/reference/documentationHowTo.rst index 75dbe8dca2..d2b67d02ca 100644 --- a/docs/source/reference/documentationHowTo.rst +++ b/docs/source/reference/documentationHowTo.rst @@ -154,9 +154,9 @@ A push is just copying those "committed" changes to your online repo. (Commit and push can be combined in one step in PyCharm, so yay) Once you've pushed all the changes you need to contribute something to the project, you will then submit a pull request, so called because you are *requesting* that the project maintainers "pull" - and merge the changes you've made into the project master repository. One of the project maintainers will probably ask - you to make some corrections or clarifications. Go back and repeat this process to make those changes, - and repeat until they're good enough to get merged. +and merge the changes you've made into the project master repository. One of the project maintainers will probably ask +you to make some corrections or clarifications. Go back and repeat this process to make those changes, +and repeat until they're good enough to get merged. So to go over all that again. You rebase *every* time you start working on something to ensure you're working on the most updated version (I do literally every time I open PyCharm). Then make your edits. diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst index bcbee4349e..ef74684182 100644 --- a/docs/source/reference/lua-scripting/engine_handlers.rst +++ b/docs/source/reference/lua-scripting/engine_handlers.rst @@ -6,14 +6,22 @@ Engine handler is a function defined by a script, that can be called by the engi +---------------------------------------------------------------------------------------------------------+ | **Can be defined by any script** | +----------------------------------+----------------------------------------------------------------------+ +| onInit(initData) | | Called once when the script is created (not loaded). `InitData can`| +| | | `be assigned to a script in openmw-cs (not yet implemented)`. | +| | | ``onInterfaceOverride`` can be called before ``onInit``. | ++----------------------------------+----------------------------------------------------------------------+ | onUpdate(dt) | | Called every frame if game not paused. `dt` is the time | | | | from the last update in seconds. | +----------------------------------+----------------------------------------------------------------------+ -| onSave() -> data | | Called when the game is saving. May be called in inactive | +| onSave() -> savedData | | Called when the game is saving. May be called in inactive | | | | state, so it shouldn't use `openmw.nearby`. | +----------------------------------+----------------------------------------------------------------------+ -| onLoad(data) | | Called on loading with the data previosly returned by | -| | | onSave. During loading the object is always inactive. | +| onLoad(savedData, initData) | | Called on loading with the data previosly returned by | +| | | onSave. During loading the object is always inactive. initData is | +| | | the same as in onInit. | ++----------------------------------+----------------------------------------------------------------------+ +| onInterfaceOverride(base) | | Called if the current script has an interface and overrides an | +| | | interface (``base``) of another script. | +----------------------------------+----------------------------------------------------------------------+ | **Only for global scripts** | +----------------------------------+----------------------------------------------------------------------+ diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index 44c79c35d8..c32fc74fa5 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -73,7 +73,7 @@ Let's write a simple example of a `Player script`: .. code-block:: Lua - -- Saved to my_lua_mod/example/player.lua + -- Save to my_lua_mod/example/player.lua local ui = require('openmw.ui') @@ -87,42 +87,82 @@ Let's write a simple example of a `Player script`: } } -In order to attach it to the player we also need a global script: +The script will be used only if it is specified in one of content files. +OpenMW Lua is an inclusive OpenMW feature, so it can not be controlled by ESP/ESM. +The options are: -.. code-block:: Lua - - -- Saved to my_lua_mod/example/global.lua - - return { - engineHandlers = { - onPlayerAdded = function(player) player:addScript('example/player.lua') end - } - } - -And one more file -- to start the global script: +1. Create text file "my_lua_mod.omwscripts" with the following line: :: - # Saved to my_lua_mod/my_lua_mod.omwscripts + PLAYER: example/player.lua - # It is just a list of global scripts to run. Each file is on a separate line. - example/global.lua +2. (not implemented yet) Add the script in OpenMW CS on "Lua scripts" view and save as "my_lua_mod.omwaddon". -Finally :ref:`register ` it in ``openmw.cfg``: + +Enable it in ``openmw.cfg`` the same way as any other mod: :: data=path/to/my_lua_mod - lua-scripts=my_lua_mod.omwscripts + content=my_lua_mod.omwscripts # or content=my_lua_mod.omwaddon Now every time the player presses "X" on a keyboard, a message is shown. + +Format of ``.omwscripts`` +========================= + +:: + + # Lines starting with '#' are comments + + GLOBAL: my_mod/some_global_script.lua + + # Script that will be automatically attached to the player + PLAYER: my_mod/player.lua + + # Local script that will be automatically attached to every NPC and every creature in the game + NPC, CREATURE: my_mod/some_other_script.lua + + # Local script that can be attached to any object by a global script + CUSTOM: my_mod/something.lua + + # Local script that will be automatically attached to any Container AND can be + # attached to any other object by a global script. + CONTAINER, CUSTOM: my_mod/container.lua + +Each script is described by one line: +``: ``. +The order of lines determines the script load order (i.e. script priorities). + +Possible flags are: + +- ``GLOBAL`` - a global script; always active, can not by stopped; +- ``CUSTOM`` - dynamic local script that can be started or stopped by a global script; +- ``PLAYER`` - an auto started player script; +- ``ACTIVATOR`` - a local script that will be automatically attached to any activator; +- ``ARMOR`` - a local script that will be automatically attached to any armor; +- ``BOOK`` - a local script that will be automatically attached to any book; +- ``CLOTHING`` - a local script that will be automatically attached to any clothing; +- ``CONTAINER`` - a local script that will be automatically attached to any container; +- ``CREATURE`` - a local script that will be automatically attached to any creature; +- ``DOOR`` - a local script that will be automatically attached to any door; +- ``INGREDIENT`` - a local script that will be automatically attached to any ingredient; +- ``LIGHT`` - a local script that will be automatically attached to any light; +- ``MISC_ITEM`` - a local script that will be automatically attached to any miscellaneous item; +- ``NPC`` - a local script that will be automatically attached to any NPC; +- ``POTION`` - a local script that will be automatically attached to any potion; +- ``WEAPON`` - a local script that will be automatically attached to any weapon. + +Several flags (except ``GLOBAL``) can be used with a single script. Use space or comma as a separator. + Hot reloading ============= It is possible to modify a script without restarting OpenMW. To apply changes, open the in-game console and run the command: ``reloadlua``. This will restart all Lua scripts using the `onSave and onLoad`_ handlers the same way as if the game was saved or loaded. -It works only with existing ``*.lua`` files that are not packed to any archives. Adding new scripts or modifying ``*.omwscripts`` files always requires restarting the game. +It reloads all ``.omwscripts`` files and ``.lua`` files that are not packed to any archives. ``.omwaddon`` files and scripts packed to BSA can not be changed without restarting the game. Script structure ================ @@ -196,7 +236,7 @@ Engine handlers An engine handler is a function defined by a script, that can be called by the engine. I.e. it is an engine-to-script interaction. Not visible to other scripts. If several scripts register an engine handler with the same name, -the engine calls all of them in the same order as the scripts were started. +the engine calls all of them according to the load order (i.e. the order of ``content=`` entries in ``openmw.cfg``) and the order of scripts in ``omwaddon/omwscripts``. Some engine handlers are allowed only for global, or only for local/player scripts. Some are universal. See :ref:`Engine handlers reference`. @@ -210,12 +250,6 @@ The value that `onSave` returns will be passed to `onLoad` when the game is load It is the only way to save the internal state of a script. All other script variables will be lost after closing the game. The saved state must be :ref:`serializable `. -The list of active global scripts is controlled by ``*.omwscripts`` files. Loading a save doesn't synchronize -the list of global scripts with those that were active previously, it only calls `onLoad` for those currently active. - -For local scripts the situation is different. When a save is loading, it tries to run all local scripts that were saved. -So if ``lua-scripts=`` entries of some mod are removed, but ``data=`` entries are still enabled, then local scripts from the mod may still run. - `onSave` and `onLoad` can be called even for objects in inactive state, so it shouldn't use `openmw.nearby`. An example: @@ -366,26 +400,28 @@ Overriding the interface and adding a debug output: .. code-block:: Lua - local interfaces = require('openmw.interfaces') + local baseInterface = nil -- will be assigned by `onInterfaceOverride` + interface = { + version = 1, + doSomething = function(x, y) + print(string.format('SomeUtils.doSomething(%d, %d)', x, y)) + baseInterface.doSomething(x, y) -- calls the original `doSomething` - -- it is important to save it before returning the new interface - local orig = interfaces.SomeUtils - - return { - interfaceName = "SomeUtils" - interface = { - version = orig.version, - doSomething = function(x, y) - print(string.format('SomeUtils.doSomething(%d, %d)', x, y)) - orig.doSomething(x, y) -- calls the original `doSomething` - - -- WRONG! Would lead to an infinite recursion. - -- interfaces.SomeUtils.doSomething(x, y) - end, - } + -- WRONG! Would lead to an infinite recursion. + -- local interfaces = require('openmw.interfaces') + -- interfaces.SomeUtils.doSomething(x, y) + end, } -A general recomendation about overriding is that the new interface should be fully compatible with the old one. + return { + interfaceName = "SomeUtils", + interface = interface, + engineHandlers = { + onInterfaceOverride = function(base) baseInterface = base end, + }, + } + +A general recommendation about overriding is that the new interface should be fully compatible with the old one. So it is fine to change the behaviour of `SomeUtils.doSomething`, but if you want to add a completely new function, it would be better to create a new interface for it. For example `SomeUtilsExtended` with an additional function `doSomethingElse`. @@ -418,7 +454,7 @@ Events are the main way of interacting between local and global scripts. They are not recommended for interactions between two global scripts, because in this case interfaces are more convenient. If several scripts register handlers for the same event, the handlers will be called in reverse order (opposite to engine handlers). -I.e. the handler from the last attached script will be called first. +I.e. the handler from the last script in the load order will be called first. Return value 'false' means "skip all other handlers for this event". Any other return value (including nil) means nothing. @@ -471,7 +507,7 @@ The protection mod attaches an additional local script to every actor. The scrip eventHandlers = { DamagedByDarkPower = reduceDarkDamage }, } -In order to be able to intercept the event, the protection script should be attached after the original script (i.e. below in the load order). +In order to be able to intercept the event, the protection script should be placed in the load order below the original script. Timers diff --git a/docs/source/reference/modding/extended.rst b/docs/source/reference/modding/extended.rst index f107617b33..db7df2e916 100644 --- a/docs/source/reference/modding/extended.rst +++ b/docs/source/reference/modding/extended.rst @@ -333,17 +333,9 @@ Lua scripting OpenMW supports Lua scripts. See :ref:`Lua scripting documentation `. It is not compatible with MWSE. A mod with Lua scripts will work only if it was developed specifically for OpenMW. -Mods can contain ``*.omwscripts`` files. They should be registered in the ``openmw.cfg`` via "lua-scripts" entries. The order of the "lua-scripts" entries can be important. If "some_lua_mod" uses API provided by "another_lua_mod", then omwscripts from "another_lua_mod" should be registered first. For example: - -:: - - data="path/to/another_lua_mod" - content=another_lua_mod.omwaddon - lua-scripts=another_lua_mod.omwscripts - - data="path/to/some_lua_mod" - content=some_lua_mod.omwaddon - lua-scripts=some_lua_mod.omwscripts +Installation of a Lua mod is the same as of any other mod: add ``data=`` and ``content=`` entries to ``openmw.cfg``. +Files with suffix ``.omwscripts`` are special type of content files and should also be enabled using ``content=`` entries. +Note that for some mods load order can be important. .. _`Graphic Herbalism`: https://www.nexusmods.com/morrowind/mods/46599 .. _`OpenMW Containers Animated`: https://www.nexusmods.com/morrowind/mods/46232 diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index fb7b537701..58d5345f65 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -441,7 +441,7 @@ Some mods add harvestable container models. When this setting is enabled, activa When this setting is turned off or when activating a regular container, the menu will open as usual. allow actors to follow over water surface ---------------------- +----------------------------------------- :Type: boolean :Range: True/False diff --git a/docs/source/reference/modding/settings/general.rst b/docs/source/reference/modding/settings/general.rst index f0ebe4f972..ee5b908b4a 100644 --- a/docs/source/reference/modding/settings/general.rst +++ b/docs/source/reference/modding/settings/general.rst @@ -61,7 +61,7 @@ Mipmapping is a way of reducing the processing power needed during minification by pregenerating a series of smaller textures. notify on saved screenshot --------------- +-------------------------- :Type: boolean :Range: True/False diff --git a/docs/source/reference/modding/settings/groundcover.rst b/docs/source/reference/modding/settings/groundcover.rst index 3e943e4284..7b060f58ad 100644 --- a/docs/source/reference/modding/settings/groundcover.rst +++ b/docs/source/reference/modding/settings/groundcover.rst @@ -51,6 +51,7 @@ Determines whether grass should respond to the player treading on it. .. list-table:: Modes :header-rows: 1 + * - Mode number - Meaning * - 0 @@ -77,6 +78,7 @@ How far away from the player grass can be before it's unaffected by being trod o .. list-table:: Presets :header-rows: 1 + * - Preset number - Range (Units) - Distance (Units) diff --git a/docs/source/reference/modding/settings/map.rst b/docs/source/reference/modding/settings/map.rst index a4d3cd7e0d..1412d6584f 100644 --- a/docs/source/reference/modding/settings/map.rst +++ b/docs/source/reference/modding/settings/map.rst @@ -125,6 +125,7 @@ max local viewing distance This setting controls the viewing distance on local map when 'distant terrain' is enabled. If this setting is greater than the viewing distance then only up to the viewing distance is used for local map, otherwise the viewing distance is used. If view distance is changed in settings menu during the game, then viewable distance on the local map is not updated. + .. warning:: Increasing this setting can increase cell load times, because the localmap take a snapshot of each cell contained in a square of 2 x (max local viewing distance) + 1 square. diff --git a/docs/source/reference/modding/settings/navigator.rst b/docs/source/reference/modding/settings/navigator.rst index fee4b2626e..aea817530e 100644 --- a/docs/source/reference/modding/settings/navigator.rst +++ b/docs/source/reference/modding/settings/navigator.rst @@ -1,5 +1,5 @@ Navigator Settings -################ +################## Main settings ************* @@ -43,7 +43,7 @@ Increasing this value may decrease performance. It's a limitation of `Recastnavigation `_ library. wait until min distance to player ------------------------------- +--------------------------------- :Type: integer :Range: >= 0 @@ -87,7 +87,7 @@ Memory will be consumed in approximately linear dependency from number of nav me But only for new locations or already dropped from cache. min update interval ms ----------------- +---------------------- :Type: integer :Range: >= 0 @@ -181,7 +181,7 @@ Every nav mesh is visible and every update is noticable. Potentially decreases performance. enable agents paths render -------------------- +-------------------------- :Type: boolean :Range: True/False @@ -193,7 +193,7 @@ Works even if Navigator is disabled. Potentially decreases performance. enable recast mesh render ----------------------- +------------------------- :Type: boolean :Range: True/False diff --git a/docs/source/reference/modding/settings/shaders.rst b/docs/source/reference/modding/settings/shaders.rst index 03b7805de6..296a351435 100644 --- a/docs/source/reference/modding/settings/shaders.rst +++ b/docs/source/reference/modding/settings/shaders.rst @@ -241,7 +241,7 @@ lighting` is on. This setting has no effect if :ref:`lighting method` is 'legacy'. minimum interior brightness ------------------------- +--------------------------- :Type: float :Range: 0.0-1.0 diff --git a/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst b/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst index 0ad35d7a50..e05177c268 100644 --- a/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst +++ b/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst @@ -15,7 +15,7 @@ Normal maps from Morrowind to OpenMW - `Tutorial - Morrowind, Part 2`_ General introduction to normal map conversion ------------------------------------------------- +--------------------------------------------- :Authors: Joakim (Lysol) Berg, Alexei (Capo) Dobrohotov :Updated: 2020-03-03 @@ -34,7 +34,7 @@ There are several techniques for bump-mapping, and normal-mapping is the most co So let's get on with it. OpenMW normal-mapping -************************ +********************* Normal-mapping in OpenMW works in a very simple way: The engine just looks for a texture with a *_n.dds* suffix, and you're done. @@ -70,7 +70,7 @@ settings.cfg_-file. Add these rows where it would make sense: See OpenMW's wiki page about `texture modding`_ to read more about it. Morrowind bump-mapping -***************************************************** +********************** **Conversion difficulty:** *Varies. Sometimes quick and easy, sometimes time-consuming and hard.* @@ -93,7 +93,7 @@ In this case you can benefit from OpenMW's normal-mapping support by using these This means that you will have to drop the bump-mapping references from the model and sometimes rename the texture. MGE XE normal-mapping -*************************************** +********************* **Conversion difficulty:** *Easy* @@ -169,7 +169,7 @@ depending on a few circumstances. In this tutorial, we will look at a very easy, although in some cases a bit time-consuming, example. Tutorial - Morrowind, Part 1 -********************** +**************************** We will be converting a quite popular texture replacer of the Hlaalu architecture, namely Lougian's `Hlaalu Bump mapped`_. Since this is just a texture pack and not a model replacer, @@ -201,7 +201,7 @@ We ignored those model files since they are not needed with OpenMW. In this tuto we will convert a mod that includes new, custom-made models. In other words, we cannot just ignore those files this time. Tutorial - Morrowind, Part 2 -********************** +**************************** The sacks included in Apel's `Various Things - Sacks`_ come in two versions – without bump-mapping, and with bump-mapping. Since we want the glory of normal-mapping in our OpenMW setup, we will go with the bump-mapped version. diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 50706b9770..35513bbb79 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -192,10 +192,26 @@ ------------------------------------------------------------------------------- -- Add new local script to the object. --- Can be called only from a global script. +-- Can be called only from a global script. Script should be specified in a content +-- file (omwgame/omwaddon/omwscripts) with a CUSTOM flag. -- @function [parent=#GameObject] addScript -- @param self --- @param #string scriptPath Path to the script in OpenMW virtual filesystem +-- @param #string scriptPath Path to the script in OpenMW virtual filesystem. + +------------------------------------------------------------------------------- +-- Whether a script with given path is attached to this object. +-- Can be called only from a global script. +-- @function [parent=#GameObject] hasScript +-- @param self +-- @param #string scriptPath Path to the script in OpenMW virtual filesystem. +-- @return #boolean + +------------------------------------------------------------------------------- +-- Removes script that was attached by `addScript` +-- Can be called only from a global script. +-- @function [parent=#GameObject] removeScript +-- @param self +-- @param #string scriptPath Path to the script in OpenMW virtual filesystem. ------------------------------------------------------------------------------- -- Moves object to given cell and position. diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt index 04446d2982..d86719f318 100644 --- a/files/shaders/CMakeLists.txt +++ b/files/shaders/CMakeLists.txt @@ -36,6 +36,9 @@ set(SHADER_FILES gui_fragment.glsl debug_vertex.glsl debug_fragment.glsl + sky_vertex.glsl + sky_fragment.glsl + skypasses.glsl ) copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_SHADERS_ROOT} ${DDIRRELATIVE} "${SHADER_FILES}") diff --git a/files/shaders/sky_fragment.glsl b/files/shaders/sky_fragment.glsl new file mode 100644 index 0000000000..cfa3650c02 --- /dev/null +++ b/files/shaders/sky_fragment.glsl @@ -0,0 +1,87 @@ +#version 120 + +#include "skypasses.glsl" + +uniform int pass; +uniform sampler2D diffuseMap; +uniform sampler2D maskMap; // PASS_MOON +uniform float opacity; // PASS_CLOUDS, PASS_ATMOSPHERE_NIGHT +uniform vec4 moonBlend; // PASS_MOON +uniform vec4 atmosphereFade; // PASS_MOON + +varying vec2 diffuseMapUV; +varying vec4 passColor; + +void paintAtmosphere(inout vec4 color) +{ + color = gl_FrontMaterial.emission; + color.a *= passColor.a; +} + +void paintAtmosphereNight(inout vec4 color) +{ + color = texture2D(diffuseMap, diffuseMapUV); + color.a *= passColor.a * opacity; +} + +void paintClouds(inout vec4 color) +{ + color = texture2D(diffuseMap, diffuseMapUV); + color.a *= passColor.a * opacity; + color.xyz = clamp(color.xyz * gl_FrontMaterial.emission.xyz, 0.0, 1.0); + + // ease transition between clear color and atmosphere/clouds + color = mix(vec4(gl_Fog.color.xyz, color.a), color, passColor.a); +} + +void paintMoon(inout vec4 color) +{ + vec4 phase = texture2D(diffuseMap, diffuseMapUV); + vec4 mask = texture2D(maskMap, diffuseMapUV); + + vec4 blendedLayer = phase * moonBlend; + color = vec4(blendedLayer.xyz + atmosphereFade.xyz, atmosphereFade.a * mask.a); +} + +void paintSun(inout vec4 color) +{ + color = texture2D(diffuseMap, diffuseMapUV); + color.a *= gl_FrontMaterial.diffuse.a; +} + +void paintSunflashQuery(inout vec4 color) +{ + const float threshold = 0.8; + + color = texture2D(diffuseMap, diffuseMapUV); + if (color.a <= threshold) + discard; +} + +void paintSunglare(inout vec4 color) +{ + color = gl_FrontMaterial.emission; + color.a = gl_FrontMaterial.diffuse.a; +} + +void main() +{ + vec4 color = vec4(0.0); + + if (pass == PASS_ATMOSPHERE) + paintAtmosphere(color); + else if (pass == PASS_ATMOSPHERE_NIGHT) + paintAtmosphereNight(color); + else if (pass == PASS_CLOUDS) + paintClouds(color); + else if (pass == PASS_MOON) + paintMoon(color); + else if (pass == PASS_SUN) + paintSun(color); + else if (pass == PASS_SUNFLASH_QUERY) + paintSunflashQuery(color); + else if (pass == PASS_SUNGLARE) + paintSunglare(color); + + gl_FragData[0] = color; +} diff --git a/files/shaders/sky_vertex.glsl b/files/shaders/sky_vertex.glsl new file mode 100644 index 0000000000..9c676140ac --- /dev/null +++ b/files/shaders/sky_vertex.glsl @@ -0,0 +1,20 @@ +#version 120 + +#include "skypasses.glsl" + +uniform mat4 projectionMatrix; +uniform int pass; + +varying vec4 passColor; +varying vec2 diffuseMapUV; + +void main() +{ + gl_Position = projectionMatrix * (gl_ModelViewMatrix * gl_Vertex); + passColor = gl_Color; + + if (pass == PASS_CLOUDS) + diffuseMapUV = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy; + else + diffuseMapUV = gl_MultiTexCoord0.xy; +} diff --git a/files/shaders/skypasses.glsl b/files/shaders/skypasses.glsl new file mode 100644 index 0000000000..e80d4eb259 --- /dev/null +++ b/files/shaders/skypasses.glsl @@ -0,0 +1,7 @@ +#define PASS_ATMOSPHERE 0 +#define PASS_ATMOSPHERE_NIGHT 1 +#define PASS_CLOUDS 2 +#define PASS_MOON 3 +#define PASS_SUN 4 +#define PASS_SUNFLASH_QUERY 5 +#define PASS_SUNGLARE 6 diff --git a/readthedocs.yml b/readthedocs.yml deleted file mode 100644 index e53e54b785..0000000000 --- a/readthedocs.yml +++ /dev/null @@ -1,2 +0,0 @@ -# Don't build any extra formats -formats: [] \ No newline at end of file