Merge remote-tracking branch 'upstream/master'

pull/3208/head^2^2
wareya 3 years ago
commit 6d98866be0

@ -8,6 +8,9 @@ stages:
- docker - docker
- linux - linux
image: debian:bullseye image: debian:bullseye
rules:
- if: $CI_PIPELINE_SOURCE == "push"
.Debian: .Debian:
extends: .Debian_Image extends: .Debian_Image
@ -52,7 +55,7 @@ Coverity:
extends: .Debian_Image extends: .Debian_Image
stage: build stage: build
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"' - if: $CI_PIPELINE_SOURCE == "schedule"
before_script: before_script:
- CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic coverity - 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 - 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: variables:
CC: gcc CC: gcc
CXX: g++ CXX: g++
artifacts:
Debian_GCC: Debian_GCC:
extends: .Debian extends: .Debian
@ -162,6 +164,7 @@ Debian_Clang_tests_Debug:
only: only:
variables: variables:
- $CI_PROJECT_ID == "7107382" - $CI_PROJECT_ID == "7107382"
- $CI_PIPELINE_SOURCE == "push"
cache: cache:
paths: paths:
- ccache/ - ccache/
@ -184,7 +187,6 @@ Debian_Clang_tests_Debug:
macOS11_Xcode12: macOS11_Xcode12:
extends: .MacOS extends: .MacOS
image: macos-11-xcode-12 image: macos-11-xcode-12
allow_failure: true
cache: cache:
key: macOS11_Xcode12.v1 key: macOS11_Xcode12.v1
variables: variables:
@ -193,6 +195,7 @@ macOS11_Xcode12:
macOS10.15_Xcode11: macOS10.15_Xcode11:
extends: .MacOS extends: .MacOS
image: macos-10.15-xcode-11 image: macos-10.15-xcode-11
allow_failure: true
cache: cache:
key: macOS10.15_Xcode11.v1 key: macOS10.15_Xcode11.v1
variables: variables:
@ -213,6 +216,8 @@ variables: &tests-targets
.Windows_Ninja_Base: .Windows_Ninja_Base:
tags: tags:
- windows - windows
rules:
- if: $CI_PIPELINE_SOURCE == "push"
before_script: before_script:
- Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
- choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1 - 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: .Windows_MSBuild_Base:
tags: tags:
- windows - windows
rules:
- if: $CI_PIPELINE_SOURCE == "push"
before_script: before_script:
- Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
- choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1 - 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
- 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: Windows_MSBuild_Engine_Release:
extends: extends:
- .Windows_MSBuild_Base - .Windows_MSBuild_Base
@ -444,6 +460,8 @@ Debian_AndroidNDK_arm64-v8a:
tags: tags:
- linux - linux
image: debian:bullseye image: debian:bullseye
rules:
- if: $CI_PIPELINE_SOURCE == "push"
variables: variables:
CCACHE_SIZE: 3G CCACHE_SIZE: 3G
cache: cache:

@ -0,0 +1,10 @@
version: 2
sphinx:
configuration: docs/source/conf.py
python:
version: 3.8
install:
- requirements: docs/requirements.txt

@ -5,12 +5,14 @@
Bug #3246: ESSImporter: Most NPCs are dead on save load 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 #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 #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 #3846: Strings starting with "-" fail to compile if not enclosed in quotes
Bug #3905: Great House Dagoth issues Bug #3905: Great House Dagoth issues
Bug #4203: Resurrecting an actor should close the loot GUI Bug #4203: Resurrecting an actor should close the loot GUI
Bug #4602: Robert's Bodies: crash inside createInstance() Bug #4602: Robert's Bodies: crash inside createInstance()
Bug #4700: Editor: Incorrect command implementation Bug #4700: Editor: Incorrect command implementation
Bug #4744: Invisible particles must still be processed 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 #5100: Persuasion doesn't always clamp the resulting disposition
Bug #5120: Scripted object spawning updates physics system Bug #5120: Scripted object spawning updates physics system
Bug #5207: Loose summons can be present in scene 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 #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 #5842: GetDisposition adds temporary disposition change from different actors
Bug #5863: GetEffect should return true after the player has teleported 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 #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 #6051: NaN water height in ESM file is not handled gracefully
Bug #6066: addtopic "return" does not work from within script. No errors thrown 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 #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 #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 #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 #6172: Some creatures can't open doors
Bug #6174: Spellmaking and Enchanting sliders differences from vanilla 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 #6184: Command and Calm and Demoralize and Frenzy and Rally magic effects inconsistencies with vanilla
Bug #6197: Infinite Casting Loop 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 #6258: Barter menu glitches out when modifying prices
Bug #6273: Respawning NPCs rotation is inconsistent Bug #6273: Respawning NPCs rotation is inconsistent
Bug #6282: Laura craft doesn't follow the player character 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 #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 #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 #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 #890: OpenMW-CS: Column filtering
Feature #2554: Modifying an object triggers the instances table to scroll to the corresponding record 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 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 #6017: Separate persistent and temporary cell references when saving
Feature #6032: Reverse-z depth buffer Feature #6032: Reverse-z depth buffer
Feature #6078: First person should not clear 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 #6162: Refactor GUI to use shaders and to be GLES and GL3+ friendly
Feature #6199: Support FBO Rendering Feature #6199: Support FBO Rendering
Feature #6249: Alpha testing support for Collada 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 #6201: Remove the "Note: No relevant classes found. No output generated" warnings
Task #6264: Remove the old classes in animation.cpp Task #6264: Remove the old classes in animation.cpp
0.47.0 0.47.0
------ ------

@ -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) 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() endif()
if(MSVC)
add_compile_options("/utf-8")
endif()
# Dependencies # Dependencies
find_package(OpenGL REQUIRED) find_package(OpenGL REQUIRED)
@ -707,6 +711,8 @@ endif()
if (BUILD_OPENMW AND APPLE) if (BUILD_OPENMW AND APPLE)
# Without these flags LuaJit crashes on startup on OSX # Without these flags LuaJit crashes on startup on OSX
set_target_properties(openmw PROPERTIES LINK_FLAGS "-pagezero_size 10000 -image_base 100000000") 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() endif()
# Apple bundling # Apple bundling

@ -32,7 +32,7 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config:
{ {
ui.setupUi (this); ui.setupUi (this);
setObjectName ("DataFilesPage"); setObjectName ("DataFilesPage");
mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget, /*showOMWScripts=*/true);
const QString encoding = mGameSettings.value("encoding", "win1252"); const QString encoding = mGameSettings.value("encoding", "win1252");
mSelector->setEncoding(encoding); mSelector->setEncoding(encoding);

@ -10,6 +10,8 @@ QSet<QString> CellNameLoader::getCellNames(QStringList &contentPaths)
// Loop through all content files // Loop through all content files
for (auto &contentPath : contentPaths) { for (auto &contentPath : contentPaths) {
if (contentPath.endsWith(".omwscripts", Qt::CaseInsensitive))
continue;
esmReader.open(contentPath.toStdString()); esmReader.open(contentPath.toStdString());
// Loop through all records // Loop through all records

@ -296,7 +296,7 @@ namespace CSMWorld
const std::string& destination, const UniversalId::Type type) const std::string& destination, const UniversalId::Type type)
{ {
int index = cloneRecordImp(origin, destination, type); int index = cloneRecordImp(origin, destination, type);
mRecords.at(index)->get().mPlugin = 0; mRecords.at(index)->get().setPlugin(0);
} }
template<typename ESXRecordT, typename IdAccessorT> template<typename ESXRecordT, typename IdAccessorT>
@ -311,7 +311,7 @@ namespace CSMWorld
int index = touchRecordImp(id); int index = touchRecordImp(id);
if (index >= 0) if (index >= 0)
{ {
mRecords.at(index)->get().mPlugin = 0; mRecords.at(index)->get().setPlugin(0);
return true; return true;
} }

@ -52,7 +52,7 @@ namespace CSMWorld
QVariant LandPluginIndexColumn::get(const Record<Land>& record) const QVariant LandPluginIndexColumn::get(const Record<Land>& record) const
{ {
return record.get().mPlugin; return record.get().getPlugin();
} }
bool LandPluginIndexColumn::isEditable() const bool LandPluginIndexColumn::isEditable() const

@ -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)); 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() void CSMWorld::ScriptContext::invalidateIds()
{ {
mIdsUpdated = false; mIdsUpdated = false;

@ -39,9 +39,6 @@ namespace CSMWorld
bool isId (const std::string& name) const override; bool isId (const std::string& name) const override;
///< Does \a name match an ID, that can be referenced? ///< 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 invalidateIds();
void clear(); void clear();

@ -24,7 +24,7 @@ CSVDoc::FileDialog::FileDialog(QWidget *parent) :
resize(400, 400); resize(400, 400);
setObjectName ("FileDialog"); setObjectName ("FileDialog");
mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget, /*showOMWScripts=*/false);
mAdjusterWidget = new AdjusterWidget (this); mAdjusterWidget = new AdjusterWidget (this);
} }

@ -111,7 +111,7 @@ namespace CSVRender
if (!mesh.empty() && node != mNodeMap.end()) if (!mesh.empty() && node != mNodeMap.end())
{ {
auto instance = sceneMgr->getInstance(mesh); auto instance = sceneMgr->getInstance(mesh);
SceneUtil::attach(instance, mSkeleton, boneName, node->second); SceneUtil::attach(instance, mSkeleton, boneName, node->second, sceneMgr);
} }
} }

@ -308,7 +308,7 @@ osg::ref_ptr<osg::Node> CSVRender::Object::makeRotateMarker (int axis)
const float OuterRadius = InnerRadius + MarkerShaftWidth; const float OuterRadius = InnerRadius + MarkerShaftWidth;
const float SegmentDistance = 100.f; 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<int>(OuterRadius * 2 * osg::PI / SegmentDistance, 24, 64);
const size_t VerticesPerSegment = 4; const size_t VerticesPerSegment = 4;
const size_t IndicesPerSegment = 24; const size_t IndicesPerSegment = 24;

@ -19,7 +19,7 @@ set(GAME_HEADER
source_group(game FILES ${GAME} ${GAME_HEADER}) source_group(game FILES ${GAME} ${GAME_HEADER})
add_openmw_dir (mwrender 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 creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager
bulletdebugdraw globalmap characterpreview camera viewovershoulder localmap water terrainstorage ripplesimulation bulletdebugdraw globalmap characterpreview camera viewovershoulder localmap water terrainstorage ripplesimulation
renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover postprocessor 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 containerstore actiontalk actiontake manualref player cellvisitors failedaction
cells localscripts customdata inventorystore ptr actionopen actionread actionharvest cells localscripts customdata inventorystore ptr actionopen actionread actionharvest
actionequip timestamp actionalchemy cellstore actionapply actioneat 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 contentloader esmloader actiontrap cellreflist cellref weather projectilemanager
cellpreloader datetimemanager cellpreloader datetimemanager
) )
@ -94,7 +94,7 @@ add_openmw_dir (mwmechanics
aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance
disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning
character actors objects aistate trading weaponpriority spellpriority weapontype spellutil character actors objects aistate trading weaponpriority spellpriority weapontype spellutil
spellabsorption spelleffects spelleffects
) )
add_openmw_dir (mwstate add_openmw_dir (mwstate

@ -495,11 +495,6 @@ void OMW::Engine::addGroundcoverFile(const std::string& file)
mGroundcoverFiles.emplace_back(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) void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame)
{ {
mSkipMenu = skipMenu; mSkipMenu = skipMenu;
@ -674,7 +669,7 @@ void OMW::Engine::setWindowIcon()
void OMW::Engine::prepareEngine (Settings::Manager & settings) void OMW::Engine::prepareEngine (Settings::Manager & settings)
{ {
mEnvironment.setStateManager ( mEnvironment.setStateManager (
new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles.at (0))); new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles));
createWindow(settings); createWindow(settings);
@ -714,7 +709,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings)
mViewer->addEventHandler(mScreenCaptureHandler); mViewer->addEventHandler(mScreenCaptureHandler);
mLuaManager = new MWLua::LuaManager(mVFS.get(), mLuaScriptListFiles); mLuaManager = new MWLua::LuaManager(mVFS.get());
mEnvironment.setLuaManager(mLuaManager); mEnvironment.setLuaManager(mLuaManager);
// Create input and UI first to set up a bootstrapping environment for // Create input and UI first to set up a bootstrapping environment for

@ -72,7 +72,6 @@ namespace OMW
std::string mCellName; std::string mCellName;
std::vector<std::string> mContentFiles; std::vector<std::string> mContentFiles;
std::vector<std::string> mGroundcoverFiles; std::vector<std::string> mGroundcoverFiles;
std::vector<std::string> mLuaScriptListFiles;
bool mSkipMenu; bool mSkipMenu;
bool mUseSound; bool mUseSound;
bool mCompileAll; bool mCompileAll;
@ -146,7 +145,6 @@ namespace OMW
*/ */
void addContentFile(const std::string& file); void addContentFile(const std::string& file);
void addGroundcoverFile(const std::string& file); void addGroundcoverFile(const std::string& file);
void addLuaScriptListFile(const std::string& file);
/// Disable or enable all sounds /// Disable or enable all sounds
void setSoundUsage(bool soundUsage); void setSoundUsage(bool soundUsage);

@ -124,9 +124,11 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
engine.addGroundcoverFile(file); engine.addGroundcoverFile(file);
} }
StringsVector luaScriptLists = variables["lua-scripts"].as<Files::EscapeStringVector>().toStdStringVector(); if (variables.count("lua-scripts"))
for (const auto& file : luaScriptLists) {
engine.addLuaScriptListFile(file); 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 // startup-settings
engine.setCell(variables["start"].as<Files::EscapeHashString>().toStdString()); engine.setCell(variables["start"].as<Files::EscapeHashString>().toStdString());

@ -30,6 +30,7 @@ namespace MWBase
virtual ~LuaManager() = default; virtual ~LuaManager() = default;
virtual void newGameStarted() = 0; virtual void newGameStarted() = 0;
virtual void gameLoaded() = 0;
virtual void registerObject(const MWWorld::Ptr& ptr) = 0; virtual void registerObject(const MWWorld::Ptr& ptr) = 0;
virtual void deregisterObject(const MWWorld::Ptr& ptr) = 0; virtual void deregisterObject(const MWWorld::Ptr& ptr) = 0;
virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0; virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0;

@ -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; 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 ///< @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 ///< @return an updated Ptr
virtual void scaleObject (const MWWorld::Ptr& ptr, float scale) = 0; virtual void scaleObject (const MWWorld::Ptr& ptr, float scale) = 0;

@ -82,7 +82,7 @@ namespace MWClass
const Creature::GMST& Creature::getGmst() const Creature::GMST& Creature::getGmst()
{ {
static const GMST gmst = [] static const GMST staticGmst = []
{ {
GMST gmst; GMST gmst;
@ -105,14 +105,17 @@ namespace MWClass
return gmst; return gmst;
} (); } ();
return gmst; return staticGmst;
} }
void Creature::ensureCustomData (const MWWorld::Ptr& ptr) const void Creature::ensureCustomData (const MWWorld::Ptr& ptr) const
{ {
if (!ptr.getRefData().getCustomData()) if (!ptr.getRefData().getCustomData())
{ {
std::unique_ptr<CreatureCustomData> data (new CreatureCustomData); auto tempData = std::make_unique<CreatureCustomData>();
CreatureCustomData* data = tempData.get();
MWMechanics::CreatureCustomDataResetter resetter(ptr);
ptr.getRefData().setCustomData(std::move(tempData));
MWWorld::LiveCellRef<ESM::Creature> *ref = ptr.get<ESM::Creature>(); MWWorld::LiveCellRef<ESM::Creature> *ref = ptr.get<ESM::Creature>();
@ -156,10 +159,7 @@ namespace MWClass
data->mCreatureStats.setGoldPool(ref->mBase->mData.mGold); data->mCreatureStats.setGoldPool(ref->mBase->mData.mGold);
data->mCreatureStats.setNeedRecalcDynamicStats(false); resetter.mPtr = {};
// store
ptr.getRefData().setCustomData(std::move(data));
getContainerStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId()); getContainerStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId());

@ -5,6 +5,7 @@
#include "../mwmechanics/levelledlist.hpp" #include "../mwmechanics/levelledlist.hpp"
#include "../mwworld/cellstore.hpp"
#include "../mwworld/customdata.hpp" #include "../mwworld/customdata.hpp"
#include "../mwmechanics/creaturestats.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<ESM::CreatureLevList> *ref = ptr.get<ESM::CreatureLevList>();
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 std::string CreatureLevList::getName (const MWWorld::ConstPtr& ptr) const
{ {
return ""; return "";

@ -32,6 +32,10 @@ namespace MWClass
///< Write additional state from \a ptr into \a state. ///< Write additional state from \a ptr into \a state.
void respawn (const MWWorld::Ptr& ptr) const override; 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;
}; };
} }

@ -266,7 +266,7 @@ namespace MWClass
const Npc::GMST& Npc::getGmst() const Npc::GMST& Npc::getGmst()
{ {
static const GMST gmst = [] static const GMST staticGmst = []
{ {
GMST gmst; GMST gmst;
@ -296,14 +296,18 @@ namespace MWClass
return gmst; return gmst;
} (); } ();
return gmst; return staticGmst;
} }
void Npc::ensureCustomData (const MWWorld::Ptr& ptr) const void Npc::ensureCustomData (const MWWorld::Ptr& ptr) const
{ {
if (!ptr.getRefData().getCustomData()) if (!ptr.getRefData().getCustomData())
{ {
std::unique_ptr<NpcCustomData> data(new NpcCustomData); bool recalculate = false;
auto tempData = std::make_unique<NpcCustomData>();
NpcCustomData* data = tempData.get();
MWMechanics::CreatureCustomDataResetter resetter(ptr);
ptr.getRefData().setCustomData(std::move(tempData));
MWWorld::LiveCellRef<ESM::NPC> *ref = ptr.get<ESM::NPC>(); MWWorld::LiveCellRef<ESM::NPC> *ref = ptr.get<ESM::NPC>();
@ -334,8 +338,6 @@ namespace MWClass
data->mNpcStats.setLevel(ref->mBase->mNpdt.mLevel); data->mNpcStats.setLevel(ref->mBase->mNpdt.mLevel);
data->mNpcStats.setBaseDisposition(ref->mBase->mNpdt.mDisposition); data->mNpcStats.setBaseDisposition(ref->mBase->mNpdt.mDisposition);
data->mNpcStats.setReputation(ref->mBase->mNpdt.mReputation); data->mNpcStats.setReputation(ref->mBase->mNpdt.mReputation);
data->mNpcStats.setNeedRecalcDynamicStats(false);
} }
else else
{ {
@ -351,7 +353,7 @@ namespace MWClass
autoCalculateAttributes(ref->mBase, data->mNpcStats); autoCalculateAttributes(ref->mBase, data->mNpcStats);
autoCalculateSkills(ref->mBase, data->mNpcStats, ptr, spellsInitialised); autoCalculateSkills(ref->mBase, data->mNpcStats, ptr, spellsInitialised);
data->mNpcStats.setNeedRecalcDynamicStats(true); recalculate = true;
} }
// Persistent actors with 0 health do not play death animation // Persistent actors with 0 health do not play death animation
@ -387,7 +389,9 @@ namespace MWClass
data->mNpcStats.setGoldPool(gold); data->mNpcStats.setGoldPool(gold);
// store // store
ptr.getRefData().setCustomData(std::move(data)); resetter.mPtr = {};
if(recalculate)
data->mNpcStats.recalculateMagicka();
// inventory // inventory
// setting ownership is used to make the NPC auto-equip his initial equipment only, and not bartered items // setting ownership is used to make the NPC auto-equip his initial equipment only, and not bartered items

@ -421,7 +421,7 @@ namespace MWDialogue
// Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with intimidate) // Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with intimidate)
npcStats.setBaseDisposition(0); npcStats.setBaseDisposition(0);
int zero = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false); 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); npcStats.setBaseDisposition(disposition);
} }

@ -347,8 +347,7 @@ namespace MWGui
{ {
if (!mScrollBar->getVisible()) if (!mScrollBar->getVisible())
return; return;
mScrollBar->setScrollPosition(std::min(static_cast<int>(mScrollBar->getScrollRange()-1), mScrollBar->setScrollPosition(std::clamp<int>(mScrollBar->getScrollPosition() - _rel*0.3, 0, mScrollBar->getScrollRange() - 1));
std::max(0, static_cast<int>(mScrollBar->getScrollPosition() - _rel*0.3))));
onScrollbarMoved(mScrollBar, mScrollBar->getScrollPosition()); onScrollbarMoved(mScrollBar, mScrollBar->getScrollPosition());
} }

@ -109,7 +109,7 @@ namespace MWGui
{ {
mEnchantmentPoints->setCaption(std::to_string(static_cast<int>(mEnchanting.getEnchantPoints(false))) + " / " + std::to_string(mEnchanting.getMaxEnchantValue())); mEnchantmentPoints->setCaption(std::to_string(static_cast<int>(mEnchanting.getEnchantPoints(false))) + " / " + std::to_string(mEnchanting.getMaxEnchantValue()));
mCharge->setCaption(std::to_string(mEnchanting.getGemCharge())); 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())); mCastCost->setCaption(std::to_string(mEnchanting.getEffectiveCastCost()));
mPrice->setCaption(std::to_string(mEnchanting.getEnchantPrice())); mPrice->setCaption(std::to_string(mEnchanting.getEnchantPrice()));

@ -610,7 +610,7 @@ namespace MWGui
static const float fNPCHealthBarFade = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fNPCHealthBarFade")->mValue.getFloat(); static const float fNPCHealthBarFade = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fNPCHealthBarFade")->mValue.getFloat();
if (fNPCHealthBarFade > 0.f) 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));
} }

@ -70,7 +70,7 @@ namespace MWGui
, mTrading(false) , mTrading(false)
, mUpdateTimer(0.f) , mUpdateTimer(0.f)
{ {
mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture(), mPreview->getTextureStateSet()));
mPreview->rebuild(); mPreview->rebuild();
mMainWidget->castType<MyGUI::Window>()->eventWindowChangeCoord += MyGUI::newDelegate(this, &InventoryWindow::onWindowResize); mMainWidget->castType<MyGUI::Window>()->eventWindowChangeCoord += MyGUI::newDelegate(this, &InventoryWindow::onWindowResize);

@ -313,9 +313,9 @@ struct JournalViewModelImpl : JournalViewModel
for (MWBase::Journal::TTopicIter i = journal->topicBegin (); i != journal->topicEnd (); ++i) for (MWBase::Journal::TTopicIter i = journal->topicBegin (); i != journal->topicEnd (); ++i)
{ {
Utf8Stream stream (i->first.c_str()); 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; continue;
visitor (i->second.getName()); visitor (i->second.getName());

@ -273,7 +273,7 @@ bool KeyboardNavigation::switchFocus(int direction, bool wrap)
if (wrap) if (wrap)
index = (index + keyFocusList.size())%keyFocusList.size(); index = (index + keyFocusList.size())%keyFocusList.size();
else else
index = std::min(std::max(0, index), static_cast<int>(keyFocusList.size())-1); index = std::clamp<int>(index, 0, keyFocusList.size() - 1);
MyGUI::Widget* next = keyFocusList[index]; MyGUI::Widget* next = keyFocusList[index];
int vertdiff = next->getTop() - focus->getTop(); int vertdiff = next->getTop() - focus->getTop();

@ -138,7 +138,7 @@ namespace MWGui
mPreview->rebuild(); mPreview->rebuild();
mPreview->setAngle (mCurrentAngle); mPreview->setAngle (mCurrentAngle);
mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture(), mPreview->getTextureStateSet()));
mPreviewImage->setRenderItemTexture(mPreviewTexture.get()); mPreviewImage->setRenderItemTexture(mPreviewTexture.get());
mPreviewImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); mPreviewImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f));

@ -171,7 +171,7 @@ namespace MWGui
else else
valueStr = MyGUI::utility::toString(int(value)); 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); value = (value-min)/(max-min);
scroll->setScrollPosition(static_cast<size_t>(value * (scroll->getScrollRange() - 1))); scroll->setScrollPosition(static_cast<size_t>(value * (scroll->getScrollRange() - 1)));

@ -1,6 +1,7 @@
#include "sortfilteritemmodel.hpp" #include "sortfilteritemmodel.hpp"
#include <components/misc/stringops.hpp> #include <components/misc/stringops.hpp>
#include <components/misc/utf8stream.hpp>
#include <components/debug/debuglog.hpp> #include <components/debug/debuglog.hpp>
#include <components/esm/loadalch.hpp> #include <components/esm/loadalch.hpp>
#include <components/esm/loadappa.hpp> #include <components/esm/loadappa.hpp>
@ -69,8 +70,8 @@ namespace
return compareType(leftType, rightType); return compareType(leftType, rightType);
// compare items by name // compare items by name
std::string leftName = Misc::StringUtils::lowerCaseUtf8(left.mBase.getClass().getName(left.mBase)); std::string leftName = Utf8Stream::lowerCaseUtf8(left.mBase.getClass().getName(left.mBase));
std::string rightName = Misc::StringUtils::lowerCaseUtf8(right.mBase.getClass().getName(right.mBase)); std::string rightName = Utf8Stream::lowerCaseUtf8(right.mBase.getClass().getName(right.mBase));
result = leftName.compare(rightName); result = leftName.compare(rightName);
if (result != 0) if (result != 0)
@ -213,7 +214,7 @@ namespace MWGui
if (!mNameFilter.empty()) 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; return itemName.find(mNameFilter) != std::string::npos;
} }
@ -226,7 +227,7 @@ namespace MWGui
for (const auto& effect : effects) 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) if (ciEffect.find(mEffectFilter) != std::string::npos)
return true; return true;
@ -285,7 +286,7 @@ namespace MWGui
return false; 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) if(compare.find(mNameFilter) == std::string::npos)
return false; return false;
@ -318,12 +319,12 @@ namespace MWGui
void SortFilterItemModel::setNameFilter (const std::string& filter) void SortFilterItemModel::setNameFilter (const std::string& filter)
{ {
mNameFilter = Misc::StringUtils::lowerCaseUtf8(filter); mNameFilter = Utf8Stream::lowerCaseUtf8(filter);
} }
void SortFilterItemModel::setEffectFilter (const std::string& filter) void SortFilterItemModel::setEffectFilter (const std::string& filter)
{ {
mEffectFilter = Misc::StringUtils::lowerCaseUtf8(filter); mEffectFilter = Utf8Stream::lowerCaseUtf8(filter);
} }
void SortFilterItemModel::update() void SortFilterItemModel::update()

@ -1,6 +1,7 @@
#include "spellmodel.hpp" #include "spellmodel.hpp"
#include <components/debug/debuglog.hpp> #include <components/debug/debuglog.hpp>
#include <components/misc/utf8stream.hpp>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
@ -69,7 +70,7 @@ namespace MWGui
fullEffectName += " " + wm->getGameSettingString(ESM::Attribute::sGmstAttributeIds[effect.mAttribute], ""); 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) if (convert.find(filter) != std::string::npos)
{ {
return true; return true;
@ -90,14 +91,14 @@ namespace MWGui
const MWWorld::ESMStore &esmStore = const MWWorld::ESMStore &esmStore =
MWBase::Environment::get().getWorld()->getStore(); MWBase::Environment::get().getWorld()->getStore();
std::string filter = Misc::StringUtils::lowerCaseUtf8(mFilter); std::string filter = Utf8Stream::lowerCaseUtf8(mFilter);
for (const ESM::Spell* spell : spells) for (const ESM::Spell* spell : spells)
{ {
if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell)
continue; continue;
std::string name = Misc::StringUtils::lowerCaseUtf8(spell->mName); std::string name = Utf8Stream::lowerCaseUtf8(spell->mName);
if (name.find(filter) == std::string::npos if (name.find(filter) == std::string::npos
&& !matchingEffectExists(filter, spell->mEffects)) && !matchingEffectExists(filter, spell->mEffects))
@ -139,7 +140,7 @@ namespace MWGui
if (enchant->mData.mType != ESM::Enchantment::WhenUsed && enchant->mData.mType != ESM::Enchantment::CastOnce) if (enchant->mData.mType != ESM::Enchantment::WhenUsed && enchant->mData.mType != ESM::Enchantment::CastOnce)
continue; 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 if (name.find(filter) == std::string::npos
&& !matchingEffectExists(filter, enchant->mEffects)) && !matchingEffectExists(filter, enchant->mEffects))

@ -599,8 +599,7 @@ namespace MWGui
text += "\n#{fontcolourhtml=normal}#{sExpelled}"; text += "\n#{fontcolourhtml=normal}#{sExpelled}";
else else
{ {
int rank = factionPair.second; const int rank = std::clamp(factionPair.second, 0, 9);
rank = std::max(0, std::min(9, rank));
text += std::string("\n#{fontcolourhtml=normal}") + faction->mRanks[rank]; text += std::string("\n#{fontcolourhtml=normal}") + faction->mRanks[rank];
if (rank < 9) if (rank < 9)

@ -70,7 +70,7 @@ namespace MWInput
} }
float deadZoneRadius = Settings::Manager::getFloat("joystick dead zone", "Input"); 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); mBindingsManager->setJoystickDeadZone(deadZoneRadius);
} }

@ -245,8 +245,8 @@ namespace MWInput
mMouseWheel += mouseWheelMove; mMouseWheel += mouseWheelMove;
const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize();
mGuiCursorX = std::max(0.f, std::min(mGuiCursorX, float(viewSize.width - 1))); mGuiCursorX = std::clamp<float>(mGuiCursorX, 0.f, viewSize.width - 1);
mGuiCursorY = std::max(0.f, std::min(mGuiCursorY, float(viewSize.height - 1))); mGuiCursorY = std::clamp<float>(mGuiCursorY, 0.f, viewSize.height - 1);
MyGUI::InputManager::getInstance().injectMouseMove(static_cast<int>(mGuiCursorX), static_cast<int>(mGuiCursorY), static_cast<int>(mMouseWheel)); MyGUI::InputManager::getInstance().injectMouseMove(static_cast<int>(mGuiCursorX), static_cast<int>(mGuiCursorY), static_cast<int>(mMouseWheel));
} }

@ -1,5 +1,7 @@
#include "actions.hpp" #include "actions.hpp"
#include <cstring>
#include <components/debug/debuglog.hpp> #include <components/debug/debuglog.hpp>
#include "../mwworld/cellstore.hpp" #include "../mwworld/cellstore.hpp"

@ -23,7 +23,7 @@ namespace MWLua
sol::usertype<AsyncPackageId> api = context.mLua->sol().new_usertype<AsyncPackageId>("AsyncPackage"); sol::usertype<AsyncPackageId> api = context.mLua->sol().new_usertype<AsyncPackageId>("AsyncPackage");
api["registerTimerCallback"] = [](const AsyncPackageId& asyncId, std::string_view name, sol::function callback) 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)}; return TimerCallback{asyncId, std::string(name)};
}; };
api["newTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId&, double delay, api["newTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId&, double delay,
@ -31,35 +31,34 @@ namespace MWLua
{ {
callback.mAsyncId.mContainer->setupSerializableTimer( callback.mAsyncId.mContainer->setupSerializableTimer(
TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, 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, api["newTimerInHours"] = [world=context.mWorldView](const AsyncPackageId&, double delay,
const TimerCallback& callback, sol::object callbackArg) const TimerCallback& callback, sol::object callbackArg)
{ {
callback.mAsyncId.mContainer->setupSerializableTimer( callback.mAsyncId.mContainer->setupSerializableTimer(
TimeUnit::HOURS, world->getGameTimeInHours() + delay, 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) api["newUnsavableTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback)
{ {
asyncId.mContainer->setupUnsavableTimer( 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) api["newUnsavableTimerInHours"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback)
{ {
asyncId.mContainer->setupUnsavableTimer( 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) 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) auto initializer = [](sol::table hiddenData)
{ {
LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY]; LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey];
hiddenData[Callback::SCRIPT_NAME_KEY] = id.toString(); return AsyncPackageId{id.mContainer, id.mIndex, hiddenData};
return AsyncPackageId{id.mContainer, id.mPath, hiddenData};
}; };
return sol::make_object(context.mLua->sol(), initializer); return sol::make_object(context.mLua->sol(), initializer);
} }

@ -16,7 +16,8 @@ namespace MWLua
class GlobalScripts : public LuaUtil::ScriptsContainer class GlobalScripts : public LuaUtil::ScriptsContainer
{ {
public: public:
GlobalScripts(LuaUtil::LuaState* lua) : LuaUtil::ScriptsContainer(lua, "Global") GlobalScripts(LuaUtil::LuaState* lua) :
LuaUtil::ScriptsContainer(lua, "Global", ESM::LuaScriptCfg::sGlobal)
{ {
registerEngineHandlers({&mActorActiveHandlers, &mNewGameHandlers, &mPlayerAddedHandlers}); registerEngineHandlers({&mActorActiveHandlers, &mNewGameHandlers, &mPlayerAddedHandlers});
} }

@ -82,14 +82,14 @@ namespace MWLua
}; };
} }
LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj) LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode)
: LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id())), mData(obj) : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id()), autoStartMode), mData(obj)
{ {
this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData)); this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData));
registerEngineHandlers({&mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers}); registerEngineHandlers({&mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers});
} }
void LocalScripts::receiveEngineEvent(const EngineEvent& event, ObjectRegistry*) void LocalScripts::receiveEngineEvent(const EngineEvent& event)
{ {
std::visit([this](auto&& arg) std::visit([this](auto&& arg)
{ {

@ -20,7 +20,7 @@ namespace MWLua
{ {
public: public:
static void initializeSelfPackage(const Context&); 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; } MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; }
@ -39,7 +39,7 @@ namespace MWLua
}; };
using EngineEvent = std::variant<OnActive, OnInactive, OnConsume>; using EngineEvent = std::variant<OnActive, OnInactive, OnConsume>;
void receiveEngineEvent(const EngineEvent&, ObjectRegistry*); void receiveEngineEvent(const EngineEvent&);
protected: protected:
SelfObject mData; SelfObject mData;

@ -25,7 +25,7 @@ namespace MWLua
{ {
auto* lua = context.mLua; auto* lua = context.mLua;
sol::table api(lua->sol(), sol::create); sol::table api(lua->sol(), sol::create);
api["API_REVISION"] = 7; api["API_REVISION"] = 8;
api["quit"] = [lua]() api["quit"] = [lua]()
{ {
std::string traceback = lua->sol()["debug"]["traceback"]().get<std::string>(); std::string traceback = lua->sol()["debug"]["traceback"]().get<std::string>();

@ -48,7 +48,7 @@ namespace MWLua
struct AsyncPackageId struct AsyncPackageId
{ {
LuaUtil::ScriptsContainer* mContainer; LuaUtil::ScriptsContainer* mContainer;
std::string mScript; int mScriptId;
sol::table mHiddenData; sol::table mHiddenData;
}; };
sol::function getAsyncPackageInitializer(const Context&); sol::function getAsyncPackageInitializer(const Context&);

@ -7,11 +7,11 @@
#include <components/esm/luascripts.hpp> #include <components/esm/luascripts.hpp>
#include <components/lua/utilpackage.hpp> #include <components/lua/utilpackage.hpp>
#include <components/lua/omwscriptsparser.hpp>
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwworld/ptr.hpp" #include "../mwworld/ptr.hpp"
#include "luabindings.hpp" #include "luabindings.hpp"
@ -20,10 +20,9 @@
namespace MWLua namespace MWLua
{ {
LuaManager::LuaManager(const VFS::Manager* vfs, const std::vector<std::string>& scriptLists) : mLua(vfs) LuaManager::LuaManager(const VFS::Manager* vfs) : mLua(vfs, &mConfiguration)
{ {
Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion(); Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion();
mGlobalScriptList = LuaUtil::parseOMWScriptsFiles(vfs, scriptLists);
mGlobalSerializer = createUserdataSerializer(false, mWorldView.getObjectRegistry()); mGlobalSerializer = createUserdataSerializer(false, mWorldView.getObjectRegistry());
mLocalSerializer = createUserdataSerializer(true, mWorldView.getObjectRegistry()); mLocalSerializer = createUserdataSerializer(true, mWorldView.getObjectRegistry());
@ -33,6 +32,14 @@ namespace MWLua
mGlobalScripts.setSerializer(mGlobalSerializer.get()); 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() void LuaManager::init()
{ {
Context context; Context context;
@ -67,23 +74,10 @@ namespace MWLua
mLocalSettingsPackage = initLocalSettingsPackage(localContext); mLocalSettingsPackage = initLocalSettingsPackage(localContext);
mPlayerSettingsPackage = initPlayerSettingsPackage(localContext); mPlayerSettingsPackage = initPlayerSettingsPackage(localContext);
mInputEvents.clear(); initConfiguration();
for (const std::string& path : mGlobalScriptList)
if (mGlobalScripts.addNewScript(path))
Log(Debug::Info) << "Global script started: " << path;
mInitialized = true; 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<std::string>(SCRIPT_NAME_KEY);
}
}
void LuaManager::update(bool paused, float dt) void LuaManager::update(bool paused, float dt)
{ {
ObjectRegistry* objectRegistry = mWorldView.getObjectRegistry(); ObjectRegistry* objectRegistry = mWorldView.getObjectRegistry();
@ -160,7 +154,7 @@ namespace MWLua
} }
LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts(); LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts();
if (scripts) if (scripts)
scripts->receiveEngineEvent(e.mEvent, objectRegistry); scripts->receiveEngineEvent(e.mEvent);
} }
mLocalEngineEvents.clear(); mLocalEngineEvents.clear();
@ -173,6 +167,11 @@ namespace MWLua
mPlayerChanged = false; mPlayerChanged = false;
mGlobalScripts.playerAdded(GObject(getId(mPlayer), objectRegistry)); mGlobalScripts.playerAdded(GObject(getId(mPlayer), objectRegistry));
} }
if (mNewGameStarted)
{
mNewGameStarted = false;
mGlobalScripts.newGameStarted();
}
for (ObjectId id : mActorAddedEvents) for (ObjectId id : mActorAddedEvents)
mGlobalScripts.actorActive(GObject(id, objectRegistry)); mGlobalScripts.actorActive(GObject(id, objectRegistry));
@ -205,8 +204,11 @@ namespace MWLua
mInputEvents.clear(); mInputEvents.clear();
mActorAddedEvents.clear(); mActorAddedEvents.clear();
mLocalEngineEvents.clear(); mLocalEngineEvents.clear();
mNewGameStarted = false;
mPlayerChanged = false; mPlayerChanged = false;
mWorldView.clear(); mWorldView.clear();
mGlobalScripts.removeAllScripts();
mGlobalScriptsStarted = false;
if (!mPlayer.isEmpty()) if (!mPlayer.isEmpty())
{ {
mPlayer.getCellRef().unsetRefNum(); mPlayer.getCellRef().unsetRefNum();
@ -225,17 +227,38 @@ namespace MWLua
mPlayer = ptr; mPlayer = ptr;
LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); LocalScripts* localScripts = ptr.getRefData().getLuaScripts();
if (!localScripts) if (!localScripts)
localScripts = createLocalScripts(ptr); localScripts = createLocalScripts(ptr, ESM::LuaScriptCfg::sPlayer);
mActiveLocalScripts.insert(localScripts); mActiveLocalScripts.insert(localScripts);
mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}}); mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}});
mPlayerChanged = true; 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) void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr)
{ {
mWorldView.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. mWorldView.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet.
LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); 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) if (localScripts)
{ {
mActiveLocalScripts.insert(localScripts); mActiveLocalScripts.insert(localScripts);
@ -281,26 +304,26 @@ namespace MWLua
return localScripts->getActorControls(); 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(); LocalScripts* localScripts = ptr.getRefData().getLuaScripts();
if (!localScripts) if (!localScripts)
{ {
localScripts = createLocalScripts(ptr); localScripts = createLocalScripts(ptr, getLuaScriptFlag(ptr));
if (ptr.isInCell() && MWBase::Environment::get().getWorld()->isCellActive(ptr.getCell())) if (ptr.isInCell() && MWBase::Environment::get().getWorld()->isCellActive(ptr.getCell()))
mActiveLocalScripts.insert(localScripts); 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(mInitialized);
assert(flag != ESM::LuaScriptCfg::sGlobal);
std::shared_ptr<LocalScripts> scripts; std::shared_ptr<LocalScripts> scripts;
// When loading a game, it can be called before LuaManager::setPlayer, if (flag == ESM::LuaScriptCfg::sPlayer)
// so we can't just check ptr == mPlayer here.
if (ptr.getCellRef().getRefIdRef() == "player")
{ {
assert(ptr.getCellRef().getRefIdRef() == "player");
scripts = std::make_shared<PlayerScripts>(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); scripts = std::make_shared<PlayerScripts>(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()));
scripts->addPackage("openmw.ui", mUserInterfacePackage); scripts->addPackage("openmw.ui", mUserInterfacePackage);
scripts->addPackage("openmw.camera", mCameraPackage); scripts->addPackage("openmw.camera", mCameraPackage);
@ -309,11 +332,12 @@ namespace MWLua
} }
else else
{ {
scripts = std::make_shared<LocalScripts>(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); scripts = std::make_shared<LocalScripts>(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()), flag);
scripts->addPackage("openmw.settings", mLocalSettingsPackage); scripts->addPackage("openmw.settings", mLocalSettingsPackage);
} }
scripts->addPackage("openmw.nearby", mNearbyPackage); scripts->addPackage("openmw.nearby", mNearbyPackage);
scripts->setSerializer(mLocalSerializer.get()); scripts->setSerializer(mLocalSerializer.get());
scripts->addAutoStartedScripts();
MWWorld::RefData& refData = ptr.getRefData(); MWWorld::RefData& refData = ptr.getRefData();
refData.setLuaScripts(std::move(scripts)); refData.setLuaScripts(std::move(scripts));
@ -344,8 +368,9 @@ namespace MWLua
loadEvents(mLua.sol(), reader, mGlobalEvents, mLocalEvents, mContentFileMapping, mGlobalLoader.get()); loadEvents(mLua.sol(), reader, mGlobalEvents, mLocalEvents, mContentFileMapping, mGlobalLoader.get());
mGlobalScripts.setSerializer(mGlobalLoader.get()); mGlobalScripts.setSerializer(mGlobalLoader.get());
mGlobalScripts.load(globalScripts, false); mGlobalScripts.load(globalScripts);
mGlobalScripts.setSerializer(mGlobalSerializer.get()); mGlobalScripts.setSerializer(mGlobalSerializer.get());
mGlobalScriptsStarted = true;
} }
void LuaManager::saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) void LuaManager::saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data)
@ -366,10 +391,10 @@ namespace MWLua
} }
mWorldView.getObjectRegistry()->registerPtr(ptr); mWorldView.getObjectRegistry()->registerPtr(ptr);
LocalScripts* scripts = createLocalScripts(ptr); LocalScripts* scripts = createLocalScripts(ptr, getLuaScriptFlag(ptr));
scripts->setSerializer(mLocalLoader.get()); scripts->setSerializer(mLocalLoader.get());
scripts->load(data, true); scripts->load(data);
scripts->setSerializer(mLocalSerializer.get()); scripts->setSerializer(mLocalSerializer.get());
// LiveCellRef is usually copied after loading, so this Ptr will become invalid and should be deregistered. // 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"; Log(Debug::Info) << "Reload Lua";
mLua.dropScriptCache(); mLua.dropScriptCache();
initConfiguration();
{ // Reload global scripts { // Reload global scripts
ESM::LuaScripts data; ESM::LuaScripts data;
mGlobalScripts.save(data); mGlobalScripts.save(data);
mGlobalScripts.removeAllScripts(); mGlobalScripts.load(data);
for (const std::string& path : mGlobalScriptList)
if (mGlobalScripts.addNewScript(path))
Log(Debug::Info) << "Global script restarted: " << path;
mGlobalScripts.load(data, false);
} }
for (const auto& [id, ptr] : mWorldView.getObjectRegistry()->mObjectMapping) for (const auto& [id, ptr] : mWorldView.getObjectRegistry()->mObjectMapping)
@ -398,8 +420,10 @@ namespace MWLua
continue; continue;
ESM::LuaScripts data; ESM::LuaScripts data;
scripts->save(data); scripts->save(data);
scripts->load(data, true); scripts->load(data);
} }
for (LocalScripts* scripts : mActiveLocalScripts)
scripts->receiveEngineEvent(LocalScripts::OnActive());
} }
} }

@ -19,25 +19,12 @@
namespace MWLua 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 class LuaManager : public MWBase::LuaManager
{ {
public: public:
LuaManager(const VFS::Manager* vfs, const std::vector<std::string>& 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(); void init();
// Called by engine.cpp every frame. For performance reasons it works in a separate // 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. // Available everywhere through the MWBase::LuaManager interface.
// LuaManager queues these events and propagates to scripts on the next `update` call. // 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 objectAddedToScene(const MWWorld::Ptr& ptr) override;
void objectRemovedFromScene(const MWWorld::Ptr& ptr) override; void objectRemovedFromScene(const MWWorld::Ptr& ptr) override;
void registerObject(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 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". void setupPlayer(const MWWorld::Ptr& ptr) override; // Should be called once after each "clear".
// Used only in luabindings // Used only in Lua bindings
void addLocalScript(const MWWorld::Ptr&, const std::string& scriptPath); void addCustomLocalScript(const MWWorld::Ptr&, int scriptId);
void addAction(std::unique_ptr<Action>&& action) { mActionQueue.push_back(std::move(action)); } void addAction(std::unique_ptr<Action>&& action) { mActionQueue.push_back(std::move(action)); }
void addTeleportPlayerAction(std::unique_ptr<TeleportAction>&& action) { mTeleportPlayerAction = std::move(action); } void addTeleportPlayerAction(std::unique_ptr<TeleportAction>&& action) { mTeleportPlayerAction = std::move(action); }
void addUIMessage(std::string_view message) { mUIMessages.emplace_back(message); } void addUIMessage(std::string_view message) { mUIMessages.emplace_back(message); }
@ -81,21 +69,27 @@ namespace MWLua
void reloadAllScripts() override; void reloadAllScripts() override;
// Used to call Lua callbacks from C++ // 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. // Wraps Lua callback into an std::function.
// NOTE: Resulted function is not thread safe. Can not be used while LuaManager::update() or // NOTE: Resulted function is not thread safe. Can not be used while LuaManager::update() or
// any other Lua-related function is running. // any other Lua-related function is running.
template <class Arg> template <class Arg>
std::function<void(Arg)> wrapLuaCallback(const Callback& c) std::function<void(Arg)> wrapLuaCallback(const LuaUtil::Callback& c)
{ {
return [this, c](Arg arg) { this->queueCallback(c, sol::make_object(c.mFunc.lua_state(), arg)); }; return [this, c](Arg arg) { this->queueCallback(c, sol::make_object(c.mFunc.lua_state(), arg)); };
} }
private: private:
LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr); void initConfiguration();
LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags);
bool mInitialized = false; bool mInitialized = false;
bool mGlobalScriptsStarted = false;
LuaUtil::ScriptsConfiguration mConfiguration;
LuaUtil::LuaState mLua; LuaUtil::LuaState mLua;
sol::table mNearbyPackage; sol::table mNearbyPackage;
sol::table mUserInterfacePackage; sol::table mUserInterfacePackage;
@ -104,12 +98,12 @@ namespace MWLua
sol::table mLocalSettingsPackage; sol::table mLocalSettingsPackage;
sol::table mPlayerSettingsPackage; sol::table mPlayerSettingsPackage;
std::vector<std::string> mGlobalScriptList;
GlobalScripts mGlobalScripts{&mLua}; GlobalScripts mGlobalScripts{&mLua};
std::set<LocalScripts*> mActiveLocalScripts; std::set<LocalScripts*> mActiveLocalScripts;
WorldView mWorldView; WorldView mWorldView;
bool mPlayerChanged = false; bool mPlayerChanged = false;
bool mNewGameStarted = false;
MWWorld::Ptr mPlayer; MWWorld::Ptr mPlayer;
GlobalEventQueue mGlobalEvents; GlobalEventQueue mGlobalEvents;
@ -127,7 +121,7 @@ namespace MWLua
struct CallbackWithData struct CallbackWithData
{ {
Callback mCallback; LuaUtil::Callback mCallback;
sol::object mArg; sol::object mArg;
}; };
std::vector<CallbackWithData> mQueuedCallbacks; std::vector<CallbackWithData> mQueuedCallbacks;

@ -1,19 +1,6 @@
#include "object.hpp" #include "object.hpp"
#include "../mwclass/activator.hpp" #include <unordered_map>
#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"
namespace MWLua namespace MWLua
{ {
@ -23,28 +10,34 @@ namespace MWLua
return std::to_string(id.mIndex) + "_" + std::to_string(id.mContentFile); return std::to_string(id.mIndex) + "_" + std::to_string(id.mContentFile);
} }
const static std::map<std::type_index, std::string_view> classNames = { struct LuaObjectTypeInfo
{typeid(MWClass::Activator), "Activator"}, {
{typeid(MWClass::Armor), "Armor"}, std::string_view mName;
{typeid(MWClass::Book), "Book"}, ESM::LuaScriptCfg::Flags mFlag = 0;
{typeid(MWClass::Clothing), "Clothing"}, };
{typeid(MWClass::Container), "Container"},
{typeid(MWClass::Creature), "Creature"}, const static std::unordered_map<ESM::RecNameInts, LuaObjectTypeInfo> luaObjectTypeInfo = {
{typeid(MWClass::Door), "Door"}, {ESM::REC_ACTI, {"Activator", ESM::LuaScriptCfg::sActivator}},
{typeid(MWClass::Ingredient), "Ingredient"}, {ESM::REC_ARMO, {"Armor", ESM::LuaScriptCfg::sArmor}},
{typeid(MWClass::Light), "Light"}, {ESM::REC_BOOK, {"Book", ESM::LuaScriptCfg::sBook}},
{typeid(MWClass::Miscellaneous), "Miscellaneous"}, {ESM::REC_CLOT, {"Clothing", ESM::LuaScriptCfg::sClothing}},
{typeid(MWClass::Npc), "NPC"}, {ESM::REC_CONT, {"Container", ESM::LuaScriptCfg::sContainer}},
{typeid(MWClass::Potion), "Potion"}, {ESM::REC_CREA, {"Creature", ESM::LuaScriptCfg::sCreature}},
{typeid(MWClass::Static), "Static"}, {ESM::REC_DOOR, {"Door", ESM::LuaScriptCfg::sDoor}},
{typeid(MWClass::Weapon), "Weapon"}, {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 getMWClassName(const std::type_index& cls_type, std::string_view fallback) std::string_view getLuaObjectTypeName(ESM::RecNameInts type, std::string_view fallback)
{ {
auto it = classNames.find(cls_type); auto it = luaObjectTypeInfo.find(type);
if (it != classNames.end()) if (it != luaObjectTypeInfo.end())
return it->second; return it->second.mName;
else else
return fallback; return fallback;
} }
@ -55,13 +48,31 @@ namespace MWLua
return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker"; 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") if (ptr.getCellRef().getRefIdRef() == "player")
return "Player"; return "Player";
if (isMarker(ptr)) if (isMarker(ptr))
return "Marker"; return "Marker";
return getMWClassName(typeid(ptr.getClass())); return getLuaObjectTypeName(static_cast<ESM::RecNameInts>(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<ESM::RecNameInts>(ptr.getType()));
if (it != luaObjectTypeInfo.end())
return it->second.mFlag;
else
return 0;
} }
std::string ptrToString(const MWWorld::Ptr& ptr) std::string ptrToString(const MWWorld::Ptr& ptr)
@ -69,7 +80,7 @@ namespace MWLua
std::string res = "object"; std::string res = "object";
res.append(idToString(getId(ptr))); res.append(idToString(getId(ptr)));
res.append(" ("); res.append(" (");
res.append(getMWClassName(ptr)); res.append(getLuaObjectTypeName(ptr));
res.append(", "); res.append(", ");
res.append(ptr.getCellRef().getRefIdRef()); res.append(ptr.getCellRef().getRefIdRef());
res.append(")"); res.append(")");

@ -4,6 +4,8 @@
#include <typeindex> #include <typeindex>
#include <components/esm/cellref.hpp> #include <components/esm/cellref.hpp>
#include <components/esm/defs.hpp>
#include <components/esm/luascripts.hpp>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
@ -19,8 +21,12 @@ namespace MWLua
std::string idToString(const ObjectId& id); std::string idToString(const ObjectId& id);
std::string ptrToString(const MWWorld::Ptr& ptr); std::string ptrToString(const MWWorld::Ptr& ptr);
bool isMarker(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 getLuaObjectTypeName(ESM::RecNameInts recordType, std::string_view fallback = "Unknown");
std::string_view getMWClassName(const MWWorld::Ptr& ptr); 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. // Holds a mapping ObjectId -> MWWord::Ptr.
class ObjectRegistry class ObjectRegistry
@ -64,7 +70,7 @@ namespace MWLua
ObjectId id() const { return mId; } ObjectId id() const { return mId; }
std::string toString() const; 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. // Updates and returns the underlying Ptr. Throws an exception if object is not available.
const MWWorld::Ptr& ptr() const; const MWWorld::Ptr& ptr() const;

@ -42,13 +42,12 @@ namespace MWLua
template <typename ObjT> template <typename ObjT>
using Cell = std::conditional_t<std::is_same_v<ObjT, LObject>, LCell, GCell>; using Cell = std::conditional_t<std::is_same_v<ObjT, LObject>, LCell, GCell>;
template <class Class> static const MWWorld::Ptr& requireRecord(ESM::RecNameInts recordType, const MWWorld::Ptr& ptr)
static const MWWorld::Ptr& requireClass(const MWWorld::Ptr& ptr)
{ {
if (typeid(Class) != typeid(ptr.getClass())) if (ptr.getType() != recordType)
{ {
std::string msg = "Requires type '"; std::string msg = "Requires type '";
msg.append(getMWClassName(typeid(Class))); msg.append(getLuaObjectTypeName(recordType));
msg.append("', but applied to "); msg.append("', but applied to ");
msg.append(ptrToString(ptr)); msg.append(ptrToString(ptr));
throw std::runtime_error(msg); throw std::runtime_error(msg);
@ -141,9 +140,43 @@ namespace MWLua
if constexpr (std::is_same_v<ObjectT, GObject>) if constexpr (std::is_same_v<ObjectT, GObject>)
{ // Only for global scripts { // 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<int> 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<int> 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<int> 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, objectT["teleport"] = [luaManager=context.mLuaManager](const GObject& object, std::string_view cell,
@ -189,7 +222,7 @@ namespace MWLua
template <class ObjectT> template <class ObjectT>
static void addDoorBindings(sol::usertype<ObjectT>& objectT, const Context& context) static void addDoorBindings(sol::usertype<ObjectT>& objectT, const Context& context)
{ {
auto ptr = [](const ObjectT& o) -> const MWWorld::Ptr& { return requireClass<MWClass::Door>(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) objectT["isTeleport"] = sol::readonly_property([ptr](const ObjectT& o)
{ {

@ -13,7 +13,7 @@ namespace MWLua
class PlayerScripts : public LocalScripts class PlayerScripts : public LocalScripts
{ {
public: 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, registerEngineHandlers({&mKeyPressHandlers, &mKeyReleaseHandlers,
&mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, &mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers,

@ -1,5 +1,7 @@
#include "activespells.hpp" #include "activespells.hpp"
#include <optional>
#include <components/debug/debuglog.hpp> #include <components/debug/debuglog.hpp>
#include <components/misc/rng.hpp> #include <components/misc/rng.hpp>
@ -14,6 +16,8 @@
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwrender/animation.hpp"
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/inventorystore.hpp" #include "../mwworld/inventorystore.hpp"
@ -96,6 +100,11 @@ namespace MWMechanics
, mType(params.mType), mWorsenings(params.mWorsenings), mNextWorsening({params.mNextWorsening}) , 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 ActiveSpells::ActiveSpellParams::toEsm() const
{ {
ESM::ActiveSpells::ActiveSpellParams params; 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? const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spellIt->mCasterActorId); //Maybe make this search outside active grid?
bool removedSpell = false; bool removedSpell = false;
std::optional<ActiveSpellParams> reflected;
for(auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) for(auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();)
{ {
bool remove = applyMagicEffect(ptr, caster, *spellIt, *it, duration); auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration);
if(remove) 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); it = spellIt->mEffects.erase(it);
else else
++it; ++it;
@ -231,6 +249,14 @@ namespace MWMechanics
if(removedSpell) if(removedSpell)
break; break;
} }
if(reflected)
{
const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().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) if(removedSpell)
continue; continue;

@ -50,6 +50,8 @@ namespace MWMechanics
ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor); 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; ESM::ActiveSpells::ActiveSpellParams toEsm() const;
friend class ActiveSpells; friend class ActiveSpells;

@ -240,10 +240,7 @@ namespace MWMechanics
{ {
// magic effects // magic effects
adjustMagicEffects (ptr, duration); adjustMagicEffects (ptr, duration);
if (ptr.getClass().getCreatureStats(ptr).needToRecalcDynamicStats())
calculateDynamicStats (ptr);
calculateCreatureStatModifiers (ptr, duration);
// fatigue restoration // fatigue restoration
calculateRestoration(ptr, duration); calculateRestoration(ptr, duration);
} }
@ -654,29 +651,6 @@ namespace MWMechanics
updateSummons(creature, mTimerDisposeSummonsCorpses == 0.f); 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<ESM::GameSetting>().find("fPCbaseMagickaMult")->mValue.getFloat();
else
base = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fNPCbaseMagickaMult")->mValue.getFloat();
double magickaFactor = base +
creatureStats.getMagicEffects().get (EffectKey (ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1;
DynamicStat<float> magicka = creatureStats.getMagicka();
float diff = (static_cast<int>(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) void Actors::restoreDynamicStats (const MWWorld::Ptr& ptr, double hours, bool sleep)
{ {
MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr);
@ -771,14 +745,6 @@ namespace MWMechanics
stats.setFatigue (fatigue); 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) bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr)
{ {
PtrActorMap::iterator it = mActors.find(ptr); PtrActorMap::iterator it = mActors.find(ptr);
@ -1047,13 +1013,10 @@ namespace MWMechanics
void Actors::updateProcessingRange() 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) // 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 maxRange = 7168.f;
static const float minProcessingRange = maxProcessingRange / 2.f; static const float minRange = maxRange / 2.f;
float actorsProcessingRange = Settings::Manager::getFloat("actors processing range", "Game"); mActorsProcessingRange = std::clamp(Settings::Manager::getFloat("actors processing range", "Game"), minRange, maxRange);
actorsProcessingRange = std::min(actorsProcessingRange, maxProcessingRange);
actorsProcessingRange = std::max(actorsProcessingRange, minProcessingRange);
mActorsProcessingRange = actorsProcessingRange;
} }
void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately) void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately)
@ -1349,7 +1312,7 @@ namespace MWMechanics
angleToApproachingActor = std::atan2(deltaPos.x(), deltaPos.y()); angleToApproachingActor = std::atan2(deltaPos.x(), deltaPos.y());
osg::Vec2f posAtT = relPos + relSpeed * t; osg::Vec2f posAtT = relPos + relSpeed * t;
float coef = (posAtT.x() * relSpeed.x() + posAtT.y() * relSpeed.y()) / (collisionDist * collisionDist * maxSpeed); 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; movementCorrection = posAtT * coef;
if (otherPtr.getClass().getCreatureStats(otherPtr).isDead()) if (otherPtr.getClass().getCreatureStats(otherPtr).isDead())
// In case of dead body still try to go around (it looks natural), but reduce the correction twice. // 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); 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 // 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);
updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer);
if (timerUpdateEquippedLight == 0)
updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches);
} }
if(timerUpdateEquippedLight == 0 && iter->first.getClass().hasInventoryStore(iter->first))
updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches);
if (luaControls && isConscious(iter->first)) if (luaControls && isConscious(iter->first))
{ {
@ -1711,10 +1672,6 @@ namespace MWMechanics
if (iter->first.getType() == ESM::Creature::sRecordId) if (iter->first.getType() == ESM::Creature::sRecordId)
soulTrap(iter->first); 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)) if (cls.isEssential(iter->first))
MWBase::Environment::get().getWindowManager()->messageBox("#{sKilledEssential}"); MWBase::Environment::get().getWindowManager()->messageBox("#{sKilledEssential}");
} }
@ -1730,8 +1687,6 @@ namespace MWMechanics
// Make sure spell effects are removed // Make sure spell effects are removed
purgeSpellEffects(stats.getActorId()); purgeSpellEffects(stats.getActorId());
// Reset dynamic stats, attributes and skills
calculateCreatureStatModifiers(iter->first, 0);
stats.getMagicEffects().add(ESM::MagicEffect::Vampirism, vampirism); stats.getMagicEffects().add(ESM::MagicEffect::Vampirism, vampirism);
if (isPlayer) if (isPlayer)
@ -1816,10 +1771,6 @@ namespace MWMechanics
continue; continue;
adjustMagicEffects (iter->first, duration); 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); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(iter->first);
if (animation) if (animation)
@ -2209,7 +2160,6 @@ namespace MWMechanics
void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) void Actors::updateMagicEffects(const MWWorld::Ptr &ptr)
{ {
adjustMagicEffects(ptr, 0.f); adjustMagicEffects(ptr, 0.f);
calculateCreatureStatModifiers(ptr, 0.f);
} }
bool Actors::isReadyToBlock(const MWWorld::Ptr &ptr) const bool Actors::isReadyToBlock(const MWWorld::Ptr &ptr) const

@ -43,10 +43,6 @@ namespace MWMechanics
void adjustMagicEffects (const MWWorld::Ptr& creature, float duration); 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 calculateRestoration (const MWWorld::Ptr& ptr, float duration);
void updateDrowning (const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer); void updateDrowning (const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer);

@ -29,4 +29,12 @@ namespace MWMechanics
const MWMechanics::MagicEffects& effects = actor.getClass().getCreatureStats(actor).getMagicEffects(); const MWMechanics::MagicEffects& effects = actor.getClass().getCreatureStats(actor).getMagicEffects();
return effects.get(ESM::MagicEffect::WaterWalking).getMagnitude() > 0; return effects.get(ESM::MagicEffect::WaterWalking).getMagnitude() > 0;
} }
CreatureCustomDataResetter::CreatureCustomDataResetter(const MWWorld::Ptr& ptr) : mPtr(ptr) {}
CreatureCustomDataResetter::~CreatureCustomDataResetter()
{
if(!mPtr.isEmpty())
mPtr.getRefData().setCustomData({});
}
} }

@ -86,6 +86,14 @@ namespace MWMechanics
template void modifyBaseInventory<ESM::Creature>(const std::string& actorId, const std::string& itemId, int amount); template void modifyBaseInventory<ESM::Creature>(const std::string& actorId, const std::string& itemId, int amount);
template void modifyBaseInventory<ESM::NPC>(const std::string& actorId, const std::string& itemId, int amount); template void modifyBaseInventory<ESM::NPC>(const std::string& actorId, const std::string& itemId, int amount);
template void modifyBaseInventory<ESM::Container>(const std::string& containerId, const std::string& itemId, int amount); template void modifyBaseInventory<ESM::Container>(const std::string& containerId, const std::string& itemId, int amount);
struct CreatureCustomDataResetter
{
MWWorld::Ptr mPtr;
CreatureCustomDataResetter(const MWWorld::Ptr& ptr);
~CreatureCustomDataResetter();
};
} }
#endif #endif

@ -2044,7 +2044,7 @@ void CharacterController::update(float duration)
mIsMovingBackward = vec.y() < 0; mIsMovingBackward = vec.y() < 0;
float maxDelta = osg::PI * duration * (2.5f - cosDelta); 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); stats.setSideMovementAngle(stats.getSideMovementAngle() + delta);
effectiveRotation += delta; effectiveRotation += delta;
} }
@ -2286,7 +2286,7 @@ void CharacterController::update(float duration)
float swimmingPitch = mAnimation->getBodyPitchRadians(); float swimmingPitch = mAnimation->getBodyPitchRadians();
float targetSwimmingPitch = -mPtr.getRefData().getPosition().rot[0]; float targetSwimmingPitch = -mPtr.getRefData().getPosition().rot[0];
float maxSwimPitchDelta = 3.0f * duration; float maxSwimPitchDelta = 3.0f * duration;
swimmingPitch += osg::clampBetween(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta); swimmingPitch += std::clamp(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta);
mAnimation->setBodyPitchRadians(swimmingPitch); mAnimation->setBodyPitchRadians(swimmingPitch);
} }
else else
@ -2522,7 +2522,7 @@ void CharacterController::unpersistAnimationState()
{ {
float start = mAnimation->getTextKeyTime(anim.mGroup+": start"); float start = mAnimation->getTextKeyTime(anim.mGroup+": start");
float stop = mAnimation->getTextKeyTime(anim.mGroup+": stop"); 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); 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(); float chameleon = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude();
if (chameleon) 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); visibility = std::min(visibility, alpha);
@ -2965,8 +2965,8 @@ void CharacterController::updateHeadTracking(float duration)
const double xLimit = osg::DegreesToRadians(40.0); const double xLimit = osg::DegreesToRadians(40.0);
const double zLimit = osg::DegreesToRadians(30.0); const double zLimit = osg::DegreesToRadians(30.0);
double zLimitOffset = mAnimation->getUpperBodyYawRadians(); double zLimitOffset = mAnimation->getUpperBodyYawRadians();
xAngleRadians = osg::clampBetween(xAngleRadians, -xLimit, xLimit); xAngleRadians = std::clamp(xAngleRadians, -xLimit, xLimit);
zAngleRadians = osg::clampBetween(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset); zAngleRadians = std::clamp(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset);
float factor = duration*5; float factor = duration*5;
factor = std::min(factor, 1.f); factor = std::min(factor, 1.f);

@ -113,10 +113,9 @@ namespace MWMechanics
+ 0.1f * attackerStats.getAttribute(ESM::Attribute::Luck).getModified(); + 0.1f * attackerStats.getAttribute(ESM::Attribute::Luck).getModified();
attackerTerm *= attackerStats.getFatigueTerm(); attackerTerm *= attackerStats.getFatigueTerm();
int x = int(blockerTerm - attackerTerm); const int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger();
int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger(); const int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger();
int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger(); int x = std::clamp<int>(blockerTerm - attackerTerm, iBlockMinChance, iBlockMaxChance);
x = std::min(iBlockMaxChance, std::max(iBlockMinChance, x));
if (Misc::Rng::roll0to99() < x) if (Misc::Rng::roll0to99() < x)
{ {

@ -7,6 +7,7 @@
#include <components/esm/esmreader.hpp> #include <components/esm/esmreader.hpp>
#include <components/esm/esmwriter.hpp> #include <components/esm/esmwriter.hpp>
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
#include "../mwworld/player.hpp" #include "../mwworld/player.hpp"
@ -22,7 +23,7 @@ namespace MWMechanics
mTalkedTo (false), mAlarmed (false), mAttacked (false), mTalkedTo (false), mAlarmed (false), mAttacked (false),
mKnockdown(false), mKnockdownOneFrame(false), mKnockdownOverOneFrame(false), mKnockdown(false), mKnockdownOneFrame(false), mKnockdownOverOneFrame(false),
mHitRecovery(false), mBlock(false), mMovementFlags(0), 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) mDeathAnimation(-1), mTimeOfDeath(), mSideMovementAngle(0), mLevel (0)
{ {
for (int i=0; i<4; ++i) for (int i=0; i<4; ++i)
@ -146,7 +147,7 @@ namespace MWMechanics
mAttributes[index] = value; mAttributes[index] = value;
if (index == ESM::Attribute::Intelligence) if (index == ESM::Attribute::Intelligence)
mRecalcMagicka = true; recalculateMagicka();
else if (index == ESM::Attribute::Strength || else if (index == ESM::Attribute::Strength ||
index == ESM::Attribute::Willpower || index == ESM::Attribute::Willpower ||
index == ESM::Attribute::Agility || index == ESM::Attribute::Agility ||
@ -208,11 +209,10 @@ namespace MWMechanics
void CreatureStats::modifyMagicEffects(const MagicEffects &effects) void CreatureStats::modifyMagicEffects(const MagicEffects &effects)
{ {
if (effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() bool recalc = effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier();
!= mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier())
mRecalcMagicka = true;
mMagicEffects.setModifiers(effects); mMagicEffects.setModifiers(effects);
if(recalc)
recalculateMagicka();
} }
void CreatureStats::setAiSetting (AiSetting index, Stat<int> value) void CreatureStats::setAiSetting (AiSetting index, Stat<int> value)
@ -400,19 +400,26 @@ namespace MWMechanics
return height; return height;
} }
bool CreatureStats::needToRecalcDynamicStats() void CreatureStats::recalculateMagicka()
{ {
if (mRecalcMagicka) auto world = MWBase::Environment::get().getWorld();
{ float intelligence = getAttribute(ESM::Attribute::Intelligence).getModified();
mRecalcMagicka = false;
return true;
}
return false;
}
void CreatureStats::setNeedRecalcDynamicStats(bool val) float base = 1.f;
{ const auto& player = world->getPlayerPtr();
mRecalcMagicka = val; if (this == &player.getClass().getCreatureStats(player))
base = world->getStore().get<ESM::GameSetting>().find("fPCbaseMagickaMult")->mValue.getFloat();
else
base = world->getStore().get<ESM::GameSetting>().find("fNPCbaseMagickaMult")->mValue.getFloat();
double magickaFactor = base + mMagicEffects.get(EffectKey(ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1;
DynamicStat<float> magicka = getMagicka();
float diff = (static_cast<int>(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) void CreatureStats::setKnockedDown(bool value)
@ -532,7 +539,7 @@ namespace MWMechanics
state.mFallHeight = mFallHeight; // TODO: vertical velocity (move from PhysicActor to CreatureStats?) state.mFallHeight = mFallHeight; // TODO: vertical velocity (move from PhysicActor to CreatureStats?)
state.mLastHitObject = mLastHitObject; state.mLastHitObject = mLastHitObject;
state.mLastHitAttemptObject = mLastHitAttemptObject; state.mLastHitAttemptObject = mLastHitAttemptObject;
state.mRecalcDynamicStats = mRecalcMagicka; state.mRecalcDynamicStats = false;
state.mDrawState = mDrawState; state.mDrawState = mDrawState;
state.mLevel = mLevel; state.mLevel = mLevel;
state.mActorId = mActorId; state.mActorId = mActorId;
@ -586,7 +593,6 @@ namespace MWMechanics
mFallHeight = state.mFallHeight; mFallHeight = state.mFallHeight;
mLastHitObject = state.mLastHitObject; mLastHitObject = state.mLastHitObject;
mLastHitAttemptObject = state.mLastHitAttemptObject; mLastHitAttemptObject = state.mLastHitAttemptObject;
mRecalcMagicka = state.mRecalcDynamicStats;
mDrawState = DrawState_(state.mDrawState); mDrawState = DrawState_(state.mDrawState);
mLevel = state.mLevel; mLevel = state.mLevel;
mActorId = state.mActorId; mActorId = state.mActorId;
@ -627,6 +633,8 @@ namespace MWMechanics
if (state.mHasAiSettings) if (state.mHasAiSettings)
for (int i=0; i<4; ++i) for (int i=0; i<4; ++i)
mAiSettings[i].readState(state.mAiSettings[i]); mAiSettings[i].readState(state.mAiSettings[i]);
if(state.mRecalcDynamicStats)
recalculateMagicka();
} }
void CreatureStats::setLastRestockTime(MWWorld::TimeStamp tradeTime) void CreatureStats::setLastRestockTime(MWWorld::TimeStamp tradeTime)

@ -65,8 +65,6 @@ namespace MWMechanics
std::string mLastHitObject; // The last object to hit this actor std::string mLastHitObject; // The last object to hit this actor
std::string mLastHitAttemptObject; // The last object to attempt 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. // For merchants: the last time items were restocked and gold pool refilled.
MWWorld::TimeStamp mLastRestock; MWWorld::TimeStamp mLastRestock;
@ -103,8 +101,7 @@ namespace MWMechanics
DrawState_ getDrawState() const; DrawState_ getDrawState() const;
void setDrawState(DrawState_ state); void setDrawState(DrawState_ state);
bool needToRecalcDynamicStats(); void recalculateMagicka();
void setNeedRecalcDynamicStats(bool val);
float getFallHeight() const; float getFallHeight() const;
void addToFallHeight(float height); void addToFallHeight(float height);

@ -13,9 +13,7 @@ float scaleDamage(float damage, const MWWorld::Ptr& attacker, const MWWorld::Ptr
const MWWorld::Ptr& player = MWMechanics::getPlayer(); const MWWorld::Ptr& player = MWMechanics::getPlayer();
// [-500, 500] // [-500, 500]
int difficultySetting = Settings::Manager::getInt("difficulty", "Game"); const int difficultySetting = std::clamp(Settings::Manager::getInt("difficulty", "Game"), -500, 500);
difficultySetting = std::min(difficultySetting, 500);
difficultySetting = std::max(difficultySetting, -500);
static const float fDifficultyMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fDifficultyMult")->mValue.getFloat(); static const float fDifficultyMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fDifficultyMult")->mValue.getFloat();

@ -356,10 +356,10 @@ namespace MWMechanics
ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass;
if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo) 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(); MWWorld::Ptr player = getPlayer();
int itemsInInventoryCount = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId()); count = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId());
count = std::min(itemsInInventoryCount, std::max(1, int(getGemCharge() * multiplier / enchantPoints))); count = std::clamp<int>(getGemCharge() * multiplier / enchantPoints, 1, count);
} }
} }

@ -1,10 +1,20 @@
#include "magiceffects.hpp" #include "magiceffects.hpp"
#include <cmath>
#include <stdexcept> #include <stdexcept>
#include <components/esm/effectlist.hpp> #include <components/esm/effectlist.hpp>
#include <components/esm/magiceffects.hpp> #include <components/esm/magiceffects.hpp>
namespace
{
// Round value to prevent precision issues
void truncate(float& value)
{
value = std::roundf(value * 1024.f) / 1024.f;
}
}
namespace MWMechanics namespace MWMechanics
{ {
EffectKey::EffectKey() : mId (0), mArg (-1) {} EffectKey::EffectKey() : mId (0), mArg (-1) {}
@ -74,6 +84,7 @@ namespace MWMechanics
{ {
mModifier += param.mModifier; mModifier += param.mModifier;
mBase += param.mBase; mBase += param.mBase;
truncate(mModifier);
return *this; return *this;
} }
@ -81,6 +92,7 @@ namespace MWMechanics
{ {
mModifier -= param.mModifier; mModifier -= param.mModifier;
mBase -= param.mBase; mBase -= param.mBase;
truncate(mModifier);
return *this; return *this;
} }

@ -77,7 +77,7 @@ namespace MWMechanics
MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr);
MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats (ptr); MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats (ptr);
npcStats.setNeedRecalcDynamicStats(true); npcStats.recalculateMagicka();
const ESM::NPC *player = ptr.get<ESM::NPC>()->mBase; const ESM::NPC *player = ptr.get<ESM::NPC>()->mBase;
@ -222,7 +222,6 @@ namespace MWMechanics
// forced update and current value adjustments // forced update and current value adjustments
mActors.updateActor (ptr, 0); mActors.updateActor (ptr, 0);
mActors.updateActor (ptr, 0);
for (int i=0; i<3; ++i) for (int i=0; i<3; ++i)
{ {
@ -546,9 +545,9 @@ namespace MWMechanics
x += ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Charm).getMagnitude(); x += ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Charm).getMagnitude();
if(clamp) if (clamp)
return std::max(0,std::min(int(x),100));//, normally clamped to [0..100] when used return std::clamp<int>(x, 0, 100);//, normally clamped to [0..100] when used
return int(x); return static_cast<int>(x);
} }
int MechanicsManager::getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) 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 flee = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Flee).getBase();
int fight = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Fight).getBase(); int fight = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Fight).getBase();
npcStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, 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, 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)); float c = -std::abs(floor(r * fPerDieRollMult));
@ -690,10 +689,10 @@ namespace MWMechanics
float s = c * fPerDieRollMult * fPerTempMult; float s = c * fPerDieRollMult * fPerTempMult;
int flee = npcStats.getAiSetting (CreatureStats::AI_Flee).getBase(); int flee = npcStats.getAiSetting (CreatureStats::AI_Flee).getBase();
int fight = npcStats.getAiSetting (CreatureStats::AI_Fight).getBase(); int fight = npcStats.getAiSetting (CreatureStats::AI_Fight).getBase();
npcStats.setAiSetting (CreatureStats::AI_Flee, npcStats.setAiSetting(CreatureStats::AI_Flee,
std::max(0, std::min(100, flee + std::min(-int(iPerMinChange), int(-s))))); std::clamp(flee + std::min(-int(iPerMinChange), int(-s)), 0, 100));
npcStats.setAiSetting (CreatureStats::AI_Fight, npcStats.setAiSetting(CreatureStats::AI_Fight,
std::max(0, std::min(100, fight + std::max(int(iPerMinChange), int(s))))); std::clamp(fight + std::max(int(iPerMinChange), int(s)), 0, 100));
} }
x = floor(-c * fPerDieRollMult); x = floor(-c * fPerDieRollMult);

@ -371,7 +371,7 @@ int MWMechanics::NpcStats::getReputation() const
void MWMechanics::NpcStats::setReputation(int reputation) void MWMechanics::NpcStats::setReputation(int reputation)
{ {
// Reputation is capped in original engine // 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 int MWMechanics::NpcStats::getCrimeId() const

@ -1,82 +0,0 @@
#include "spellabsorption.hpp"
#include <components/misc/rng.hpp>
#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<int>(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<ESM::Static>().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<ESM::Spell>().search(spellId);
int spellCost = 0;
if (spell)
{
spellCost = MWMechanics::calcSpellCost(*spell);
}
else
{
const ESM::Enchantment* enchantment = esmStore.get<ESM::Enchantment>().search(spellId);
if (enchantment)
spellCost = getEffectiveEnchantmentCastCost(static_cast<float>(enchantment->mData.mCost), caster);
}
// Magicka is increased by the cost of the spell
DynamicStat<float> magicka = stats.getMagicka();
magicka.setCurrent(magicka.getCurrent() + spellCost);
stats.setMagicka(magicka);
return true;
}
}

@ -1,17 +0,0 @@
#ifndef MWMECHANICS_SPELLABSORPTION_H
#define MWMECHANICS_SPELLABSORPTION_H
#include <string>
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

@ -22,38 +22,11 @@
#include "actorutil.hpp" #include "actorutil.hpp"
#include "aifollow.hpp" #include "aifollow.hpp"
#include "creaturestats.hpp" #include "creaturestats.hpp"
#include "spellabsorption.hpp"
#include "spelleffects.hpp" #include "spelleffects.hpp"
#include "spellutil.hpp" #include "spellutil.hpp"
#include "summoning.hpp" #include "summoning.hpp"
#include "weapontype.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<ESM::Static>().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 namespace MWMechanics
{ {
CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool manualSpell) 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, 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(); const bool targetIsActor = !target.isEmpty() && target.getClass().isActor();
if (targetIsActor) if (targetIsActor)
@ -123,7 +96,6 @@ namespace MWMechanics
} }
} }
ESM::EffectList reflectedEffects;
ActiveSpells::ActiveSpellParams params(*this, caster); ActiveSpells::ActiveSpellParams params(*this, caster);
bool castByPlayer = (!caster.isEmpty() && caster == getPlayer()); bool castByPlayer = (!caster.isEmpty() && caster == getPlayer());
@ -136,9 +108,6 @@ namespace MWMechanics
// throughout the iteration of this spell's // throughout the iteration of this spell's
// effects, we display a "can't re-cast" message // 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; int currentEffectIndex = 0;
for (std::vector<ESM::ENAMstruct>::const_iterator effectIt (effects.mList.begin()); for (std::vector<ESM::ENAMstruct>::const_iterator effectIt (effects.mList.begin());
!target.isEmpty() && effectIt != effects.mList.end(); ++effectIt, ++currentEffectIndex) !target.isEmpty() && effectIt != effects.mList.end(); ++effectIt, ++currentEffectIndex)
@ -167,19 +136,6 @@ namespace MWMechanics
&& (caster.isEmpty() || !caster.getClass().isActor())) && (caster.isEmpty() || !caster.getClass().isActor()))
continue; 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; ActiveSpells::ActiveEffect effect;
effect.mEffectId = effectIt->mEffectID; effect.mEffectId = effectIt->mEffectID;
effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; effect.mArg = MWMechanics::EffectKey(*effectIt).mArg;
@ -189,13 +145,8 @@ namespace MWMechanics
effect.mTimeLeft = 0.f; effect.mTimeLeft = 0.f;
effect.mEffectIndex = currentEffectIndex; effect.mEffectIndex = currentEffectIndex;
effect.mFlags = ESM::ActiveEffect::Flag_None; effect.mFlags = ESM::ActiveEffect::Flag_None;
if(mManualSpell)
// Avoid applying harmful effects to the player in god mode effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect;
if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful)
{
effect.mMinMagnitude = 0;
effect.mMaxMagnitude = 0;
}
bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration);
effect.mDuration = hasDuration ? static_cast<float>(effectIt->mDuration) : 1.f; effect.mDuration = hasDuration ? static_cast<float>(effectIt->mDuration) : 1.f;
@ -209,14 +160,14 @@ namespace MWMechanics
// add to list of active effects, to apply in next frame // add to list of active effects, to apply in next frame
params.getEffects().emplace_back(effect); 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 (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. // 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); 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); playEffects(target, *magicEffect);
} }
@ -227,9 +178,6 @@ namespace MWMechanics
if (!target.isEmpty()) if (!target.isEmpty())
{ {
if (!reflectedEffects.mList.empty())
inflict(caster, target, reflectedEffects, range, true, exploded);
if (!params.getEffects().empty()) if (!params.getEffects().empty())
{ {
if(targetIsActor) if(targetIsActor)
@ -237,6 +185,7 @@ namespace MWMechanics
else else
{ {
// Apply effects instantly. We can ignore effect deletion since the entire params object gets deleted afterwards anyway // 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()) for(auto& effect : params.getEffects())
applyMagicEffect(target, caster, params, effect, 0.f); applyMagicEffect(target, caster, params, effect, 0.f);
} }

@ -62,7 +62,7 @@ namespace MWMechanics
/// @note \a target can be any type of object, not just actors. /// @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. /// @note \a caster can be any type of object, or even an empty object.
void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, 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); void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping = true);

@ -16,6 +16,7 @@
#include "../mwmechanics/aifollow.hpp" #include "../mwmechanics/aifollow.hpp"
#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/npcstats.hpp"
#include "../mwmechanics/spellresistance.hpp" #include "../mwmechanics/spellresistance.hpp"
#include "../mwmechanics/spellutil.hpp"
#include "../mwmechanics/summoning.hpp" #include "../mwmechanics/summoning.hpp"
#include "../mwrender/animation.hpp" #include "../mwrender/animation.hpp"
@ -261,6 +262,97 @@ namespace
return false; 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<ESM::Static>().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<ESM::Spell>().search(spellId);
int spellCost = 0;
if (spell)
{
spellCost = MWMechanics::calcSpellCost(*spell);
}
else
{
const ESM::Enchantment* enchantment = esmStore.get<ESM::Enchantment>().search(spellId);
if (enchantment)
spellCost = MWMechanics::getEffectiveEnchantmentCastCost(static_cast<float>(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<ESM::Spell>().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<int, std::string> sBoundItemsMap{ static const std::map<int, std::string> sBoundItemsMap{
{ESM::MagicEffect::BoundBattleAxe, "sMagicBoundBattleAxeID"}, {ESM::MagicEffect::BoundBattleAxe, "sMagicBoundBattleAxeID"},
{ESM::MagicEffect::BoundBoots, "sMagicBoundBootsID"}, {ESM::MagicEffect::BoundBoots, "sMagicBoundBootsID"},
@ -279,7 +371,7 @@ namespace
namespace MWMechanics 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(); const auto world = MWBase::Environment::get().getWorld();
bool godmode = target == getPlayer() && world->getGodModeState(); 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) if (!target.isInCell() || !target.getCell()->isExterior() || godmode)
break; break;
float time = world->getTimeStamp().getHour(); 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; float damageScale = 1.f - timeDiff / 7.f;
// When cloudy, the sun damage effect is halved // When cloudy, the sun damage effect is halved
static float fMagicSunBlockedMult = world->getStore().get<ESM::GameSetting>().find("fMagicSunBlockedMult")->mValue.getFloat(); static float fMagicSunBlockedMult = world->getStore().get<ESM::GameSetting>().find("fMagicSunBlockedMult")->mValue.getFloat();
@ -609,7 +701,7 @@ void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, co
fortifySkill(target, effect, effect.mMagnitude); fortifySkill(target, effect, effect.mMagnitude);
break; break;
case ESM::MagicEffect::FortifyMaximumMagicka: case ESM::MagicEffect::FortifyMaximumMagicka:
target.getClass().getCreatureStats(target).setNeedRecalcDynamicStats(true); recalculateMagicka = true;
break; break;
case ESM::MagicEffect::AbsorbHealth: case ESM::MagicEffect::AbsorbHealth:
case ESM::MagicEffect::AbsorbMagicka: 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(); const auto world = MWBase::Environment::get().getWorld();
bool invalid = false; bool invalid = false;
bool receivedMagicDamage = false; bool receivedMagicDamage = false;
bool recalculateMagicka = false;
if(effect.mEffectId == ESM::MagicEffect::Corprus && spellParams.shouldWorsen()) if(effect.mEffectId == ESM::MagicEffect::Corprus && spellParams.shouldWorsen())
{ {
spellParams.worsen(); spellParams.worsen();
for(auto& otherEffect : spellParams.getEffects()) for(auto& otherEffect : spellParams.getEffects())
{ {
if(isCorprusEffect(otherEffect)) if(isCorprusEffect(otherEffect))
applyMagicEffect(target, caster, spellParams, otherEffect, invalid, receivedMagicDamage); applyMagicEffect(target, caster, spellParams, otherEffect, invalid, receivedMagicDamage, recalculateMagicka);
} }
if(target == getPlayer()) if(target == getPlayer())
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}");
return false; return MagicApplicationResult::APPLIED;
} }
else if(effect.mEffectId == ESM::MagicEffect::Levitate && !world->isLevitationEnabled()) else if(effect.mEffectId == ESM::MagicEffect::Levitate && !world->isLevitationEnabled())
{ {
if(target == getPlayer()) if(target == getPlayer())
MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}"); MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}");
return true; return MagicApplicationResult::REMOVED;
} }
const auto* magicEffect = world->getStore().get<ESM::MagicEffect>().find(effect.mEffectId); const auto* magicEffect = world->getStore().get<ESM::MagicEffect>().find(effect.mEffectId);
if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) 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) if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce)
{ {
effect.mTimeLeft -= dt; effect.mTimeLeft -= dt;
return false; return MagicApplicationResult::APPLIED;
} }
else if(!dt) else if(!dt)
return false; return MagicApplicationResult::APPLIED;
} }
if(effect.mEffectId == ESM::MagicEffect::Lock) if(effect.mEffectId == ESM::MagicEffect::Lock)
{ {
@ -770,28 +863,19 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac
} }
else else
{ {
auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); auto& stats = target.getClass().getCreatureStats(target);
if(spellParams.getType() != ESM::ActiveSpells::Type_Ability && !(effect.mFlags & (ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Ignore_Resistances))) auto& magnitudes = stats.getMagicEffects();
if(spellParams.getType() != ESM::ActiveSpells::Type_Ability && !(effect.mFlags & ESM::ActiveEffect::Flag_Applied))
{ {
const ESM::Spell* spell = nullptr; MagicApplicationResult result = applyProtections(target, caster, spellParams, effect, magicEffect);
if(spellParams.getType() == ESM::ActiveSpells::Type_Temporary) if(result != MagicApplicationResult::APPLIED)
spell = world->getStore().get<ESM::Spell>().search(spellParams.getId()); return result;
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;
} }
float oldMagnitude = 0.f; float oldMagnitude = 0.f;
if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) if(effect.mFlags & ESM::ActiveEffect::Flag_Applied)
oldMagnitude = effect.mMagnitude; oldMagnitude = effect.mMagnitude;
else if(spellParams.getType() == ESM::ActiveSpells::Type_Consumable || spellParams.getType() == ESM::ActiveSpells::Type_Temporary)
playEffects(target, *magicEffect);
float magnitude = roll(effect); 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 //Note that there's an early out for Flag_Applied AppliedOnce effects so we don't have to exclude them here
effect.mMagnitude = magnitude; effect.mMagnitude = magnitude;
@ -809,13 +893,13 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac
effect.mMagnitude = oldMagnitude; effect.mMagnitude = oldMagnitude;
effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove;
effect.mTimeLeft -= dt; effect.mTimeLeft -= dt;
return false; return MagicApplicationResult::APPLIED;
} }
} }
if(effect.mEffectId == ESM::MagicEffect::Corprus) if(effect.mEffectId == ESM::MagicEffect::Corprus)
spellParams.worsen(); spellParams.worsen();
else else
applyMagicEffect(target, caster, spellParams, effect, invalid, receivedMagicDamage); applyMagicEffect(target, caster, spellParams, effect, invalid, receivedMagicDamage, recalculateMagicka);
effect.mMagnitude = magnitude; effect.mMagnitude = magnitude;
magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(effect.mMagnitude - oldMagnitude)); 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; effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove;
if (receivedMagicDamage && target == getPlayer()) if (receivedMagicDamage && target == getPlayer())
MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); 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) 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); fortifySkill(target, effect, -effect.mMagnitude);
break; break;
case ESM::MagicEffect::FortifyMaximumMagicka: case ESM::MagicEffect::FortifyMaximumMagicka:
target.getClass().getCreatureStats(target).setNeedRecalcDynamicStats(true); target.getClass().getCreatureStats(target).recalculateMagicka();
break; break;
case ESM::MagicEffect::AbsorbAttribute: case ESM::MagicEffect::AbsorbAttribute:
{ {

@ -10,8 +10,13 @@
namespace MWMechanics namespace MWMechanics
{ {
enum class MagicApplicationResult
{
APPLIED, REMOVED, REFLECTED
};
// Applies a tick of a single effect. Returns true if the effect should be removed immediately // 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 // Undoes permanent effects created by ESM::MagicEffect::AppliedOnce
void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spell, const ESM::ActiveEffect& effect); void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spell, const ESM::ActiveEffect& effect);

@ -162,7 +162,10 @@ namespace MWMechanics
float castChance = baseChance + castBonus; float castChance = baseChance + castBonus;
castChance *= stats.getFatigueTerm(); 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) float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka)

@ -128,8 +128,7 @@ namespace MWMechanics
} }
// Take hit chance in account, but do not allow rating become negative. // Take hit chance in account, but do not allow rating become negative.
float chance = getHitChance(actor, enemy, value) / 100.f; rating *= std::clamp(getHitChance(actor, enemy, value) / 100.f, 0.01f, 1.f);
rating *= std::min(1.f, std::max(0.01f, chance));
if (weapclass != ESM::WeaponType::Ammo) if (weapclass != ESM::WeaponType::Ammo)
rating *= weapon->mData.mSpeed; rating *= weapon->mData.mSpeed;

@ -12,6 +12,7 @@
#include "collisiontype.hpp" #include "collisiontype.hpp"
#include "mtphysics.hpp" #include "mtphysics.hpp"
#include "trace.h"
#include <cmath> #include <cmath>
@ -21,8 +22,8 @@ namespace MWPhysics
Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler, bool canWaterWalk) Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler, bool canWaterWalk)
: mStandingOnPtr(nullptr), mCanWaterWalk(canWaterWalk), mWalkingOnWater(false) : mStandingOnPtr(nullptr), mCanWaterWalk(canWaterWalk), mWalkingOnWater(false)
, mMeshTranslation(shape->mCollisionBox.center), mOriginalHalfExtents(shape->mCollisionBox.extents) , mMeshTranslation(shape->mCollisionBox.mCenter), mOriginalHalfExtents(shape->mCollisionBox.mExtents)
, mVelocity(0,0,0), mStuckFrames(0), mLastStuckPosition{0, 0, 0} , mStuckFrames(0), mLastStuckPosition{0, 0, 0}
, mForce(0.f, 0.f, 0.f), mOnGround(true), mOnSlope(false) , mForce(0.f, 0.f, 0.f), mOnGround(true), mOnSlope(false)
, mInternalCollisionMode(true) , mInternalCollisionMode(true)
, mExternalCollisionMode(true) , mExternalCollisionMode(true)
@ -123,7 +124,6 @@ void Actor::updatePosition()
mSimulationPosition = worldPosition; mSimulationPosition = worldPosition;
mPositionOffset = osg::Vec3f(); mPositionOffset = osg::Vec3f();
mStandingOnPtr = nullptr; mStandingOnPtr = nullptr;
mSkipCollisions = true;
mSkipSimulation = true; mSkipSimulation = true;
} }
@ -133,11 +133,6 @@ void Actor::setSimulationPosition(const osg::Vec3f& position)
mSimulationPosition = position; mSimulationPosition = position;
} }
osg::Vec3f Actor::getSimulationPosition() const
{
return mSimulationPosition;
}
osg::Vec3f Actor::getScaledMeshTranslation() const osg::Vec3f Actor::getScaledMeshTranslation() const
{ {
return mRotation * osg::componentMultiply(mMeshTranslation, mScale); return mRotation * osg::componentMultiply(mMeshTranslation, mScale);
@ -167,17 +162,19 @@ bool Actor::setPosition(const osg::Vec3f& position)
{ {
std::scoped_lock lock(mPositionMutex); std::scoped_lock lock(mPositionMutex);
applyOffsetChange(); applyOffsetChange();
bool hasChanged = mPosition != position || mWorldPositionChanged; bool hasChanged = (mPosition != position && !mSkipSimulation) || mWorldPositionChanged;
mPreviousPosition = mPosition; if (!mSkipSimulation)
mPosition = position; {
mPreviousPosition = mPosition;
mPosition = position;
}
return hasChanged; return hasChanged;
} }
void Actor::adjustPosition(const osg::Vec3f& offset, bool ignoreCollisions) void Actor::adjustPosition(const osg::Vec3f& offset)
{ {
std::scoped_lock lock(mPositionMutex); std::scoped_lock lock(mPositionMutex);
mPositionOffset += offset; mPositionOffset += offset;
mSkipCollisions = mSkipCollisions || ignoreCollisions;
} }
void Actor::applyOffsetChange() void Actor::applyOffsetChange()
@ -191,16 +188,6 @@ void Actor::applyOffsetChange()
mWorldPositionChanged = true; mWorldPositionChanged = true;
} }
osg::Vec3f Actor::getPosition() const
{
return mPosition;
}
osg::Vec3f Actor::getPreviousPosition() const
{
return mPreviousPosition;
}
void Actor::setRotation(osg::Quat quat) void Actor::setRotation(osg::Quat quat)
{ {
std::scoped_lock lock(mPositionMutex); std::scoped_lock lock(mPositionMutex);
@ -288,19 +275,15 @@ void Actor::setStandingOnPtr(const MWWorld::Ptr& ptr)
mStandingOnPtr = 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);
} }
} }

@ -12,6 +12,7 @@
class btCollisionShape; class btCollisionShape;
class btCollisionObject; class btCollisionObject;
class btCollisionWorld;
class btConvexShape; class btConvexShape;
namespace Resource namespace Resource
@ -59,7 +60,6 @@ namespace MWPhysics
* to account for e.g. scripted movements * to account for e.g. scripted movements
*/ */
void setSimulationPosition(const osg::Vec3f& position); void setSimulationPosition(const osg::Vec3f& position);
osg::Vec3f getSimulationPosition() const;
void updateCollisionObjectPosition(); void updateCollisionObjectPosition();
@ -89,15 +89,11 @@ namespace MWPhysics
void updatePosition(); void updatePosition();
// register a position offset that will be applied during simulation. // 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 // apply position offset. Can't be called during simulation
void applyOffsetChange(); void applyOffsetChange();
osg::Vec3f getPosition() const;
osg::Vec3f getPreviousPosition() const;
/** /**
* Returns the half extents of the collision body (scaled according to rendering scale) * 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, * @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; mLastStuckPosition = position;
} }
bool skipCollisions(); bool canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const;
void setVelocity(osg::Vec3f velocity);
osg::Vec3f velocity();
private: private:
MWWorld::Ptr mStandingOnPtr; MWWorld::Ptr mStandingOnPtr;
@ -190,13 +183,8 @@ namespace MWPhysics
osg::Quat mRotation; osg::Quat mRotation;
osg::Vec3f mScale; osg::Vec3f mScale;
osg::Vec3f mSimulationPosition;
osg::Vec3f mPosition;
osg::Vec3f mPreviousPosition;
osg::Vec3f mPositionOffset; osg::Vec3f mPositionOffset;
osg::Vec3f mVelocity;
bool mWorldPositionChanged; bool mWorldPositionChanged;
bool mSkipCollisions;
bool mSkipSimulation; bool mSkipSimulation;
mutable std::mutex mPositionMutex; mutable std::mutex mPositionMutex;

@ -15,9 +15,9 @@ namespace MWPhysics
const btVector3& position, const btScalar radius) const btVector3& position, const btScalar radius)
{ {
const btVector3 nearest( const btVector3 nearest(
std::max(aabbMin.x(), std::min(aabbMax.x(), position.x())), std::clamp(position.x(), aabbMin.x(), aabbMax.x()),
std::max(aabbMin.y(), std::min(aabbMax.y(), position.y())), std::clamp(position.y(), aabbMin.y(), aabbMax.y()),
std::max(aabbMin.z(), std::min(aabbMax.z(), position.z())) std::clamp(position.z(), aabbMin.z(), aabbMax.z())
); );
return nearest.distance(position) < radius; return nearest.distance(position) < radius;
} }

@ -3,6 +3,7 @@
#include <BulletCollision/CollisionDispatch/btCollisionObject.h> #include <BulletCollision/CollisionDispatch/btCollisionObject.h>
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h> #include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
#include <BulletCollision/CollisionShapes/btCollisionShape.h> #include <BulletCollision/CollisionShapes/btCollisionShape.h>
#include <BulletCollision/CollisionShapes/btConvexShape.h>
#include <components/esm/loadgmst.hpp> #include <components/esm/loadgmst.hpp>
#include <components/misc/convert.hpp> #include <components/misc/convert.hpp>
@ -19,6 +20,8 @@
#include "constants.hpp" #include "constants.hpp"
#include "contacttestwrapper.h" #include "contacttestwrapper.h"
#include "physicssystem.hpp" #include "physicssystem.hpp"
#include "projectile.hpp"
#include "projectileconvexcallback.hpp"
#include "stepper.hpp" #include "stepper.hpp"
#include "trace.h" #include "trace.h"
@ -116,7 +119,7 @@ namespace MWPhysics
} }
void MovementSolver::move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, void MovementSolver::move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld,
WorldFrameData& worldData) const WorldFrameData& worldData)
{ {
// Reset per-frame data // Reset per-frame data
actor.mWalkingOnWater = false; actor.mWalkingOnWater = false;
@ -203,7 +206,7 @@ namespace MWPhysics
if((newPosition - nextpos).length2() > 0.0001) if((newPosition - nextpos).length2() > 0.0001)
{ {
// trace to where character would go if there were no obstructions // 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 // check for obstructions
if(tracer.mFraction >= 1.0f) if(tracer.mFraction >= 1.0f)
@ -338,7 +341,7 @@ namespace MWPhysics
osg::Vec3f from = newPosition; osg::Vec3f from = newPosition;
auto dropDistance = 2*sGroundOffset + (actor.mIsOnGround ? sStepSizeDown : 0); auto dropDistance = 2*sGroundOffset + (actor.mIsOnGround ? sStepSizeDown : 0);
osg::Vec3f to = newPosition - osg::Vec3f(0,0,dropDistance); 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(tracer.mFraction < 1.0f)
{ {
if (!isActor(tracer.mHitObject)) if (!isActor(tracer.mHitObject))
@ -398,6 +401,29 @@ namespace MWPhysics
actor.mPosition.z() -= actor.mHalfExtentsZ; // vanilla-accurate 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<const btConvexShape*>(shape), from_, to_, resultCallback);
projectile.mPosition = Misc::Convert::toOsg(projectile.mProjectile->isActive() ? btTo : resultCallback.m_hitPointWorld);
}
btVector3 addMarginToDelta(btVector3 delta) btVector3 addMarginToDelta(btVector3 delta)
{ {
if(delta.length2() == 0.0) if(delta.length2() == 0.0)

@ -37,13 +37,15 @@ namespace MWPhysics
class Actor; class Actor;
struct ActorFrameData; struct ActorFrameData;
struct ProjectileFrameData;
struct WorldFrameData; struct WorldFrameData;
class MovementSolver class MovementSolver
{ {
public: public:
static osg::Vec3f traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight); 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); static void unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld);
}; };
} }

@ -105,10 +105,134 @@ namespace
return actorData.mPosition.z() < actorData.mSwimLevel; 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); 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<MWPhysics::PtrHolder*>(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 namespace Config
@ -235,13 +359,12 @@ namespace MWPhysics
return std::make_tuple(numSteps, actualDelta); return std::make_tuple(numSteps, actualDelta);
} }
void PhysicsTaskScheduler::applyQueuedMovements(float & timeAccum, std::vector<std::shared_ptr<Actor>>&& actors, std::vector<ActorFrameData>&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) void PhysicsTaskScheduler::applyQueuedMovements(float & timeAccum, std::vector<Simulation>&& simulations, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats)
{ {
// This function run in the main thread. // This function run in the main thread.
// While the mSimulationMutex is held, background physics threads can't run. // While the mSimulationMutex is held, background physics threads can't run.
MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); MaybeExclusiveLock lock(mSimulationMutex, mNumThreads);
assert(actors.size() == actorsData.size());
double timeStart = mTimer->tick(); double timeStart = mTimer->tick();
@ -259,19 +382,19 @@ namespace MWPhysics
timeAccum -= numSteps*newDelta; timeAccum -= numSteps*newDelta;
// init // 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; mPrevStepCount = numSteps;
mRemainingSteps = numSteps; mRemainingSteps = numSteps;
mTimeAccum = timeAccum; mTimeAccum = timeAccum;
mPhysicsDt = newDelta; mPhysicsDt = newDelta;
mActors = std::move(actors); mSimulations = std::move(simulations);
mActorsFrameData = std::move(actorsData);
mAdvanceSimulation = (mRemainingSteps != 0); mAdvanceSimulation = (mRemainingSteps != 0);
mNewFrame = true; mNewFrame = true;
mNumJobs = mActorsFrameData.size(); mNumJobs = mSimulations.size();
mNextLOS.store(0, std::memory_order_relaxed); mNextLOS.store(0, std::memory_order_relaxed);
mNextJob.store(0, std::memory_order_release); mNextJob.store(0, std::memory_order_release);
@ -301,8 +424,7 @@ namespace MWPhysics
MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); MaybeExclusiveLock lock(mSimulationMutex, mNumThreads);
mBudget.reset(mDefaultPhysicsDt); mBudget.reset(mDefaultPhysicsDt);
mAsyncBudget.reset(0.0f); mAsyncBudget.reset(0.0f);
mActors.clear(); mSimulations.clear();
mActorsFrameData.clear();
for (const auto& [_, actor] : actors) for (const auto& [_, actor] : actors)
{ {
actor->updatePosition(); actor->updatePosition();
@ -448,7 +570,7 @@ namespace MWPhysics
} }
else if (const auto projectile = std::dynamic_pointer_cast<Projectile>(ptr)) else if (const auto projectile = std::dynamic_pointer_cast<Projectile>(ptr))
{ {
projectile->commitPositionChange(); projectile->updateCollisionObjectPosition();
mCollisionWorld->updateSingleAabb(projectile->getCollisionObject()); mCollisionWorld->updateSingleAabb(projectile->getCollisionObject());
} }
} }
@ -467,47 +589,11 @@ namespace MWPhysics
void PhysicsTaskScheduler::updateActorsPositions() 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);
{ std::visit(vis, sim);
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<MWPhysics::PtrHolder*>(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);
} }
} }
@ -532,10 +618,11 @@ namespace MWPhysics
{ {
mPreStepBarrier->wait([this] { afterPreStep(); }); mPreStepBarrier->wait([this] { afterPreStep(); });
int job = 0; int job = 0;
const Visitors::Move vis{mPhysicsDt, mCollisionWorld, *mWorldFrameData};
while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs)
{ {
MaybeLock lockColWorld(mCollisionWorldMutex, mNumThreads); MaybeLock lockColWorld(mCollisionWorldMutex, mNumThreads);
MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld, *mWorldFrameData); std::visit(vis, mSimulations[job]);
} }
mPostStepBarrier->wait([this] { afterPostStep(); }); mPostStepBarrier->wait([this] { afterPostStep(); });
@ -577,7 +664,7 @@ namespace MWPhysics
void PhysicsTaskScheduler::releaseSharedStates() void PhysicsTaskScheduler::releaseSharedStates()
{ {
std::scoped_lock lock(mSimulationMutex, mUpdateAabbMutex); std::scoped_lock lock(mSimulationMutex, mUpdateAabbMutex);
mActors.clear(); mSimulations.clear();
mUpdateAabb.clear(); mUpdateAabb.clear();
} }
@ -586,10 +673,11 @@ namespace MWPhysics
updateAabbs(); updateAabbs();
if (!mRemainingSteps) if (!mRemainingSteps)
return; return;
for (size_t i = 0; i < mActors.size(); ++i) const Visitors::PreStep vis{mCollisionWorld};
for (auto& sim : mSimulations)
{ {
MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads);
MovementSolver::unstuck(mActorsFrameData[i], mCollisionWorld); std::visit(vis, sim);
} }
} }
@ -618,7 +706,8 @@ namespace MWPhysics
void PhysicsTaskScheduler::syncWithMainThread() void PhysicsTaskScheduler::syncWithMainThread()
{ {
for (size_t i = 0; i < mActors.size(); ++i) const Visitors::Sync vis{mAdvanceSimulation, mTimeAccum, mPhysicsDt, this};
updateActor(*mActors[i], mActorsFrameData[i], mAdvanceSimulation, mTimeAccum, mPhysicsDt); for (auto& sim : mSimulations)
std::visit(vis, sim);
} }
} }

@ -7,6 +7,7 @@
#include <shared_mutex> #include <shared_mutex>
#include <thread> #include <thread>
#include <unordered_set> #include <unordered_set>
#include <variant>
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h> #include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
@ -39,7 +40,7 @@ namespace MWPhysics
/// @param timeAccum accumulated time from previous run to interpolate movements /// @param timeAccum accumulated time from previous run to interpolate movements
/// @param actorsData per actor data needed to compute new positions /// @param actorsData per actor data needed to compute new positions
/// @return new position of each actor /// @return new position of each actor
void applyQueuedMovements(float & timeAccum, std::vector<std::shared_ptr<Actor>>&& actors, std::vector<ActorFrameData>&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); void applyQueuedMovements(float & timeAccum, std::vector<Simulation>&& simulations, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats);
void resetSimulation(const ActorMap& actors); void resetSimulation(const ActorMap& actors);
@ -57,14 +58,12 @@ namespace MWPhysics
bool getLineOfSight(const std::shared_ptr<Actor>& actor1, const std::shared_ptr<Actor>& actor2); bool getLineOfSight(const std::shared_ptr<Actor>& actor1, const std::shared_ptr<Actor>& actor2);
void debugDraw(); void debugDraw();
void* getUserPointer(const btCollisionObject* object) const; void* getUserPointer(const btCollisionObject* object) const;
void releaseSharedStates(); // destroy all objects whose destructor can't be safely called from ~PhysicsTaskScheduler() void releaseSharedStates(); // destroy all objects whose destructor can't be safely called from ~PhysicsTaskScheduler()
private: private:
void doSimulation(); void doSimulation();
void worker(); void worker();
void updateActorsPositions(); void updateActorsPositions();
void updateActor(Actor& actor, ActorFrameData& actorData, bool simulationPerformed, float timeAccum, float dt) const;
bool hasLineOfSight(const Actor* actor1, const Actor* actor2); bool hasLineOfSight(const Actor* actor1, const Actor* actor2);
void refreshLOSCache(); void refreshLOSCache();
void updateAabbs(); void updateAabbs();
@ -77,8 +76,7 @@ namespace MWPhysics
void syncWithMainThread(); void syncWithMainThread();
std::unique_ptr<WorldFrameData> mWorldFrameData; std::unique_ptr<WorldFrameData> mWorldFrameData;
std::vector<std::shared_ptr<Actor>> mActors; std::vector<Simulation> mSimulations;
std::vector<ActorFrameData> mActorsFrameData;
std::unordered_set<const btCollisionObject*> mCollisionObjects; std::unordered_set<const btCollisionObject*> mCollisionObjects;
float mDefaultPhysicsDt; float mDefaultPhysicsDt;
float mPhysicsDt; float mPhysicsDt;

@ -24,7 +24,7 @@ namespace MWPhysics
, mTaskScheduler(scheduler) , mTaskScheduler(scheduler)
{ {
mPtr = ptr; mPtr = ptr;
mCollisionObject = BulletHelpers::makeCollisionObject(mShapeInstance->getCollisionShape(), mCollisionObject = BulletHelpers::makeCollisionObject(mShapeInstance->mCollisionShape.get(),
Misc::Convert::toBullet(mPosition), Misc::Convert::toBullet(rotation)); Misc::Convert::toBullet(mPosition), Misc::Convert::toBullet(rotation));
mCollisionObject->setUserPointer(this); mCollisionObject->setUserPointer(this);
mShapeInstance->setLocalScaling(mScale); mShapeInstance->setLocalScaling(mScale);
@ -109,9 +109,9 @@ namespace MWPhysics
if (mShapeInstance->mAnimatedShapes.empty()) if (mShapeInstance->mAnimatedShapes.empty())
return false; return false;
assert (mShapeInstance->getCollisionShape()->isCompound()); assert (mShapeInstance->mCollisionShape->isCompound());
btCompoundShape* compound = static_cast<btCompoundShape*>(mShapeInstance->getCollisionShape()); btCompoundShape* compound = static_cast<btCompoundShape*>(mShapeInstance->mCollisionShape.get());
for (const auto& [recIndex, shapeIndex] : mShapeInstance->mAnimatedShapes) for (const auto& [recIndex, shapeIndex] : mShapeInstance->mAnimatedShapes)
{ {
auto nodePathFound = mRecIndexToNodePath.find(recIndex); auto nodePathFound = mRecIndexToNodePath.find(recIndex);

@ -60,19 +60,6 @@
namespace 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) void handleJump(const MWWorld::Ptr &ptr)
{ {
if (!ptr.getClass().isActor()) if (!ptr.getClass().isActor())
@ -370,6 +357,8 @@ namespace MWPhysics
bool PhysicsSystem::getLineOfSight(const MWWorld::ConstPtr &actor1, const MWWorld::ConstPtr &actor2) const 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 it1 = mActors.find(actor1.mRef);
const auto it2 = mActors.find(actor2.mRef); const auto it2 = mActors.find(actor2.mRef);
if (it1 == mActors.end() || it2 == mActors.end()) if (it1 == mActors.end() || it2 == mActors.end())
@ -386,7 +375,8 @@ namespace MWPhysics
bool PhysicsSystem::canMoveToWaterSurface(const MWWorld::ConstPtr &actor, const float waterlevel) 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 osg::Vec3f PhysicsSystem::getHalfExtents(const MWWorld::ConstPtr &actor) const
@ -491,7 +481,7 @@ namespace MWPhysics
if (ptr.mRef->mData.mPhysicsPostponed) if (ptr.mRef->mData.mPhysicsPostponed)
return; return;
osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance = mShapeManager->getInstance(mesh); osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance = mShapeManager->getInstance(mesh);
if (!shapeInstance || !shapeInstance->getCollisionShape()) if (!shapeInstance || !shapeInstance->mCollisionShape)
return; return;
assert(!getObject(ptr)); assert(!getObject(ptr));
@ -526,10 +516,10 @@ namespace MWPhysics
void PhysicsSystem::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated) void PhysicsSystem::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated)
{ {
if (auto found = mObjects.find(old.mRef); found != mObjects.end()) if (auto foundObject = mObjects.find(old.mRef); foundObject != mObjects.end())
found->second->updatePtr(updated); foundObject->second->updatePtr(updated);
else if (auto found = mActors.find(old.mRef); found != mActors.end()) else if (auto foundActor = mActors.find(old.mRef); foundActor != mActors.end())
found->second->updatePtr(updated); foundActor->second->updatePtr(updated);
for (auto& [_, actor] : mActors) 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) void PhysicsSystem::updateRotation(const MWWorld::Ptr &ptr, osg::Quat rotate)
{ {
if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end())
@ -655,7 +618,7 @@ namespace MWPhysics
osg::ref_ptr<const Resource::BulletShape> shape = mShapeManager->getShape(mesh); osg::ref_ptr<const Resource::BulletShape> shape = mShapeManager->getShape(mesh);
// Try to get shape from basic model as fallback for creatures // 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); const std::string fallbackModel = ptr.getClass().getModel(ptr);
if (fallbackModel != mesh) if (fallbackModel != mesh)
@ -680,7 +643,7 @@ namespace MWPhysics
{ {
osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance = mShapeManager->getInstance(mesh); osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance = mShapeManager->getInstance(mesh);
assert(shapeInstance); 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++; mProjectileId++;
@ -727,11 +690,10 @@ namespace MWPhysics
actor->setVelocity(osg::Vec3f()); actor->setVelocity(osg::Vec3f());
} }
std::pair<std::vector<std::shared_ptr<Actor>>, std::vector<ActorFrameData>> PhysicsSystem::prepareFrameData(bool willSimulate) std::vector<Simulation> PhysicsSystem::prepareSimulation(bool willSimulate)
{ {
std::pair<std::vector<std::shared_ptr<Actor>>, std::vector<ActorFrameData>> framedata; std::vector<Simulation> simulations;
framedata.first.reserve(mActors.size()); simulations.reserve(mActors.size() + mProjectiles.size());
framedata.second.reserve(mActors.size());
const MWBase::World *world = MWBase::Environment::get().getWorld(); const MWBase::World *world = MWBase::Environment::get().getWorld();
for (const auto& [ref, physicActor] : mActors) for (const auto& [ref, physicActor] : mActors)
{ {
@ -756,18 +718,23 @@ namespace MWPhysics
physicActor->setCanWaterWalk(waterCollision); physicActor->setCanWaterWalk(waterCollision);
// Slow fall reduces fall speed by a factor of (effect magnitude / 200) // 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 godmode = ptr == world->getPlayerConstPtr() && world->getGodModeState();
const bool inert = stats.isDead() || (!godmode && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0); const bool inert = stats.isDead() || (!godmode && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0);
framedata.first.emplace_back(physicActor); simulations.emplace_back(ActorSimulation{physicActor, ActorFrameData{*physicActor, inert, waterCollision, slowFall, waterlevel}});
framedata.second.emplace_back(*physicActor, inert, waterCollision, slowFall, waterlevel);
// if the simulation will run, a jump request will be fulfilled. Update mechanics accordingly. // if the simulation will run, a jump request will be fulfilled. Update mechanics accordingly.
if (willSimulate) if (willSimulate)
handleJump(ptr); 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) 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); mTaskScheduler->resetSimulation(mActors);
else else
{ {
auto [actors, framedata] = prepareFrameData(mTimeAccum >= mPhysicsDt); auto simulations = prepareSimulation(mTimeAccum >= mPhysicsDt);
// modifies mTimeAccum // 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()) , mWasOnGround(actor.getOnGround())
, mIsAquatic(actor.getPtr().getClass().isPureWaterCreature(actor.getPtr())) , mIsAquatic(actor.getPtr().getClass().isPureWaterCreature(actor.getPtr()))
, mWaterCollision(waterCollision) , 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() WorldFrameData::WorldFrameData()

@ -7,6 +7,7 @@
#include <set> #include <set>
#include <unordered_map> #include <unordered_map>
#include <algorithm> #include <algorithm>
#include <variant>
#include <osg/Quat> #include <osg/Quat>
#include <osg/BoundingBox> #include <osg/BoundingBox>
@ -75,7 +76,6 @@ namespace MWPhysics
struct ActorFrameData struct ActorFrameData
{ {
ActorFrameData(Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel); ActorFrameData(Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel);
void updatePosition(Actor& actor, btCollisionWorld* world);
osg::Vec3f mPosition; osg::Vec3f mPosition;
osg::Vec3f mInertia; osg::Vec3f mInertia;
const btCollisionObject* mStandingOn; const btCollisionObject* mStandingOn;
@ -100,6 +100,16 @@ namespace MWPhysics
const bool mSkipCollisionDetection; 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 struct WorldFrameData
{ {
WorldFrameData(); WorldFrameData();
@ -107,6 +117,10 @@ namespace MWPhysics
osg::Vec3f mStormDirection; osg::Vec3f mStormDirection;
}; };
using ActorSimulation = std::pair<std::shared_ptr<Actor>, ActorFrameData>;
using ProjectileSimulation = std::pair<std::shared_ptr<Projectile>, ProjectileFrameData>;
using Simulation = std::variant<ActorSimulation, ProjectileSimulation>;
class PhysicsSystem : public RayCastingInterface class PhysicsSystem : public RayCastingInterface
{ {
public: public:
@ -124,7 +138,6 @@ namespace MWPhysics
int addProjectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius); 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 setCaster(int projectileId, const MWWorld::Ptr& caster);
void updateProjectile(const int projectileId, const osg::Vec3f &position) const;
void removeProjectile(const int projectileId); void removeProjectile(const int projectileId);
void updatePtr (const MWWorld::Ptr& old, const MWWorld::Ptr& updated); void updatePtr (const MWWorld::Ptr& old, const MWWorld::Ptr& updated);
@ -253,7 +266,7 @@ namespace MWPhysics
void updateWater(); void updateWater();
std::pair<std::vector<std::shared_ptr<Actor>>, std::vector<ActorFrameData>> prepareFrameData(bool willSimulate); std::vector<Simulation> prepareSimulation(bool willSimulate);
std::unique_ptr<btBroadphaseInterface> mBroadphase; std::unique_ptr<btBroadphaseInterface> mBroadphase;
std::unique_ptr<btDefaultCollisionConfiguration> mCollisionConfiguration; std::unique_ptr<btDefaultCollisionConfiguration> mCollisionConfiguration;

@ -31,14 +31,15 @@ Projectile::Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, f
mCollisionObject->setCollisionShape(mShape.get()); mCollisionObject->setCollisionShape(mShape.get());
mCollisionObject->setUserPointer(this); mCollisionObject->setUserPointer(this);
setPosition(position); mPosition = position;
mPreviousPosition = position;
setCaster(caster); setCaster(caster);
const int collisionMask = CollisionType_World | CollisionType_HeightMap | const int collisionMask = CollisionType_World | CollisionType_HeightMap |
CollisionType_Actor | CollisionType_Door | CollisionType_Water | CollisionType_Projectile; CollisionType_Actor | CollisionType_Door | CollisionType_Water | CollisionType_Projectile;
mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Projectile, collisionMask); mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Projectile, collisionMask);
commitPositionChange(); updateCollisionObjectPosition();
} }
Projectile::~Projectile() Projectile::~Projectile()
@ -48,29 +49,12 @@ Projectile::~Projectile()
mTaskScheduler->removeCollisionObject(mCollisionObject.get()); 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); 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) void Projectile::hit(const btCollisionObject* target, btVector3 pos, btVector3 normal)

@ -36,10 +36,7 @@ namespace MWPhysics
btConvexShape* getConvexShape() const { return mConvexShape; } btConvexShape* getConvexShape() const { return mConvexShape; }
void commitPositionChange(); void updateCollisionObjectPosition();
void setPosition(const osg::Vec3f& position);
osg::Vec3f getPosition() const;
bool isActive() const bool isActive() const
{ {
@ -80,13 +77,11 @@ namespace MWPhysics
std::unique_ptr<btCollisionShape> mShape; std::unique_ptr<btCollisionShape> mShape;
btConvexShape* mConvexShape; btConvexShape* mConvexShape;
bool mTransformUpdatePending;
bool mHitWater; bool mHitWater;
std::atomic<bool> mActive; std::atomic<bool> mActive;
MWWorld::Ptr mCaster; MWWorld::Ptr mCaster;
const btCollisionObject* mCasterColObj; const btCollisionObject* mCasterColObj;
const btCollisionObject* mHitTarget; const btCollisionObject* mHitTarget;
osg::Vec3f mPosition;
btVector3 mHitPosition; btVector3 mHitPosition;
btVector3 mHitNormal; btVector3 mHitNormal;

@ -30,9 +30,49 @@ namespace MWPhysics
return mCollisionObject.get(); 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: protected:
MWWorld::Ptr mPtr; MWWorld::Ptr mPtr;
std::unique_ptr<btCollisionObject> mCollisionObject; std::unique_ptr<btCollisionObject> mCollisionObject;
osg::Vec3f mVelocity;
osg::Vec3f mSimulationPosition;
osg::Vec3f mPosition;
osg::Vec3f mPreviousPosition;
}; };
} }

@ -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. // 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. // 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; float upDistance = 0;
if(!mUpStepper.mHitObject) if(!mUpStepper.mHitObject)
@ -117,7 +117,7 @@ namespace MWPhysics
downStepSize = upDistance; downStepSize = upDistance;
else else
downStepSize = moveDistance + upDistance + sStepSizeDown; 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 // 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 // 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

@ -12,38 +12,84 @@
namespace MWPhysics 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(); const btTransform &trans = actor->getWorldTransform();
btTransform from(trans); btTransform transFrom(trans);
btTransform to(trans); btTransform transTo(trans);
from.setOrigin(btstart); transFrom.setOrigin(from);
to.setOrigin(btend); transTo.setOrigin(to);
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;
const btCollisionShape *shape = actor->getCollisionShape(); const btCollisionShape *shape = actor->getCollisionShape();
assert(shape->isConvex()); assert(shape->isConvex());
world->convexSweepTest(static_cast<const btConvexShape*>(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<const btConvexShape*>(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: // Copy the hit data over to our trace results struct:
if(newTraceCallback.hasHit()) if(traceCallback.hasHit())
{ {
mFraction = newTraceCallback.m_closestHitFraction; mFraction = traceCallback.m_closestHitFraction;
mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); // 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; mEndPos = (end-start)*mFraction + start;
mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld); mHitPoint = Misc::Convert::toOsg(traceCallback.m_hitPointWorld);
mHitObject = newTraceCallback.m_hitCollisionObject; mHitObject = traceCallback.m_hitCollisionObject;
} }
else 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; mEndPos = end;
mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f); mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f);
mFraction = 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) 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 auto traceCallback = sweepHelper(actor->getCollisionObject(), Misc::Convert::toBullet(start), Misc::Convert::toBullet(end), world, true);
const btVector3 btend = Misc::Convert::toBullet(end); if(traceCallback.hasHit())
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())
{ {
mFraction = newTraceCallback.m_closestHitFraction; mFraction = traceCallback.m_closestHitFraction;
mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); mPlaneNormal = Misc::Convert::toOsg(traceCallback.m_hitNormalWorld);
mEndPos = (end-start)*mFraction + start; mEndPos = (end-start)*mFraction + start;
} }
else else

@ -20,7 +20,7 @@ namespace MWPhysics
float mFraction; 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); void findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world);
}; };
} }

@ -11,6 +11,7 @@
#include <components/resource/resourcesystem.hpp> #include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp> #include <components/resource/scenemanager.hpp>
#include <components/sceneutil/attach.hpp>
#include <components/sceneutil/lightmanager.hpp> #include <components/sceneutil/lightmanager.hpp>
#include <components/sceneutil/lightutil.hpp> #include <components/sceneutil/lightutil.hpp>
#include <components/sceneutil/visitor.hpp> #include <components/sceneutil/visitor.hpp>
@ -74,7 +75,7 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st
osg::ref_ptr<osg::Node> instance = mResourceSystem->getSceneManager()->getInstance(model, parent); osg::ref_ptr<osg::Node> instance = mResourceSystem->getSceneManager()->getInstance(model, parent);
const NodeMap& nodeMap = getNodeMap(); 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()) if (found == nodeMap.end())
return PartHolderPtr(); return PartHolderPtr();
@ -84,30 +85,58 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st
return PartHolderPtr(new PartHolder(instance)); return PartHolderPtr(new PartHolder(instance));
} }
std::string ActorAnimation::getShieldMesh(const MWWorld::ConstPtr& shield) const osg::ref_ptr<osg::Node> ActorAnimation::attach(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool isLight)
{
osg::ref_ptr<const osg::Node> 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<ESM::Armor>()->mBase; const ESM::Armor *armor = shield.get<ESM::Armor>()->mBase;
const std::vector<ESM::PartReference>& bodyparts = armor->mParts.mParts; const std::vector<ESM::PartReference>& bodyparts = armor->mParts.mParts;
// Try to recover the body part model, use ground model as a fallback otherwise.
if (!bodyparts.empty()) if (!bodyparts.empty())
{ {
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
const MWWorld::Store<ESM::BodyPart> &partStore = store.get<ESM::BodyPart>(); const MWWorld::Store<ESM::BodyPart> &partStore = store.get<ESM::BodyPart>();
// Try to get shield model from bodyparts first, with ground model as fallback
for (const auto& part : bodyparts) for (const auto& part : bodyparts)
{ {
// Assume all creatures use the male mesh. if (part.mPart != ESM::PRT_Shield)
if (part.mPart != ESM::PRT_Shield || part.mMale.empty())
continue; 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; const ESM::BodyPart *bodypart = partStore.search(bodypartName);
break; 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()) if (mesh.empty())
return mesh; return mesh;
@ -143,7 +172,7 @@ bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const
const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr);
const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); 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) if(stats.getDrawState() != MWMechanics::DrawState_Weapon)
return false; return false;
@ -201,7 +230,7 @@ void ActorAnimation::updateHolsteredShield(bool showCarriedLeft)
return; return;
} }
std::string mesh = getShieldMesh(*shield); std::string mesh = getSheathedShieldMesh(*shield);
if (mesh.empty()) if (mesh.empty())
return; return;
@ -255,7 +284,7 @@ bool ActorAnimation::useShieldAnimations() const
const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft);
if (weapon != inv.end() && shield != inv.end() && if (weapon != inv.end() && shield != inv.end() &&
shield->getType() == ESM::Armor::sRecordId && shield->getType() == ESM::Armor::sRecordId &&
!getShieldMesh(*shield).empty()) !getSheathedShieldMesh(*shield).empty())
{ {
auto type = weapon->getType(); auto type = weapon->getType();
if(type == ESM::Weapon::sRecordId) if(type == ESM::Weapon::sRecordId)

@ -45,7 +45,8 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener
virtual void updateHolsteredWeapon(bool showHolsteredWeapons); virtual void updateHolsteredWeapon(bool showHolsteredWeapons);
virtual void updateHolsteredShield(bool showCarriedLeft); virtual void updateHolsteredShield(bool showCarriedLeft);
virtual void updateQuiver(); 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 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, bool enchantedGlow, osg::Vec4f* glowColor);
virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename) 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); osg::Vec4f stubColor = osg::Vec4f(0,0,0,0);
return attachMesh(model, bonename, false, &stubColor); return attachMesh(model, bonename, false, &stubColor);
}; };
osg::ref_ptr<osg::Node> attach(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool isLight);
PartHolderPtr mScabbard; PartHolderPtr mScabbard;
PartHolderPtr mHolsteredShield; PartHolderPtr mHolsteredShield;

@ -389,11 +389,6 @@ namespace MWRender
mAlpha = alpha; mAlpha = alpha;
} }
void setLightSource(const osg::ref_ptr<SceneUtil::LightSource>& lightSource)
{
mLightSource = lightSource;
}
protected: protected:
void setDefaults(osg::StateSet* stateset) override void setDefaults(osg::StateSet* stateset) override
{ {
@ -416,13 +411,10 @@ namespace MWRender
{ {
osg::Material* material = static_cast<osg::Material*>(stateset->getAttribute(osg::StateAttribute::MATERIAL)); osg::Material* material = static_cast<osg::Material*>(stateset->getAttribute(osg::StateAttribute::MATERIAL));
material->setAlpha(osg::Material::FRONT_AND_BACK, mAlpha); material->setAlpha(osg::Material::FRONT_AND_BACK, mAlpha);
if (mLightSource)
mLightSource->setActorFade(mAlpha);
} }
private: private:
float mAlpha; float mAlpha;
osg::ref_ptr<SceneUtil::LightSource> mLightSource;
}; };
struct Animation::AnimSource struct Animation::AnimSource
@ -968,8 +960,9 @@ namespace MWRender
{ {
osg::ref_ptr<osg::Node> node = getNodeMap().at(it->first); // this should not throw, we already checked for the node existing in addAnimSource osg::ref_ptr<osg::Node> node = getNodeMap().at(it->first); // this should not throw, we already checked for the node existing in addAnimSource
node->addUpdateCallback(it->second); osg::Callback* callback = it->second->getAsCallback();
mActiveControllers.emplace_back(node, it->second); node->addUpdateCallback(callback);
mActiveControllers.emplace_back(node, callback);
if (blendMask == 0 && node == mAccumRoot) if (blendMask == 0 && node == mAccumRoot)
{ {
@ -1319,10 +1312,10 @@ namespace MWRender
cache.insert(std::make_pair(model, created)); cache.insert(std::make_pair(model, created));
return sceneMgr->createInstance(created); return sceneMgr->getInstance(created);
} }
else else
return sceneMgr->createInstance(found->second); return sceneMgr->getInstance(found->second);
} }
else else
{ {
@ -1484,6 +1477,7 @@ namespace MWRender
bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior();
mExtraLightSource = SceneUtil::addLight(parent, esmLight, Mask_ParticleSystem, Mask_Lighting, exterior); 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) 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; parentNode = mInsert;
else else
{ {
NodeMap::const_iterator found = getNodeMap().find(Misc::StringUtils::lowerCase(bonename)); NodeMap::const_iterator found = getNodeMap().find(bonename);
if (found == getNodeMap().end()) if (found == getNodeMap().end())
throw std::runtime_error("Can't find bone " + bonename); 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 const osg::Node* Animation::getNode(const std::string &name) const
{ {
std::string lowerName = Misc::StringUtils::lowerCase(name); NodeMap::const_iterator found = getNodeMap().find(name);
NodeMap::const_iterator found = getNodeMap().find(lowerName);
if (found == getNodeMap().end()) if (found == getNodeMap().end())
return nullptr; return nullptr;
else else
@ -1639,7 +1632,6 @@ namespace MWRender
if (mTransparencyUpdater == nullptr) if (mTransparencyUpdater == nullptr)
{ {
mTransparencyUpdater = new TransparencyUpdater(alpha); mTransparencyUpdater = new TransparencyUpdater(alpha);
mTransparencyUpdater->setLightSource(mExtraLightSource);
mObjectRoot->addCullCallback(mTransparencyUpdater); mObjectRoot->addCullCallback(mTransparencyUpdater);
} }
else else
@ -1650,6 +1642,8 @@ namespace MWRender
mObjectRoot->removeCullCallback(mTransparencyUpdater); mObjectRoot->removeCullCallback(mTransparencyUpdater);
mTransparencyUpdater = nullptr; mTransparencyUpdater = nullptr;
} }
if (mExtraLightSource)
mExtraLightSource->setActorFade(alpha);
} }
void Animation::setLightEffect(float effect) void Animation::setLightEffect(float effect)

@ -7,8 +7,10 @@
#include <components/sceneutil/textkeymap.hpp> #include <components/sceneutil/textkeymap.hpp>
#include <components/sceneutil/util.hpp> #include <components/sceneutil/util.hpp>
#include <components/sceneutil/nodecallback.hpp> #include <components/sceneutil/nodecallback.hpp>
#include <components/misc/stringops.hpp>
#include <vector> #include <vector>
#include <unordered_map>
namespace ESM namespace ESM
{ {
@ -157,6 +159,8 @@ public:
virtual bool updateCarriedLeftVisible(const int weaptype) const { return false; }; virtual bool updateCarriedLeftVisible(const int weaptype) const { return false; };
typedef std::unordered_map<std::string, osg::ref_ptr<osg::MatrixTransform>, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> NodeMap;
protected: protected:
class AnimationTime : public SceneUtil::ControllerSource class AnimationTime : public SceneUtil::ControllerSource
{ {
@ -250,8 +254,6 @@ protected:
std::shared_ptr<AnimationTime> mAnimationTimePtr[sNumBlendMasks]; std::shared_ptr<AnimationTime> mAnimationTimePtr[sNumBlendMasks];
// Stored in all lowercase for a case-insensitive lookup
typedef std::map<std::string, osg::ref_ptr<osg::MatrixTransform> > NodeMap;
mutable NodeMap mNodeMap; mutable NodeMap mNodeMap;
mutable bool mNodeMapCreated; mutable bool mNodeMapCreated;

@ -236,7 +236,7 @@ namespace MWRender
mTotalMovement += speed * duration; mTotalMovement += speed * duration;
speed /= (1.f + speed / 500.f); speed /= (1.f + speed / 500.f);
float maxDelta = 300.f * duration; 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); mMaxNextCameraDistance = mCameraDistance + duration * (100.f + mBaseCameraDistance);
updateStandingPreviewMode(); updateStandingPreviewMode();
@ -434,7 +434,7 @@ namespace MWRender
{ {
const float epsilon = 0.000001f; const float epsilon = 0.000001f;
float limit = static_cast<float>(osg::PI_2) - epsilon; float limit = static_cast<float>(osg::PI_2) - epsilon;
mPitch = osg::clampBetween(angle, -limit, limit); mPitch = std::clamp(angle, -limit, limit);
} }
float Camera::getCameraDistance() const float Camera::getCameraDistance() const
@ -460,7 +460,7 @@ namespace MWRender
} }
mIsNearest = mBaseCameraDistance <= mNearest; mIsNearest = mBaseCameraDistance <= mNearest;
mBaseCameraDistance = osg::clampBetween(mBaseCameraDistance, mNearest, mFurthest); mBaseCameraDistance = std::clamp(mBaseCameraDistance, mNearest, mFurthest);
Settings::Manager::setFloat("third person camera distance", "Camera", mBaseCameraDistance); Settings::Manager::setFloat("third person camera distance", "Camera", mBaseCameraDistance);
} }

@ -190,7 +190,9 @@ namespace MWRender
mTexture->setInternalFormat(GL_RGBA); mTexture->setInternalFormat(GL_RGBA);
mTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR);
mTexture->setFilter(osg::Texture::MAG_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; mCamera = new osg::Camera;
// hints that the camera is not relative to the master camera // hints that the camera is not relative to the master camera

@ -18,6 +18,7 @@ namespace osg
class Camera; class Camera;
class Group; class Group;
class Viewport; class Viewport;
class StateSet;
} }
namespace MWRender namespace MWRender
@ -41,6 +42,8 @@ namespace MWRender
void rebuild(); void rebuild();
osg::ref_ptr<osg::Texture2D> getTexture(); osg::ref_ptr<osg::Texture2D> getTexture();
/// Get the osg::StateSet required to render the texture correctly, if any.
osg::StateSet* getTextureStateSet() { return mTextureStateSet; }
private: private:
CharacterPreview(const CharacterPreview&); CharacterPreview(const CharacterPreview&);
@ -54,6 +57,7 @@ namespace MWRender
osg::ref_ptr<osg::Group> mParent; osg::ref_ptr<osg::Group> mParent;
Resource::ResourceSystem* mResourceSystem; Resource::ResourceSystem* mResourceSystem;
osg::ref_ptr<osg::Texture2D> mTexture; osg::ref_ptr<osg::Texture2D> mTexture;
osg::ref_ptr<osg::StateSet> mTextureStateSet;
osg::ref_ptr<osg::Camera> mCamera; osg::ref_ptr<osg::Camera> mCamera;
osg::ref_ptr<DrawOnceCallback> mDrawOnceCallback; osg::ref_ptr<DrawOnceCallback> mDrawOnceCallback;

@ -126,7 +126,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot)
if (bonename != "Weapon Bone") if (bonename != "Weapon Bone")
{ {
const NodeMap& nodeMap = getNodeMap(); 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()) if (found == nodeMap.end())
bonename = "Weapon Bone"; bonename = "Weapon Bone";
} }
@ -139,32 +139,13 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot)
bonename = "Shield Bone"; bonename = "Shield Bone";
if (item.getType() == ESM::Armor::sRecordId) if (item.getType() == ESM::Armor::sRecordId)
{ {
// Shield body part model should be used if possible. itemModel = getShieldMesh(item, false);
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
for (const auto& part : item.get<ESM::Armor>()->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<ESM::BodyPart>().search(part.mMale);
if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty())
{
itemModel = "meshes\\" + bodypart->mModel;
break;
}
}
} }
} }
try try
{ {
osg::ref_ptr<const osg::Node> node = mResourceSystem->getSceneManager()->getTemplate(itemModel); osg::ref_ptr<osg::Node> attached = attach(itemModel, bonename, bonename, item.getType() == ESM::Light::sRecordId);
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<osg::Node> attached = SceneUtil::attach(node, mObjectRoot, bonename, found->second.get());
scene.reset(new PartHolder(attached)); scene.reset(new PartHolder(attached));

@ -562,8 +562,8 @@ bool LocalMap::isPositionExplored (float nX, float nY, int x, int y)
if (!segment.mFogOfWarImage) if (!segment.mFogOfWarImage)
return false; return false;
nX = std::max(0.f, std::min(1.f, nX)); nX = std::clamp(nX, 0.f, 1.f);
nY = std::max(0.f, std::min(1.f, nY)); nY = std::clamp(nY, 0.f, 1.f);
int texU = static_cast<int>((sFogOfWarResolution - 1) * nX); int texU = static_cast<int>((sFogOfWarResolution - 1) * nX);
int texV = static_cast<int>((sFogOfWarResolution - 1) * nY); int texV = static_cast<int>((sFogOfWarResolution - 1) * nY);
@ -648,7 +648,7 @@ void LocalMap::updatePlayer (const osg::Vec3f& position, const osg::Quat& orient
uint32_t clr = *(uint32_t*)data; uint32_t clr = *(uint32_t*)data;
uint8_t alpha = (clr >> 24); 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); uint32_t val = (uint32_t) (alpha << 24);
if ( *data != val) if ( *data != val)
{ {

@ -82,34 +82,6 @@ std::string getVampireHead(const std::string& race, bool female)
return "meshes\\" + bodyPart->mModel; return "meshes\\" + bodyPart->mModel;
} }
std::string getShieldBodypartMesh(const std::vector<ESM::PartReference>& bodyparts, bool female)
{
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
const MWWorld::Store<ESM::BodyPart> &partStore = store.get<ESM::BodyPart>();
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(); 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); std::string mesh = getShieldMesh(shield, !mNpc->isMale());
const ESM::Armor *armor = shield.get<ESM::Armor>()->mBase;
const std::vector<ESM::PartReference>& 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());
if (mesh.empty()) if (mesh.empty())
return std::string(); return std::string();
@ -678,7 +645,7 @@ void NpcAnimation::updateParts()
{ {
const ESM::Light *light = part.get<ESM::Light>()->mBase; const ESM::Light *light = part.get<ESM::Light>()->mBase;
addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft,
1, "meshes\\"+light->mModel); 1, "meshes\\"+light->mModel, false, nullptr, true);
if (mObjectParts[ESM::PRT_Shield]) if (mObjectParts[ESM::PRT_Shield])
addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), light); 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<const osg::Node> templateNode = mResourceSystem->getSceneManager()->getTemplate(model); osg::ref_ptr<osg::Node> attached = attach(model, bonename, bonefilter, isLight);
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<osg::Node> attached = SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second);
if (enchantedGlow) if (enchantedGlow)
mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor); 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; 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]) if(priority <= mPartPriorities[type])
return false; return false;
@ -813,7 +773,7 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g
if (weaponBonename != bonename) if (weaponBonename != bonename)
{ {
const NodeMap& nodeMap = getNodeMap(); 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()) if (found != nodeMap.end())
bonename = weaponBonename; 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 // 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; 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) catch (std::exception& e)
{ {
@ -1011,13 +971,10 @@ void NpcAnimation::showCarriedLeft(bool show)
// For shields we must try to use the body part model // For shields we must try to use the body part model
if (iter->getType() == ESM::Armor::sRecordId) if (iter->getType() == ESM::Armor::sRecordId)
{ {
const ESM::Armor *armor = iter->get<ESM::Armor>()->mBase; mesh = getShieldMesh(*iter, !mNpc->isMale());
const std::vector<ESM::PartReference>& bodyparts = armor->mParts.mParts;
if (!bodyparts.empty())
mesh = getShieldBodypartMesh(bodyparts, !mNpc->isMale());
} }
if (mesh.empty() || addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1, 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()) if (mesh.empty())
reserveIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1); reserveIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1);

@ -83,13 +83,13 @@ private:
NpcType getNpcType() const; NpcType getNpcType() const;
PartHolderPtr insertBoundedPart(const std::string &model, const std::string &bonename, 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 removeIndividualPart(ESM::PartReferenceType type);
void reserveIndividualPart(ESM::PartReferenceType type, int group, int priority); void reserveIndividualPart(ESM::PartReferenceType type, int group, int priority);
bool addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, 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 removePartGroup(int group);
void addPartGroup(int group, int priority, const std::vector<ESM::PartReference> &parts, void addPartGroup(int group, int priority, const std::vector<ESM::PartReference> &parts,
bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr); bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr);
@ -105,7 +105,7 @@ private:
protected: protected:
void addControllers() override; void addControllers() override;
bool isArrowAttached() const override; bool isArrowAttached() const override;
std::string getShieldMesh(const MWWorld::ConstPtr& shield) const override; std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const override;
public: public:
/** /**

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save