Merge remote-tracking branch 'upstream/master'

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

@ -8,6 +8,9 @@ stages:
- docker
- linux
image: debian:bullseye
rules:
- if: $CI_PIPELINE_SOURCE == "push"
.Debian:
extends: .Debian_Image
@ -52,7 +55,7 @@ Coverity:
extends: .Debian_Image
stage: build
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
- if: $CI_PIPELINE_SOURCE == "schedule"
before_script:
- CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic coverity
- curl -o /tmp/cov-analysis-linux64.tgz https://scan.coverity.com/download/linux64 --form project=$COVERITY_SCAN_PROJECT_NAME --form token=$COVERITY_SCAN_TOKEN
@ -70,7 +73,6 @@ Coverity:
variables:
CC: gcc
CXX: g++
artifacts:
Debian_GCC:
extends: .Debian
@ -162,6 +164,7 @@ Debian_Clang_tests_Debug:
only:
variables:
- $CI_PROJECT_ID == "7107382"
- $CI_PIPELINE_SOURCE == "push"
cache:
paths:
- ccache/
@ -184,7 +187,6 @@ Debian_Clang_tests_Debug:
macOS11_Xcode12:
extends: .MacOS
image: macos-11-xcode-12
allow_failure: true
cache:
key: macOS11_Xcode12.v1
variables:
@ -193,6 +195,7 @@ macOS11_Xcode12:
macOS10.15_Xcode11:
extends: .MacOS
image: macos-10.15-xcode-11
allow_failure: true
cache:
key: macOS10.15_Xcode11.v1
variables:
@ -213,6 +216,8 @@ variables: &tests-targets
.Windows_Ninja_Base:
tags:
- windows
rules:
- if: $CI_PIPELINE_SOURCE == "push"
before_script:
- Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
- choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1
@ -329,6 +334,8 @@ Windows_Ninja_Tests_RelWithDebInfo:
.Windows_MSBuild_Base:
tags:
- windows
rules:
- if: $CI_PIPELINE_SOURCE == "push"
before_script:
- Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
- choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1
@ -389,6 +396,15 @@ Windows_Ninja_Tests_RelWithDebInfo:
- MSVC2019_64/*/*/*/*/*/*/*.log
- MSVC2019_64/*/*/*/*/*/*/*/*.log
Daily_Windows_MSBuild_Engine_Release:on-schedule:
extends:
- .Windows_MSBuild_Base
variables:
<<: *engine-targets
config: "Release"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
Windows_MSBuild_Engine_Release:
extends:
- .Windows_MSBuild_Base
@ -444,6 +460,8 @@ Debian_AndroidNDK_arm64-v8a:
tags:
- linux
image: debian:bullseye
rules:
- if: $CI_PIPELINE_SOURCE == "push"
variables:
CCACHE_SIZE: 3G
cache:

@ -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 #3514: Editing a reference's position after loading an esp file makes the reference disappear
Bug #3737: Scripts from The Underground 2 .esp do not play (all patched versions)
Bug #3792: 1 frame late magicka recalc breaks early scripted magicka reactions to Intelligence change
Bug #3846: Strings starting with "-" fail to compile if not enclosed in quotes
Bug #3905: Great House Dagoth issues
Bug #4203: Resurrecting an actor should close the loot GUI
Bug #4602: Robert's Bodies: crash inside createInstance()
Bug #4700: Editor: Incorrect command implementation
Bug #4744: Invisible particles must still be processed
Bug #5088: Sky abruptly changes direction during certain weather transitions
Bug #5100: Persuasion doesn't always clamp the resulting disposition
Bug #5120: Scripted object spawning updates physics system
Bug #5207: Loose summons can be present in scene
@ -25,6 +27,8 @@
Bug #5801: A multi-effect spell with the intervention effects and recall always favors Almsivi intervention
Bug #5842: GetDisposition adds temporary disposition change from different actors
Bug #5863: GetEffect should return true after the player has teleported
Bug #5913: Failed assertion during Ritual of Trees quest
Bug #5937: Lights always need to be rotated by 90 degrees
Bug #6037: Morrowind Content Language Cannot be Set to English in OpenMW Launcher
Bug #6051: NaN water height in ESM file is not handled gracefully
Bug #6066: addtopic "return" does not work from within script. No errors thrown
@ -40,10 +44,13 @@
Bug #6133: Cannot reliably sneak or steal in the sight of the NPCs siding with player
Bug #6143: Capturing a screenshot makes engine to be a temporary unresponsive
Bug #6165: Paralyzed player character can pickup items when the inventory is open
Bug #6168: Weather particles flicker for a frame at start of storms
Bug #6172: Some creatures can't open doors
Bug #6174: Spellmaking and Enchanting sliders differences from vanilla
Bug #6184: Command and Calm and Demoralize and Frenzy and Rally magic effects inconsistencies with vanilla
Bug #6197: Infinite Casting Loop
Bug #6253: Multiple instances of Reflect stack additively
Bug #6255: Reflect is different from vanilla
Bug #6258: Barter menu glitches out when modifying prices
Bug #6273: Respawning NPCs rotation is inconsistent
Bug #6282: Laura craft doesn't follow the player character
@ -57,6 +64,9 @@
Bug #6322: Total sold/cost should reset to 0 when there are no items offered
Bug #6323: Wyrmhaven: Alboin doesn't follower the player character out of his house
Bug #6326: Detect Enchantment/Key should detect items in unresolved containers
Bug #6347: PlaceItem/PlaceItemCell/PlaceAt should work with levelled creatures
Bug #6363: Some scripts in Morrowland fail to work
Bug #6376: Creatures should be able to use torches
Feature #890: OpenMW-CS: Column filtering
Feature #2554: Modifying an object triggers the instances table to scroll to the corresponding record
Feature #2780: A way to see current OpenMW version in the console
@ -72,6 +82,7 @@
Feature #6017: Separate persistent and temporary cell references when saving
Feature #6032: Reverse-z depth buffer
Feature #6078: First person should not clear depth buffer
Feature #6161: Refactor Sky to use shaders and GLES/GL3 friendly
Feature #6162: Refactor GUI to use shaders and to be GLES and GL3+ friendly
Feature #6199: Support FBO Rendering
Feature #6249: Alpha testing support for Collada
@ -80,7 +91,6 @@
Task #6201: Remove the "Note: No relevant classes found. No output generated" warnings
Task #6264: Remove the old classes in animation.cpp
0.47.0
------

@ -199,6 +199,10 @@ if (WIN32)
option(USE_DEBUG_CONSOLE "whether a debug console should be enabled for debug builds, if false debug output is redirected to Visual Studio output" ON)
endif()
if(MSVC)
add_compile_options("/utf-8")
endif()
# Dependencies
find_package(OpenGL REQUIRED)
@ -707,6 +711,8 @@ endif()
if (BUILD_OPENMW AND APPLE)
# Without these flags LuaJit crashes on startup on OSX
set_target_properties(openmw PROPERTIES LINK_FLAGS "-pagezero_size 10000 -image_base 100000000")
target_compile_definitions(components PRIVATE GL_SILENCE_DEPRECATION=1)
target_compile_definitions(openmw PRIVATE GL_SILENCE_DEPRECATION=1)
endif()
# Apple bundling

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

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

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

@ -52,7 +52,7 @@ namespace CSMWorld
QVariant LandPluginIndexColumn::get(const Record<Land>& record) const
{
return record.get().mPlugin;
return record.get().getPlugin();
}
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));
}
bool CSMWorld::ScriptContext::isJournalId (const std::string& name) const
{
return mData.getJournals().searchId (name)!=-1;
}
void CSMWorld::ScriptContext::invalidateIds()
{
mIdsUpdated = false;

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

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

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

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

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

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

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

@ -124,9 +124,11 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
engine.addGroundcoverFile(file);
}
StringsVector luaScriptLists = variables["lua-scripts"].as<Files::EscapeStringVector>().toStdStringVector();
for (const auto& file : luaScriptLists)
engine.addLuaScriptListFile(file);
if (variables.count("lua-scripts"))
{
Log(Debug::Warning) << "Lua scripts have been specified via the old lua-scripts option and will not be loaded. "
"Please update them to a version which uses the new omwscripts format.";
}
// startup-settings
engine.setCell(variables["start"].as<Files::EscapeHashString>().toStdString());

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

@ -289,7 +289,7 @@ namespace MWBase
virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) = 0;
///< @return an updated Ptr
virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) = 0;
virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, const osg::Vec3f& vec) = 0;
///< @return an updated Ptr
virtual void scaleObject (const MWWorld::Ptr& ptr, float scale) = 0;

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

@ -5,6 +5,7 @@
#include "../mwmechanics/levelledlist.hpp"
#include "../mwworld/cellstore.hpp"
#include "../mwworld/customdata.hpp"
#include "../mwmechanics/creaturestats.hpp"
@ -27,6 +28,24 @@ namespace MWClass
}
};
MWWorld::Ptr CreatureLevList::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const
{
const MWWorld::LiveCellRef<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
{
return "";

@ -32,6 +32,10 @@ namespace MWClass
///< Write additional state from \a ptr into \a state.
void respawn (const MWWorld::Ptr& ptr) const override;
MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override;
void adjustPosition(const MWWorld::Ptr& ptr, bool force) const override;
};
}

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

@ -421,7 +421,7 @@ namespace MWDialogue
// Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with intimidate)
npcStats.setBaseDisposition(0);
int zero = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false);
int disposition = std::min(100 - zero, std::max(mOriginalDisposition + mPermanentDispositionChange, -zero));
int disposition = std::clamp(mOriginalDisposition + mPermanentDispositionChange, -zero, 100 - zero);
npcStats.setBaseDisposition(disposition);
}

@ -347,8 +347,7 @@ namespace MWGui
{
if (!mScrollBar->getVisible())
return;
mScrollBar->setScrollPosition(std::min(static_cast<int>(mScrollBar->getScrollRange()-1),
std::max(0, static_cast<int>(mScrollBar->getScrollPosition() - _rel*0.3))));
mScrollBar->setScrollPosition(std::clamp<int>(mScrollBar->getScrollPosition() - _rel*0.3, 0, mScrollBar->getScrollRange() - 1));
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()));
mCharge->setCaption(std::to_string(mEnchanting.getGemCharge()));
mSuccessChance->setCaption(std::to_string(std::max(0, std::min(100, mEnchanting.getEnchantChance()))));
mSuccessChance->setCaption(std::to_string(std::clamp(mEnchanting.getEnchantChance(), 0, 100)));
mCastCost->setCaption(std::to_string(mEnchanting.getEffectiveCastCost()));
mPrice->setCaption(std::to_string(mEnchanting.getEnchantPrice()));

@ -610,7 +610,7 @@ namespace MWGui
static const float fNPCHealthBarFade = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fNPCHealthBarFade")->mValue.getFloat();
if (fNPCHealthBarFade > 0.f)
mEnemyHealth->setAlpha(std::max(0.f, std::min(1.f, mEnemyHealthTimer/fNPCHealthBarFade)));
mEnemyHealth->setAlpha(std::clamp(mEnemyHealthTimer / fNPCHealthBarFade, 0.f, 1.f));
}

@ -70,7 +70,7 @@ namespace MWGui
, mTrading(false)
, mUpdateTimer(0.f)
{
mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture()));
mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture(), mPreview->getTextureStateSet()));
mPreview->rebuild();
mMainWidget->castType<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)
{
Utf8Stream stream (i->first.c_str());
Utf8Stream::UnicodeChar first = Misc::StringUtils::toLowerUtf8(stream.peek());
Utf8Stream::UnicodeChar first = Utf8Stream::toLowerUtf8(stream.peek());
if (first != Misc::StringUtils::toLowerUtf8(character))
if (first != Utf8Stream::toLowerUtf8(character))
continue;
visitor (i->second.getName());

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

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

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

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

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

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

@ -70,7 +70,7 @@ namespace MWInput
}
float deadZoneRadius = Settings::Manager::getFloat("joystick dead zone", "Input");
deadZoneRadius = std::min(std::max(deadZoneRadius, 0.0f), 0.5f);
deadZoneRadius = std::clamp(deadZoneRadius, 0.f, 0.5f);
mBindingsManager->setJoystickDeadZone(deadZoneRadius);
}

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

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

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

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

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

@ -20,7 +20,7 @@ namespace MWLua
{
public:
static void initializeSelfPackage(const Context&);
LocalScripts(LuaUtil::LuaState* lua, const LObject& obj);
LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode);
MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; }
@ -39,7 +39,7 @@ namespace MWLua
};
using EngineEvent = std::variant<OnActive, OnInactive, OnConsume>;
void receiveEngineEvent(const EngineEvent&, ObjectRegistry*);
void receiveEngineEvent(const EngineEvent&);
protected:
SelfObject mData;

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

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

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

@ -19,25 +19,12 @@
namespace MWLua
{
// Wrapper for a single-argument Lua function.
// Holds information about the script the function belongs to.
// Needed to prevent callback calls if the script was removed.
struct Callback
{
static constexpr std::string_view SCRIPT_NAME_KEY = "name";
sol::function mFunc;
sol::table mHiddenData;
void operator()(sol::object arg) const;
};
class LuaManager : public MWBase::LuaManager
{
public:
LuaManager(const VFS::Manager* vfs, const std::vector<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();
// Called by engine.cpp every frame. For performance reasons it works in a separate
@ -49,7 +36,8 @@ namespace MWLua
// Available everywhere through the MWBase::LuaManager interface.
// LuaManager queues these events and propagates to scripts on the next `update` call.
void newGameStarted() override { mGlobalScripts.newGameStarted(); }
void newGameStarted() override;
void gameLoaded() override;
void objectAddedToScene(const MWWorld::Ptr& ptr) override;
void objectRemovedFromScene(const MWWorld::Ptr& ptr) override;
void registerObject(const MWWorld::Ptr& ptr) override;
@ -62,8 +50,8 @@ namespace MWLua
void clear() override; // should be called before loading game or starting a new game to reset internal state.
void setupPlayer(const MWWorld::Ptr& ptr) override; // Should be called once after each "clear".
// Used only in luabindings
void addLocalScript(const MWWorld::Ptr&, const std::string& scriptPath);
// Used only in Lua bindings
void addCustomLocalScript(const MWWorld::Ptr&, int scriptId);
void addAction(std::unique_ptr<Action>&& action) { mActionQueue.push_back(std::move(action)); }
void addTeleportPlayerAction(std::unique_ptr<TeleportAction>&& action) { mTeleportPlayerAction = std::move(action); }
void addUIMessage(std::string_view message) { mUIMessages.emplace_back(message); }
@ -81,21 +69,27 @@ namespace MWLua
void reloadAllScripts() override;
// Used to call Lua callbacks from C++
void queueCallback(Callback callback, sol::object arg) { mQueuedCallbacks.push_back({std::move(callback), std::move(arg)}); }
void queueCallback(LuaUtil::Callback callback, sol::object arg)
{
mQueuedCallbacks.push_back({std::move(callback), std::move(arg)});
}
// Wraps Lua callback into an std::function.
// NOTE: Resulted function is not thread safe. Can not be used while LuaManager::update() or
// any other Lua-related function is running.
template <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)); };
}
private:
LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr);
void initConfiguration();
LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags);
bool mInitialized = false;
bool mGlobalScriptsStarted = false;
LuaUtil::ScriptsConfiguration mConfiguration;
LuaUtil::LuaState mLua;
sol::table mNearbyPackage;
sol::table mUserInterfacePackage;
@ -104,12 +98,12 @@ namespace MWLua
sol::table mLocalSettingsPackage;
sol::table mPlayerSettingsPackage;
std::vector<std::string> mGlobalScriptList;
GlobalScripts mGlobalScripts{&mLua};
std::set<LocalScripts*> mActiveLocalScripts;
WorldView mWorldView;
bool mPlayerChanged = false;
bool mNewGameStarted = false;
MWWorld::Ptr mPlayer;
GlobalEventQueue mGlobalEvents;
@ -127,7 +121,7 @@ namespace MWLua
struct CallbackWithData
{
Callback mCallback;
LuaUtil::Callback mCallback;
sol::object mArg;
};
std::vector<CallbackWithData> mQueuedCallbacks;

@ -1,19 +1,6 @@
#include "object.hpp"
#include "../mwclass/activator.hpp"
#include "../mwclass/armor.hpp"
#include "../mwclass/book.hpp"
#include "../mwclass/clothing.hpp"
#include "../mwclass/container.hpp"
#include "../mwclass/creature.hpp"
#include "../mwclass/door.hpp"
#include "../mwclass/ingredient.hpp"
#include "../mwclass/light.hpp"
#include "../mwclass/misc.hpp"
#include "../mwclass/npc.hpp"
#include "../mwclass/potion.hpp"
#include "../mwclass/static.hpp"
#include "../mwclass/weapon.hpp"
#include <unordered_map>
namespace MWLua
{
@ -23,28 +10,34 @@ namespace MWLua
return std::to_string(id.mIndex) + "_" + std::to_string(id.mContentFile);
}
const static std::map<std::type_index, std::string_view> classNames = {
{typeid(MWClass::Activator), "Activator"},
{typeid(MWClass::Armor), "Armor"},
{typeid(MWClass::Book), "Book"},
{typeid(MWClass::Clothing), "Clothing"},
{typeid(MWClass::Container), "Container"},
{typeid(MWClass::Creature), "Creature"},
{typeid(MWClass::Door), "Door"},
{typeid(MWClass::Ingredient), "Ingredient"},
{typeid(MWClass::Light), "Light"},
{typeid(MWClass::Miscellaneous), "Miscellaneous"},
{typeid(MWClass::Npc), "NPC"},
{typeid(MWClass::Potion), "Potion"},
{typeid(MWClass::Static), "Static"},
{typeid(MWClass::Weapon), "Weapon"},
struct LuaObjectTypeInfo
{
std::string_view mName;
ESM::LuaScriptCfg::Flags mFlag = 0;
};
const static std::unordered_map<ESM::RecNameInts, LuaObjectTypeInfo> luaObjectTypeInfo = {
{ESM::REC_ACTI, {"Activator", ESM::LuaScriptCfg::sActivator}},
{ESM::REC_ARMO, {"Armor", ESM::LuaScriptCfg::sArmor}},
{ESM::REC_BOOK, {"Book", ESM::LuaScriptCfg::sBook}},
{ESM::REC_CLOT, {"Clothing", ESM::LuaScriptCfg::sClothing}},
{ESM::REC_CONT, {"Container", ESM::LuaScriptCfg::sContainer}},
{ESM::REC_CREA, {"Creature", ESM::LuaScriptCfg::sCreature}},
{ESM::REC_DOOR, {"Door", ESM::LuaScriptCfg::sDoor}},
{ESM::REC_INGR, {"Ingredient", ESM::LuaScriptCfg::sIngredient}},
{ESM::REC_LIGH, {"Light", ESM::LuaScriptCfg::sLight}},
{ESM::REC_MISC, {"Miscellaneous", ESM::LuaScriptCfg::sMiscItem}},
{ESM::REC_NPC_, {"NPC", ESM::LuaScriptCfg::sNPC}},
{ESM::REC_ALCH, {"Potion", ESM::LuaScriptCfg::sPotion}},
{ESM::REC_STAT, {"Static"}},
{ESM::REC_WEAP, {"Weapon", ESM::LuaScriptCfg::sWeapon}},
};
std::string_view 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);
if (it != classNames.end())
return it->second;
auto it = luaObjectTypeInfo.find(type);
if (it != luaObjectTypeInfo.end())
return it->second.mName;
else
return fallback;
}
@ -55,13 +48,31 @@ namespace MWLua
return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker";
}
std::string_view getMWClassName(const MWWorld::Ptr& ptr)
std::string_view getLuaObjectTypeName(const MWWorld::Ptr& ptr)
{
// Behaviour of this function is a part of OpenMW Lua API. We can not just return
// `ptr.getTypeDescription()` because its implementation is distributed over many files
// and can be accidentally changed. We use `ptr.getTypeDescription()` only as a fallback
// for types that are not present in `luaObjectTypeInfo` (for such types result stability
// is not necessary because they are not listed in OpenMW Lua documentation).
if (ptr.getCellRef().getRefIdRef() == "player")
return "Player";
if (isMarker(ptr))
return "Marker";
return getMWClassName(typeid(ptr.getClass()));
return getLuaObjectTypeName(static_cast<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)
@ -69,7 +80,7 @@ namespace MWLua
std::string res = "object";
res.append(idToString(getId(ptr)));
res.append(" (");
res.append(getMWClassName(ptr));
res.append(getLuaObjectTypeName(ptr));
res.append(", ");
res.append(ptr.getCellRef().getRefIdRef());
res.append(")");

@ -4,6 +4,8 @@
#include <typeindex>
#include <components/esm/cellref.hpp>
#include <components/esm/defs.hpp>
#include <components/esm/luascripts.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
@ -19,8 +21,12 @@ namespace MWLua
std::string idToString(const ObjectId& id);
std::string ptrToString(const MWWorld::Ptr& ptr);
bool isMarker(const MWWorld::Ptr& ptr);
std::string_view getMWClassName(const std::type_index& cls_type, std::string_view fallback = "Unknown");
std::string_view getMWClassName(const MWWorld::Ptr& ptr);
std::string_view getLuaObjectTypeName(ESM::RecNameInts recordType, std::string_view fallback = "Unknown");
std::string_view getLuaObjectTypeName(const MWWorld::Ptr& ptr);
// Each script has a set of flags that controls to which objects the script should be
// automatically attached. This function maps each object types to one of the flags.
ESM::LuaScriptCfg::Flags getLuaScriptFlag(const MWWorld::Ptr& ptr);
// Holds a mapping ObjectId -> MWWord::Ptr.
class ObjectRegistry
@ -64,7 +70,7 @@ namespace MWLua
ObjectId id() const { return mId; }
std::string toString() const;
std::string_view type() const { return getMWClassName(ptr()); }
std::string_view type() const { return getLuaObjectTypeName(ptr()); }
// Updates and returns the underlying Ptr. Throws an exception if object is not available.
const MWWorld::Ptr& ptr() const;

@ -42,13 +42,12 @@ namespace MWLua
template <typename ObjT>
using Cell = std::conditional_t<std::is_same_v<ObjT, LObject>, LCell, GCell>;
template <class Class>
static const MWWorld::Ptr& requireClass(const MWWorld::Ptr& ptr)
static const MWWorld::Ptr& requireRecord(ESM::RecNameInts recordType, const MWWorld::Ptr& ptr)
{
if (typeid(Class) != typeid(ptr.getClass()))
if (ptr.getType() != recordType)
{
std::string msg = "Requires type '";
msg.append(getMWClassName(typeid(Class)));
msg.append(getLuaObjectTypeName(recordType));
msg.append("', but applied to ");
msg.append(ptrToString(ptr));
throw std::runtime_error(msg);
@ -141,9 +140,43 @@ namespace MWLua
if constexpr (std::is_same_v<ObjectT, GObject>)
{ // 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,
@ -189,7 +222,7 @@ namespace MWLua
template <class ObjectT>
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)
{

@ -13,7 +13,7 @@ namespace MWLua
class PlayerScripts : public LocalScripts
{
public:
PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj)
PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj, ESM::LuaScriptCfg::sPlayer)
{
registerEngineHandlers({&mKeyPressHandlers, &mKeyReleaseHandlers,
&mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers,

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

@ -50,6 +50,8 @@ namespace MWMechanics
ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor);
ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor);
ESM::ActiveSpells::ActiveSpellParams toEsm() const;
friend class ActiveSpells;

@ -240,10 +240,7 @@ namespace MWMechanics
{
// magic effects
adjustMagicEffects (ptr, duration);
if (ptr.getClass().getCreatureStats(ptr).needToRecalcDynamicStats())
calculateDynamicStats (ptr);
calculateCreatureStatModifiers (ptr, duration);
// fatigue restoration
calculateRestoration(ptr, duration);
}
@ -654,29 +651,6 @@ namespace MWMechanics
updateSummons(creature, mTimerDisposeSummonsCorpses == 0.f);
}
void Actors::calculateDynamicStats (const MWWorld::Ptr& ptr)
{
CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr);
float intelligence = creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified();
float base = 1.f;
if (ptr == getPlayer())
base = MWBase::Environment::get().getWorld()->getStore().get<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)
{
MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr);
@ -771,14 +745,6 @@ namespace MWMechanics
stats.setFatigue (fatigue);
}
void Actors::calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration)
{
CreatureStats &creatureStats = ptr.getClass().getCreatureStats(ptr);
if (creatureStats.needToRecalcDynamicStats())
calculateDynamicStats(ptr);
}
bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr)
{
PtrActorMap::iterator it = mActors.find(ptr);
@ -1047,13 +1013,10 @@ namespace MWMechanics
void Actors::updateProcessingRange()
{
// We have to cap it since using high values (larger than 7168) will make some quests harder or impossible to complete (bug #1876)
static const float maxProcessingRange = 7168.f;
static const float minProcessingRange = maxProcessingRange / 2.f;
static const float maxRange = 7168.f;
static const float minRange = maxRange / 2.f;
float actorsProcessingRange = Settings::Manager::getFloat("actors processing range", "Game");
actorsProcessingRange = std::min(actorsProcessingRange, maxProcessingRange);
actorsProcessingRange = std::max(actorsProcessingRange, minProcessingRange);
mActorsProcessingRange = actorsProcessingRange;
mActorsProcessingRange = std::clamp(Settings::Manager::getFloat("actors processing range", "Game"), minRange, maxRange);
}
void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately)
@ -1349,7 +1312,7 @@ namespace MWMechanics
angleToApproachingActor = std::atan2(deltaPos.x(), deltaPos.y());
osg::Vec2f posAtT = relPos + relSpeed * t;
float coef = (posAtT.x() * relSpeed.x() + posAtT.y() * relSpeed.y()) / (collisionDist * collisionDist * maxSpeed);
coef *= osg::clampBetween((maxDistForPartialAvoiding - dist) / (maxDistForPartialAvoiding - maxDistForStrictAvoiding), 0.f, 1.f);
coef *= std::clamp((maxDistForPartialAvoiding - dist) / (maxDistForPartialAvoiding - maxDistForStrictAvoiding), 0.f, 1.f);
movementCorrection = posAtT * coef;
if (otherPtr.getClass().getCreatureStats(otherPtr).isDead())
// In case of dead body still try to go around (it looks natural), but reduce the correction twice.
@ -1530,15 +1493,13 @@ namespace MWMechanics
stats.getAiSequence().execute(iter->first, *ctrl, duration, /*outOfRange*/true);
}
if(iter->first.getClass().isNpc())
if(inProcessingRange && iter->first.getClass().isNpc())
{
// We can not update drowning state for actors outside of AI distance - they can not resurface to breathe
if (inProcessingRange)
updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer);
if (timerUpdateEquippedLight == 0)
updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches);
updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer);
}
if(timerUpdateEquippedLight == 0 && iter->first.getClass().hasInventoryStore(iter->first))
updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches);
if (luaControls && isConscious(iter->first))
{
@ -1711,10 +1672,6 @@ namespace MWMechanics
if (iter->first.getType() == ESM::Creature::sRecordId)
soulTrap(iter->first);
// Magic effects will be reset later, and the magic effect that could kill the actor
// needs to be determined now
calculateCreatureStatModifiers(iter->first, 0);
if (cls.isEssential(iter->first))
MWBase::Environment::get().getWindowManager()->messageBox("#{sKilledEssential}");
}
@ -1730,8 +1687,6 @@ namespace MWMechanics
// Make sure spell effects are removed
purgeSpellEffects(stats.getActorId());
// Reset dynamic stats, attributes and skills
calculateCreatureStatModifiers(iter->first, 0);
stats.getMagicEffects().add(ESM::MagicEffect::Vampirism, vampirism);
if (isPlayer)
@ -1816,10 +1771,6 @@ namespace MWMechanics
continue;
adjustMagicEffects (iter->first, duration);
if (iter->first.getClass().getCreatureStats(iter->first).needToRecalcDynamicStats())
calculateDynamicStats (iter->first);
calculateCreatureStatModifiers (iter->first, duration);
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(iter->first);
if (animation)
@ -2209,7 +2160,6 @@ namespace MWMechanics
void Actors::updateMagicEffects(const MWWorld::Ptr &ptr)
{
adjustMagicEffects(ptr, 0.f);
calculateCreatureStatModifiers(ptr, 0.f);
}
bool Actors::isReadyToBlock(const MWWorld::Ptr &ptr) const

@ -43,10 +43,6 @@ namespace MWMechanics
void adjustMagicEffects (const MWWorld::Ptr& creature, float duration);
void calculateDynamicStats (const MWWorld::Ptr& ptr);
void calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration);
void calculateRestoration (const MWWorld::Ptr& ptr, float duration);
void updateDrowning (const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer);

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

@ -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::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);
struct CreatureCustomDataResetter
{
MWWorld::Ptr mPtr;
CreatureCustomDataResetter(const MWWorld::Ptr& ptr);
~CreatureCustomDataResetter();
};
}
#endif

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

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

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

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

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

@ -356,10 +356,10 @@ namespace MWMechanics
ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass;
if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo)
{
static const float multiplier = std::max(0.f, std::min(1.0f, Settings::Manager::getFloat("projectiles enchant multiplier", "Game")));
static const float multiplier = std::clamp(Settings::Manager::getFloat("projectiles enchant multiplier", "Game"), 0.f, 1.f);
MWWorld::Ptr player = getPlayer();
int itemsInInventoryCount = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId());
count = std::min(itemsInInventoryCount, std::max(1, int(getGemCharge() * multiplier / enchantPoints)));
count = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId());
count = std::clamp<int>(getGemCharge() * multiplier / enchantPoints, 1, count);
}
}

@ -1,10 +1,20 @@
#include "magiceffects.hpp"
#include <cmath>
#include <stdexcept>
#include <components/esm/effectlist.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
{
EffectKey::EffectKey() : mId (0), mArg (-1) {}
@ -74,6 +84,7 @@ namespace MWMechanics
{
mModifier += param.mModifier;
mBase += param.mBase;
truncate(mModifier);
return *this;
}
@ -81,6 +92,7 @@ namespace MWMechanics
{
mModifier -= param.mModifier;
mBase -= param.mBase;
truncate(mModifier);
return *this;
}

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

@ -371,7 +371,7 @@ int MWMechanics::NpcStats::getReputation() const
void MWMechanics::NpcStats::setReputation(int reputation)
{
// Reputation is capped in original engine
mReputation = std::min(255, std::max(0, reputation));
mReputation = std::clamp(reputation, 0, 255);
}
int MWMechanics::NpcStats::getCrimeId() const

@ -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 "aifollow.hpp"
#include "creaturestats.hpp"
#include "spellabsorption.hpp"
#include "spelleffects.hpp"
#include "spellutil.hpp"
#include "summoning.hpp"
#include "weapontype.hpp"
namespace
{
bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect,
const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects)
{
if (caster.isEmpty() || caster == target || !target.getClass().isActor())
return false;
bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful;
bool isUnreflectable = magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable;
if (!isHarmful || isUnreflectable)
return false;
float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude();
if (Misc::Rng::roll0to99() >= reflect)
return false;
const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get<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
{
CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool manualSpell)
@ -82,7 +55,7 @@ namespace MWMechanics
}
void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster,
const ESM::EffectList &effects, ESM::RangeType range, bool reflected, bool exploded)
const ESM::EffectList &effects, ESM::RangeType range, bool exploded)
{
const bool targetIsActor = !target.isEmpty() && target.getClass().isActor();
if (targetIsActor)
@ -123,7 +96,6 @@ namespace MWMechanics
}
}
ESM::EffectList reflectedEffects;
ActiveSpells::ActiveSpellParams params(*this, caster);
bool castByPlayer = (!caster.isEmpty() && caster == getPlayer());
@ -136,9 +108,6 @@ namespace MWMechanics
// throughout the iteration of this spell's
// effects, we display a "can't re-cast" message
// Try absorbing the spell. Some handling must still happen for absorbed effects.
bool absorbed = absorbSpell(mId, caster, target);
int currentEffectIndex = 0;
for (std::vector<ESM::ENAMstruct>::const_iterator effectIt (effects.mList.begin());
!target.isEmpty() && effectIt != effects.mList.end(); ++effectIt, ++currentEffectIndex)
@ -167,19 +136,6 @@ namespace MWMechanics
&& (caster.isEmpty() || !caster.getClass().isActor()))
continue;
// Notify the target actor they've been hit
bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful;
if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful)
target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true);
// Avoid proceeding further for absorbed spells.
if (absorbed)
continue;
// Reflect harmful effects
if (!reflected && reflectEffect(*effectIt, magicEffect, caster, target, reflectedEffects))
continue;
ActiveSpells::ActiveEffect effect;
effect.mEffectId = effectIt->mEffectID;
effect.mArg = MWMechanics::EffectKey(*effectIt).mArg;
@ -189,13 +145,8 @@ namespace MWMechanics
effect.mTimeLeft = 0.f;
effect.mEffectIndex = currentEffectIndex;
effect.mFlags = ESM::ActiveEffect::Flag_None;
// Avoid applying harmful effects to the player in god mode
if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful)
{
effect.mMinMagnitude = 0;
effect.mMaxMagnitude = 0;
}
if(mManualSpell)
effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect;
bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration);
effect.mDuration = hasDuration ? static_cast<float>(effectIt->mDuration) : 1.f;
@ -209,14 +160,14 @@ namespace MWMechanics
// add to list of active effects, to apply in next frame
params.getEffects().emplace_back(effect);
bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth;
bool effectAffectsHealth = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth;
if (castByPlayer && target != caster && targetIsActor && effectAffectsHealth)
{
// If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar.
MWBase::Environment::get().getWindowManager()->setEnemy(target);
}
if (targetIsActor || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)
if (!targetIsActor && magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)
{
playEffects(target, *magicEffect);
}
@ -227,9 +178,6 @@ namespace MWMechanics
if (!target.isEmpty())
{
if (!reflectedEffects.mList.empty())
inflict(caster, target, reflectedEffects, range, true, exploded);
if (!params.getEffects().empty())
{
if(targetIsActor)
@ -237,6 +185,7 @@ namespace MWMechanics
else
{
// Apply effects instantly. We can ignore effect deletion since the entire params object gets deleted afterwards anyway
// and we can ignore reflection since non-actors cannot reflect spells
for(auto& effect : params.getEffects())
applyMagicEffect(target, caster, params, effect, 0.f);
}

@ -62,7 +62,7 @@ namespace MWMechanics
/// @note \a target can be any type of object, not just actors.
/// @note \a caster can be any type of object, or even an empty object.
void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster,
const ESM::EffectList& effects, ESM::RangeType range, bool reflected=false, bool exploded=false);
const ESM::EffectList& effects, ESM::RangeType range, bool exploded=false);
};
void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping = true);

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

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

@ -162,7 +162,10 @@ namespace MWMechanics
float castChance = baseChance + castBonus;
castChance *= stats.getFatigueTerm();
return std::max(0.f, cap ? std::min(100.f, castChance) : castChance);
if (cap)
return std::clamp(castChance, 0.f, 100.f);
return std::max(castChance, 0.f);
}
float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka)

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

@ -12,6 +12,7 @@
#include "collisiontype.hpp"
#include "mtphysics.hpp"
#include "trace.h"
#include <cmath>
@ -21,8 +22,8 @@ namespace MWPhysics
Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler, bool canWaterWalk)
: mStandingOnPtr(nullptr), mCanWaterWalk(canWaterWalk), mWalkingOnWater(false)
, mMeshTranslation(shape->mCollisionBox.center), mOriginalHalfExtents(shape->mCollisionBox.extents)
, mVelocity(0,0,0), mStuckFrames(0), mLastStuckPosition{0, 0, 0}
, mMeshTranslation(shape->mCollisionBox.mCenter), mOriginalHalfExtents(shape->mCollisionBox.mExtents)
, mStuckFrames(0), mLastStuckPosition{0, 0, 0}
, mForce(0.f, 0.f, 0.f), mOnGround(true), mOnSlope(false)
, mInternalCollisionMode(true)
, mExternalCollisionMode(true)
@ -123,7 +124,6 @@ void Actor::updatePosition()
mSimulationPosition = worldPosition;
mPositionOffset = osg::Vec3f();
mStandingOnPtr = nullptr;
mSkipCollisions = true;
mSkipSimulation = true;
}
@ -133,11 +133,6 @@ void Actor::setSimulationPosition(const osg::Vec3f& position)
mSimulationPosition = position;
}
osg::Vec3f Actor::getSimulationPosition() const
{
return mSimulationPosition;
}
osg::Vec3f Actor::getScaledMeshTranslation() const
{
return mRotation * osg::componentMultiply(mMeshTranslation, mScale);
@ -167,17 +162,19 @@ bool Actor::setPosition(const osg::Vec3f& position)
{
std::scoped_lock lock(mPositionMutex);
applyOffsetChange();
bool hasChanged = mPosition != position || mWorldPositionChanged;
mPreviousPosition = mPosition;
mPosition = position;
bool hasChanged = (mPosition != position && !mSkipSimulation) || mWorldPositionChanged;
if (!mSkipSimulation)
{
mPreviousPosition = mPosition;
mPosition = position;
}
return hasChanged;
}
void Actor::adjustPosition(const osg::Vec3f& offset, bool ignoreCollisions)
void Actor::adjustPosition(const osg::Vec3f& offset)
{
std::scoped_lock lock(mPositionMutex);
mPositionOffset += offset;
mSkipCollisions = mSkipCollisions || ignoreCollisions;
}
void Actor::applyOffsetChange()
@ -191,16 +188,6 @@ void Actor::applyOffsetChange()
mWorldPositionChanged = true;
}
osg::Vec3f Actor::getPosition() const
{
return mPosition;
}
osg::Vec3f Actor::getPreviousPosition() const
{
return mPreviousPosition;
}
void Actor::setRotation(osg::Quat quat)
{
std::scoped_lock lock(mPositionMutex);
@ -288,19 +275,15 @@ void Actor::setStandingOnPtr(const MWWorld::Ptr& ptr)
mStandingOnPtr = ptr;
}
bool Actor::skipCollisions()
{
return std::exchange(mSkipCollisions, false);
}
void Actor::setVelocity(osg::Vec3f velocity)
{
mVelocity = velocity;
}
osg::Vec3f Actor::velocity()
bool Actor::canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const
{
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 btCollisionObject;
class btCollisionWorld;
class btConvexShape;
namespace Resource
@ -59,7 +60,6 @@ namespace MWPhysics
* to account for e.g. scripted movements
*/
void setSimulationPosition(const osg::Vec3f& position);
osg::Vec3f getSimulationPosition() const;
void updateCollisionObjectPosition();
@ -89,15 +89,11 @@ namespace MWPhysics
void updatePosition();
// register a position offset that will be applied during simulation.
void adjustPosition(const osg::Vec3f& offset, bool ignoreCollisions);
void adjustPosition(const osg::Vec3f& offset);
// apply position offset. Can't be called during simulation
void applyOffsetChange();
osg::Vec3f getPosition() const;
osg::Vec3f getPreviousPosition() const;
/**
* Returns the half extents of the collision body (scaled according to rendering scale)
* @note The reason we need this extra method is because of an inconsistency in MW - NPC race scales aren't applied to the collision shape,
@ -160,10 +156,7 @@ namespace MWPhysics
mLastStuckPosition = position;
}
bool skipCollisions();
void setVelocity(osg::Vec3f velocity);
osg::Vec3f velocity();
bool canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const;
private:
MWWorld::Ptr mStandingOnPtr;
@ -190,13 +183,8 @@ namespace MWPhysics
osg::Quat mRotation;
osg::Vec3f mScale;
osg::Vec3f mSimulationPosition;
osg::Vec3f mPosition;
osg::Vec3f mPreviousPosition;
osg::Vec3f mPositionOffset;
osg::Vec3f mVelocity;
bool mWorldPositionChanged;
bool mSkipCollisions;
bool mSkipSimulation;
mutable std::mutex mPositionMutex;

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

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

@ -37,13 +37,15 @@ namespace MWPhysics
class Actor;
struct ActorFrameData;
struct ProjectileFrameData;
struct WorldFrameData;
class MovementSolver
{
public:
static osg::Vec3f traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight);
static void move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, WorldFrameData& worldData);
static void move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, const WorldFrameData& worldData);
static void move(ProjectileFrameData& projectile, float time, const btCollisionWorld* collisionWorld);
static void unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld);
};
}

@ -105,10 +105,134 @@ namespace
return actorData.mPosition.z() < actorData.mSwimLevel;
}
osg::Vec3f interpolateMovements(MWPhysics::Actor& actor, MWPhysics::ActorFrameData& actorData, float timeAccum, float physicsDt)
osg::Vec3f interpolateMovements(const MWPhysics::PtrHolder& ptr, float timeAccum, float physicsDt)
{
const float interpolationFactor = std::clamp(timeAccum / physicsDt, 0.0f, 1.0f);
return actorData.mPosition * interpolationFactor + actor.getPreviousPosition() * (1.f - interpolationFactor);
return ptr.getPosition() * interpolationFactor + ptr.getPreviousPosition() * (1.f - interpolationFactor);
}
namespace Visitors
{
struct InitPosition
{
const btCollisionWorld* mCollisionWorld;
void operator()(MWPhysics::ActorSimulation& sim) const
{
auto& [actor, frameData] = sim;
actor->applyOffsetChange();
frameData.mPosition = actor->getPosition();
if (frameData.mWaterCollision && frameData.mPosition.z() < frameData.mWaterlevel && actor->canMoveToWaterSurface(frameData.mWaterlevel, mCollisionWorld))
{
const auto offset = osg::Vec3f(0, 0, frameData.mWaterlevel - frameData.mPosition.z());
MWBase::Environment::get().getWorld()->moveObjectBy(actor->getPtr(), offset);
actor->applyOffsetChange();
frameData.mPosition = actor->getPosition();
}
frameData.mOldHeight = frameData.mPosition.z();
const auto rotation = actor->getPtr().getRefData().getPosition().asRotationVec3();
frameData.mRotation = osg::Vec2f(rotation.x(), rotation.z());
frameData.mInertia = actor->getInertialForce();
frameData.mStuckFrames = actor->getStuckFrames();
frameData.mLastStuckPosition = actor->getLastStuckPosition();
}
void operator()(MWPhysics::ProjectileSimulation& sim) const
{
}
};
struct PreStep
{
btCollisionWorld* mCollisionWorld;
void operator()(MWPhysics::ActorSimulation& sim) const
{
MWPhysics::MovementSolver::unstuck(sim.second, mCollisionWorld);
}
void operator()(MWPhysics::ProjectileSimulation& sim) const
{
}
};
struct UpdatePosition
{
btCollisionWorld* mCollisionWorld;
void operator()(MWPhysics::ActorSimulation& sim) const
{
auto& [actor, frameData] = sim;
if (actor->setPosition(frameData.mPosition))
{
frameData.mPosition = actor->getPosition(); // account for potential position change made by script
actor->updateCollisionObjectPosition();
mCollisionWorld->updateSingleAabb(actor->getCollisionObject());
}
}
void operator()(MWPhysics::ProjectileSimulation& sim) const
{
auto& [proj, frameData] = sim;
proj->setPosition(frameData.mPosition);
proj->updateCollisionObjectPosition();
mCollisionWorld->updateSingleAabb(proj->getCollisionObject());
}
};
struct Move
{
const float mPhysicsDt;
const btCollisionWorld* mCollisionWorld;
const MWPhysics::WorldFrameData& mWorldFrameData;
void operator()(MWPhysics::ActorSimulation& sim) const
{
MWPhysics::MovementSolver::move(sim.second, mPhysicsDt, mCollisionWorld, mWorldFrameData);
}
void operator()(MWPhysics::ProjectileSimulation& sim) const
{
MWPhysics::MovementSolver::move(sim.second, mPhysicsDt, mCollisionWorld);
}
};
struct Sync
{
const bool mAdvanceSimulation;
const float mTimeAccum;
const float mPhysicsDt;
const MWPhysics::PhysicsTaskScheduler* scheduler;
void operator()(MWPhysics::ActorSimulation& sim) const
{
auto& [actor, frameData] = sim;
auto ptr = actor->getPtr();
MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);
const float heightDiff = frameData.mPosition.z() - frameData.mOldHeight;
const bool isStillOnGround = (mAdvanceSimulation && frameData.mWasOnGround && frameData.mIsOnGround);
if (isStillOnGround || frameData.mFlying || isUnderWater(frameData) || frameData.mSlowFall < 1)
stats.land(ptr == MWMechanics::getPlayer() && (frameData.mFlying || isUnderWater(frameData)));
else if (heightDiff < 0)
stats.addToFallHeight(-heightDiff);
actor->setSimulationPosition(::interpolateMovements(*actor, mTimeAccum, mPhysicsDt));
actor->setLastStuckPosition(frameData.mLastStuckPosition);
actor->setStuckFrames(frameData.mStuckFrames);
if (mAdvanceSimulation)
{
MWWorld::Ptr standingOn;
auto* ptrHolder = static_cast<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
@ -235,13 +359,12 @@ namespace MWPhysics
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.
// While the mSimulationMutex is held, background physics threads can't run.
MaybeExclusiveLock lock(mSimulationMutex, mNumThreads);
assert(actors.size() == actorsData.size());
double timeStart = mTimer->tick();
@ -259,19 +382,19 @@ namespace MWPhysics
timeAccum -= numSteps*newDelta;
// init
for (size_t i = 0; i < actors.size(); ++i)
const Visitors::InitPosition vis{mCollisionWorld};
for (auto& sim : simulations)
{
actorsData[i].updatePosition(*actors[i], mCollisionWorld);
std::visit(vis, sim);
}
mPrevStepCount = numSteps;
mRemainingSteps = numSteps;
mTimeAccum = timeAccum;
mPhysicsDt = newDelta;
mActors = std::move(actors);
mActorsFrameData = std::move(actorsData);
mSimulations = std::move(simulations);
mAdvanceSimulation = (mRemainingSteps != 0);
mNewFrame = true;
mNumJobs = mActorsFrameData.size();
mNumJobs = mSimulations.size();
mNextLOS.store(0, std::memory_order_relaxed);
mNextJob.store(0, std::memory_order_release);
@ -301,8 +424,7 @@ namespace MWPhysics
MaybeExclusiveLock lock(mSimulationMutex, mNumThreads);
mBudget.reset(mDefaultPhysicsDt);
mAsyncBudget.reset(0.0f);
mActors.clear();
mActorsFrameData.clear();
mSimulations.clear();
for (const auto& [_, actor] : actors)
{
actor->updatePosition();
@ -448,7 +570,7 @@ namespace MWPhysics
}
else if (const auto projectile = std::dynamic_pointer_cast<Projectile>(ptr))
{
projectile->commitPositionChange();
projectile->updateCollisionObjectPosition();
mCollisionWorld->updateSingleAabb(projectile->getCollisionObject());
}
}
@ -467,47 +589,11 @@ namespace MWPhysics
void PhysicsTaskScheduler::updateActorsPositions()
{
for (size_t i = 0; i < mActors.size(); ++i)
const Visitors::UpdatePosition vis{mCollisionWorld};
for (auto& sim : mSimulations)
{
if (mActors[i]->setPosition(mActorsFrameData[i].mPosition))
{
MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads);
mActorsFrameData[i].mPosition = mActors[i]->getPosition(); // account for potential position change made by script
mActors[i]->updateCollisionObjectPosition();
mCollisionWorld->updateSingleAabb(mActors[i]->getCollisionObject());
}
}
}
void PhysicsTaskScheduler::updateActor(Actor& actor, ActorFrameData& actorData, bool simulationPerformed, float timeAccum, float dt) const
{
auto ptr = actor.getPtr();
MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);
const float heightDiff = actorData.mPosition.z() - actorData.mOldHeight;
const bool isStillOnGround = (simulationPerformed && actorData.mWasOnGround && actorData.mIsOnGround);
if (isStillOnGround || actorData.mFlying || isUnderWater(actorData) || actorData.mSlowFall < 1)
stats.land(ptr == MWMechanics::getPlayer() && (actorData.mFlying || isUnderWater(actorData)));
else if (heightDiff < 0)
stats.addToFallHeight(-heightDiff);
actor.setSimulationPosition(interpolateMovements(actor, actorData, timeAccum, dt));
actor.setLastStuckPosition(actorData.mLastStuckPosition);
actor.setStuckFrames(actorData.mStuckFrames);
if (simulationPerformed)
{
MWWorld::Ptr standingOn;
auto* ptrHolder = static_cast<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);
MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads);
std::visit(vis, sim);
}
}
@ -532,10 +618,11 @@ namespace MWPhysics
{
mPreStepBarrier->wait([this] { afterPreStep(); });
int job = 0;
const Visitors::Move vis{mPhysicsDt, mCollisionWorld, *mWorldFrameData};
while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs)
{
MaybeLock lockColWorld(mCollisionWorldMutex, mNumThreads);
MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld, *mWorldFrameData);
std::visit(vis, mSimulations[job]);
}
mPostStepBarrier->wait([this] { afterPostStep(); });
@ -577,7 +664,7 @@ namespace MWPhysics
void PhysicsTaskScheduler::releaseSharedStates()
{
std::scoped_lock lock(mSimulationMutex, mUpdateAabbMutex);
mActors.clear();
mSimulations.clear();
mUpdateAabb.clear();
}
@ -586,10 +673,11 @@ namespace MWPhysics
updateAabbs();
if (!mRemainingSteps)
return;
for (size_t i = 0; i < mActors.size(); ++i)
const Visitors::PreStep vis{mCollisionWorld};
for (auto& sim : mSimulations)
{
MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads);
MovementSolver::unstuck(mActorsFrameData[i], mCollisionWorld);
std::visit(vis, sim);
}
}
@ -618,7 +706,8 @@ namespace MWPhysics
void PhysicsTaskScheduler::syncWithMainThread()
{
for (size_t i = 0; i < mActors.size(); ++i)
updateActor(*mActors[i], mActorsFrameData[i], mAdvanceSimulation, mTimeAccum, mPhysicsDt);
const Visitors::Sync vis{mAdvanceSimulation, mTimeAccum, mPhysicsDt, this};
for (auto& sim : mSimulations)
std::visit(vis, sim);
}
}

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

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

@ -60,19 +60,6 @@
namespace
{
bool canMoveToWaterSurface(const MWPhysics::Actor* physicActor, const float waterlevel, btCollisionWorld* world)
{
if (!physicActor)
return false;
const float halfZ = physicActor->getHalfExtents().z();
const osg::Vec3f actorPosition = physicActor->getPosition();
const osg::Vec3f startingPosition(actorPosition.x(), actorPosition.y(), actorPosition.z() + halfZ);
const osg::Vec3f destinationPosition(actorPosition.x(), actorPosition.y(), waterlevel + halfZ);
MWPhysics::ActorTracer tracer;
tracer.doTrace(physicActor->getCollisionObject(), startingPosition, destinationPosition, world);
return (tracer.mFraction >= 1.0f);
}
void handleJump(const MWWorld::Ptr &ptr)
{
if (!ptr.getClass().isActor())
@ -370,6 +357,8 @@ namespace MWPhysics
bool PhysicsSystem::getLineOfSight(const MWWorld::ConstPtr &actor1, const MWWorld::ConstPtr &actor2) const
{
if (actor1 == actor2) return true;
const auto it1 = mActors.find(actor1.mRef);
const auto it2 = mActors.find(actor2.mRef);
if (it1 == mActors.end() || it2 == mActors.end())
@ -386,7 +375,8 @@ namespace MWPhysics
bool PhysicsSystem::canMoveToWaterSurface(const MWWorld::ConstPtr &actor, const float waterlevel)
{
return ::canMoveToWaterSurface(getActor(actor), waterlevel, mCollisionWorld.get());
const auto* physactor = getActor(actor);
return physactor && physactor->canMoveToWaterSurface(waterlevel, mCollisionWorld.get());
}
osg::Vec3f PhysicsSystem::getHalfExtents(const MWWorld::ConstPtr &actor) const
@ -491,7 +481,7 @@ namespace MWPhysics
if (ptr.mRef->mData.mPhysicsPostponed)
return;
osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance = mShapeManager->getInstance(mesh);
if (!shapeInstance || !shapeInstance->getCollisionShape())
if (!shapeInstance || !shapeInstance->mCollisionShape)
return;
assert(!getObject(ptr));
@ -526,10 +516,10 @@ namespace MWPhysics
void PhysicsSystem::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated)
{
if (auto found = mObjects.find(old.mRef); found != mObjects.end())
found->second->updatePtr(updated);
else if (auto found = mActors.find(old.mRef); found != mActors.end())
found->second->updatePtr(updated);
if (auto foundObject = mObjects.find(old.mRef); foundObject != mObjects.end())
foundObject->second->updatePtr(updated);
else if (auto foundActor = mActors.find(old.mRef); foundActor != mActors.end())
foundActor->second->updatePtr(updated);
for (auto& [_, actor] : mActors)
{
@ -592,33 +582,6 @@ namespace MWPhysics
}
}
void PhysicsSystem::updateProjectile(const int projectileId, const osg::Vec3f &position) const
{
const auto foundProjectile = mProjectiles.find(projectileId);
assert(foundProjectile != mProjectiles.end());
auto* projectile = foundProjectile->second.get();
btVector3 btFrom = Misc::Convert::toBullet(projectile->getPosition());
btVector3 btTo = Misc::Convert::toBullet(position);
if (btFrom == btTo)
return;
ProjectileConvexCallback resultCallback(projectile->getCasterCollisionObject(), projectile->getCollisionObject(), btFrom, btTo, projectile);
resultCallback.m_collisionFilterMask = 0xff;
resultCallback.m_collisionFilterGroup = CollisionType_Projectile;
const btQuaternion btrot = btQuaternion::getIdentity();
btTransform from_ (btrot, btFrom);
btTransform to_ (btrot, btTo);
mTaskScheduler->convexSweepTest(projectile->getConvexShape(), from_, to_, resultCallback);
const auto newpos = projectile->isActive() ? position : Misc::Convert::toOsg(projectile->getHitPosition());
projectile->setPosition(newpos);
mTaskScheduler->updateSingleAabb(foundProjectile->second);
}
void PhysicsSystem::updateRotation(const MWWorld::Ptr &ptr, osg::Quat rotate)
{
if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end())
@ -655,7 +618,7 @@ namespace MWPhysics
osg::ref_ptr<const Resource::BulletShape> shape = mShapeManager->getShape(mesh);
// Try to get shape from basic model as fallback for creatures
if (!ptr.getClass().isNpc() && shape && shape->mCollisionBox.extents.length2() == 0)
if (!ptr.getClass().isNpc() && shape && shape->mCollisionBox.mExtents.length2() == 0)
{
const std::string fallbackModel = ptr.getClass().getModel(ptr);
if (fallbackModel != mesh)
@ -680,7 +643,7 @@ namespace MWPhysics
{
osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance = mShapeManager->getInstance(mesh);
assert(shapeInstance);
float radius = computeRadius ? shapeInstance->mCollisionBox.extents.length() / 2.f : 1.f;
float radius = computeRadius ? shapeInstance->mCollisionBox.mExtents.length() / 2.f : 1.f;
mProjectileId++;
@ -727,11 +690,10 @@ namespace MWPhysics
actor->setVelocity(osg::Vec3f());
}
std::pair<std::vector<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;
framedata.first.reserve(mActors.size());
framedata.second.reserve(mActors.size());
std::vector<Simulation> simulations;
simulations.reserve(mActors.size() + mProjectiles.size());
const MWBase::World *world = MWBase::Environment::get().getWorld();
for (const auto& [ref, physicActor] : mActors)
{
@ -756,18 +718,23 @@ namespace MWPhysics
physicActor->setCanWaterWalk(waterCollision);
// Slow fall reduces fall speed by a factor of (effect magnitude / 200)
const float slowFall = 1.f - std::max(0.f, std::min(1.f, effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f));
const float slowFall = 1.f - std::clamp(effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f, 0.f, 1.f);
const bool godmode = ptr == world->getPlayerConstPtr() && world->getGodModeState();
const bool inert = stats.isDead() || (!godmode && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0);
framedata.first.emplace_back(physicActor);
framedata.second.emplace_back(*physicActor, inert, waterCollision, slowFall, waterlevel);
simulations.emplace_back(ActorSimulation{physicActor, ActorFrameData{*physicActor, inert, waterCollision, slowFall, waterlevel}});
// if the simulation will run, a jump request will be fulfilled. Update mechanics accordingly.
if (willSimulate)
handleJump(ptr);
}
return framedata;
for (const auto& [id, projectile] : mProjectiles)
{
simulations.emplace_back(ProjectileSimulation{projectile, ProjectileFrameData{*projectile}});
}
return simulations;
}
void PhysicsSystem::stepSimulation(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats)
@ -793,9 +760,9 @@ namespace MWPhysics
mTaskScheduler->resetSimulation(mActors);
else
{
auto [actors, framedata] = prepareFrameData(mTimeAccum >= mPhysicsDt);
auto simulations = prepareSimulation(mTimeAccum >= mPhysicsDt);
// modifies mTimeAccum
mTaskScheduler->applyQueuedMovements(mTimeAccum, std::move(actors), std::move(framedata), frameStart, frameNumber, stats);
mTaskScheduler->applyQueuedMovements(mTimeAccum, std::move(simulations), frameStart, frameNumber, stats);
}
}
@ -974,25 +941,17 @@ namespace MWPhysics
, mWasOnGround(actor.getOnGround())
, mIsAquatic(actor.getPtr().getClass().isPureWaterCreature(actor.getPtr()))
, mWaterCollision(waterCollision)
, mSkipCollisionDetection(actor.skipCollisions() || !actor.getCollisionMode())
, mSkipCollisionDetection(!actor.getCollisionMode())
{
}
void ActorFrameData::updatePosition(Actor& actor, btCollisionWorld* world)
ProjectileFrameData::ProjectileFrameData(Projectile& projectile)
: mPosition(projectile.getPosition())
, mMovement(projectile.velocity())
, mCaster(projectile.getCasterCollisionObject())
, mCollisionObject(projectile.getCollisionObject())
, mProjectile(&projectile)
{
actor.applyOffsetChange();
mPosition = actor.getPosition();
if (mWaterCollision && mPosition.z() < mWaterlevel && canMoveToWaterSurface(&actor, mWaterlevel, world))
{
mPosition.z() = mWaterlevel;
MWBase::Environment::get().getWorld()->moveObject(actor.getPtr(), mPosition, false);
}
mOldHeight = mPosition.z();
const auto rotation = actor.getPtr().getRefData().getPosition().asRotationVec3();
mRotation = osg::Vec2f(rotation.x(), rotation.z());
mInertia = actor.getInertialForce();
mStuckFrames = actor.getStuckFrames();
mLastStuckPosition = actor.getLastStuckPosition();
}
WorldFrameData::WorldFrameData()

@ -7,6 +7,7 @@
#include <set>
#include <unordered_map>
#include <algorithm>
#include <variant>
#include <osg/Quat>
#include <osg/BoundingBox>
@ -75,7 +76,6 @@ namespace MWPhysics
struct ActorFrameData
{
ActorFrameData(Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel);
void updatePosition(Actor& actor, btCollisionWorld* world);
osg::Vec3f mPosition;
osg::Vec3f mInertia;
const btCollisionObject* mStandingOn;
@ -100,6 +100,16 @@ namespace MWPhysics
const bool mSkipCollisionDetection;
};
struct ProjectileFrameData
{
explicit ProjectileFrameData(Projectile& projectile);
osg::Vec3f mPosition;
osg::Vec3f mMovement;
const btCollisionObject* mCaster;
const btCollisionObject* mCollisionObject;
Projectile* mProjectile;
};
struct WorldFrameData
{
WorldFrameData();
@ -107,6 +117,10 @@ namespace MWPhysics
osg::Vec3f mStormDirection;
};
using ActorSimulation = std::pair<std::shared_ptr<Actor>, ActorFrameData>;
using ProjectileSimulation = std::pair<std::shared_ptr<Projectile>, ProjectileFrameData>;
using Simulation = std::variant<ActorSimulation, ProjectileSimulation>;
class PhysicsSystem : public RayCastingInterface
{
public:
@ -124,7 +138,6 @@ namespace MWPhysics
int addProjectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius);
void setCaster(int projectileId, const MWWorld::Ptr& caster);
void updateProjectile(const int projectileId, const osg::Vec3f &position) const;
void removeProjectile(const int projectileId);
void updatePtr (const MWWorld::Ptr& old, const MWWorld::Ptr& updated);
@ -253,7 +266,7 @@ namespace MWPhysics
void updateWater();
std::pair<std::vector<std::shared_ptr<Actor>>, std::vector<ActorFrameData>> prepareFrameData(bool willSimulate);
std::vector<Simulation> prepareSimulation(bool willSimulate);
std::unique_ptr<btBroadphaseInterface> mBroadphase;
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->setUserPointer(this);
setPosition(position);
mPosition = position;
mPreviousPosition = position;
setCaster(caster);
const int collisionMask = CollisionType_World | CollisionType_HeightMap |
CollisionType_Actor | CollisionType_Door | CollisionType_Water | CollisionType_Projectile;
mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Projectile, collisionMask);
commitPositionChange();
updateCollisionObjectPosition();
}
Projectile::~Projectile()
@ -48,29 +49,12 @@ Projectile::~Projectile()
mTaskScheduler->removeCollisionObject(mCollisionObject.get());
}
void Projectile::commitPositionChange()
{
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
void Projectile::updateCollisionObjectPosition()
{
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)

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

@ -30,9 +30,49 @@ namespace MWPhysics
return mCollisionObject.get();
}
void setVelocity(osg::Vec3f velocity)
{
mVelocity = velocity;
}
osg::Vec3f velocity()
{
return std::exchange(mVelocity, osg::Vec3f());
}
void setSimulationPosition(const osg::Vec3f& position)
{
mSimulationPosition = position;
}
osg::Vec3f getSimulationPosition() const
{
return mSimulationPosition;
}
void setPosition(const osg::Vec3f& position)
{
mPreviousPosition = mPosition;
mPosition = position;
}
osg::Vec3f getPosition() const
{
return mPosition;
}
osg::Vec3f getPreviousPosition() const
{
return mPreviousPosition;
}
protected:
MWWorld::Ptr mPtr;
std::unique_ptr<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.
// This algorithm has a couple of minor problems, but they don't cause problems for sane geometry, and just prevent stepping on insane geometry.
mUpStepper.doTrace(mColObj, position, position + osg::Vec3f(0.0f, 0.0f, Constants::sStepSizeUp), mColWorld);
mUpStepper.doTrace(mColObj, position, position + osg::Vec3f(0.0f, 0.0f, Constants::sStepSizeUp), mColWorld, onGround);
float upDistance = 0;
if(!mUpStepper.mHitObject)
@ -117,7 +117,7 @@ namespace MWPhysics
downStepSize = upDistance;
else
downStepSize = moveDistance + upDistance + sStepSizeDown;
mDownStepper.doTrace(mColObj, tracerDest, tracerDest + osg::Vec3f(0.0f, 0.0f, -downStepSize), mColWorld);
mDownStepper.doTrace(mColObj, tracerDest, tracerDest + osg::Vec3f(0.0f, 0.0f, -downStepSize), mColWorld, onGround);
// can't step down onto air, non-walkable-slopes, or actors
// NOTE: using a capsule causes isWalkableSlope (used in canStepDown) to fail on certain geometry that were intended to be valid at the bottoms of stairs

@ -12,38 +12,84 @@
namespace MWPhysics
{
void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world)
ActorConvexCallback sweepHelper(const btCollisionObject *actor, const btVector3& from, const btVector3& to, const btCollisionWorld* world, bool actorFilter)
{
const btVector3 btstart = Misc::Convert::toBullet(start);
const btVector3 btend = Misc::Convert::toBullet(end);
const btTransform &trans = actor->getWorldTransform();
btTransform from(trans);
btTransform to(trans);
from.setOrigin(btstart);
to.setOrigin(btend);
const btVector3 motion = btstart-btend;
ActorConvexCallback newTraceCallback(actor, motion, btScalar(0.0), world);
// Inherit the actor's collision group and mask
newTraceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup;
newTraceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask;
btTransform transFrom(trans);
btTransform transTo(trans);
transFrom.setOrigin(from);
transTo.setOrigin(to);
const btCollisionShape *shape = actor->getCollisionShape();
assert(shape->isConvex());
world->convexSweepTest(static_cast<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:
if(newTraceCallback.hasHit())
if(traceCallback.hasHit())
{
mFraction = newTraceCallback.m_closestHitFraction;
mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld);
mFraction = traceCallback.m_closestHitFraction;
// ensure fraction is correct (covers intended distance traveled instead of actual distance traveled)
if(doing_short_trace && (end-start).length2() > 0.0)
mFraction *= (btend-btstart).length() / (end-start).length();
mPlaneNormal = Misc::Convert::toOsg(traceCallback.m_hitNormalWorld);
mEndPos = (end-start)*mFraction + start;
mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld);
mHitObject = newTraceCallback.m_hitCollisionObject;
mHitPoint = Misc::Convert::toOsg(traceCallback.m_hitPointWorld);
mHitObject = traceCallback.m_hitCollisionObject;
}
else
{
if(doing_short_trace)
{
btend = Misc::Convert::toBullet(end);
const auto newTraceCallback = sweepHelper(actor, btstart, btend, world, false);
if(newTraceCallback.hasHit())
{
mFraction = newTraceCallback.m_closestHitFraction;
mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld);
mEndPos = (end-start)*mFraction + start;
mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld);
mHitObject = newTraceCallback.m_hitCollisionObject;
return;
}
}
// fallthrough
mEndPos = end;
mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f);
mFraction = 1.0f;
@ -54,25 +100,11 @@ void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& star
void ActorTracer::findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world)
{
const btVector3 btstart = Misc::Convert::toBullet(start);
const btVector3 btend = Misc::Convert::toBullet(end);
const btTransform &trans = actor->getCollisionObject()->getWorldTransform();
btTransform from(trans.getBasis(), btstart);
btTransform to(trans.getBasis(), btend);
const btVector3 motion = btstart-btend;
ActorConvexCallback newTraceCallback(actor->getCollisionObject(), motion, btScalar(0.0), world);
// Inherit the actor's collision group and mask
newTraceCallback.m_collisionFilterGroup = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterGroup;
newTraceCallback.m_collisionFilterMask = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterMask;
newTraceCallback.m_collisionFilterMask &= ~CollisionType_Actor;
world->convexSweepTest(actor->getConvexShape(), from, to, newTraceCallback);
if(newTraceCallback.hasHit())
const auto traceCallback = sweepHelper(actor->getCollisionObject(), Misc::Convert::toBullet(start), Misc::Convert::toBullet(end), world, true);
if(traceCallback.hasHit())
{
mFraction = newTraceCallback.m_closestHitFraction;
mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld);
mFraction = traceCallback.m_closestHitFraction;
mPlaneNormal = Misc::Convert::toOsg(traceCallback.m_hitNormalWorld);
mEndPos = (end-start)*mFraction + start;
}
else

@ -20,7 +20,7 @@ namespace MWPhysics
float mFraction;
void doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world);
void doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world, bool attempt_short_trace = false);
void findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world);
};
}

@ -11,6 +11,7 @@
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/sceneutil/attach.hpp>
#include <components/sceneutil/lightmanager.hpp>
#include <components/sceneutil/lightutil.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);
const NodeMap& nodeMap = getNodeMap();
NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename));
NodeMap::const_iterator found = nodeMap.find(bonename);
if (found == nodeMap.end())
return PartHolderPtr();
@ -84,30 +85,58 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st
return PartHolderPtr(new PartHolder(instance));
}
std::string ActorAnimation::getShieldMesh(const MWWorld::ConstPtr& shield) const
osg::ref_ptr<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 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())
{
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
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)
{
// Assume all creatures use the male mesh.
if (part.mPart != ESM::PRT_Shield || part.mMale.empty())
if (part.mPart != ESM::PRT_Shield)
continue;
const ESM::BodyPart *bodypart = partStore.search(part.mMale);
if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty())
std::string bodypartName;
if (female && !part.mFemale.empty())
bodypartName = part.mFemale;
else if (!part.mMale.empty())
bodypartName = part.mMale;
if (!bodypartName.empty())
{
mesh = "meshes\\" + bodypart->mModel;
break;
const ESM::BodyPart *bodypart = partStore.search(bodypartName);
if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor)
return std::string();
if (!bodypart->mModel.empty())
return "meshes\\" + bodypart->mModel;
}
}
}
return shield.getClass().getModel(shield);
}
std::string ActorAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const
{
std::string mesh = getShieldMesh(shield, false);
if (mesh.empty())
return mesh;
@ -143,7 +172,7 @@ bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const
const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr);
const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft);
if (shield != inv.end() && shield->getType() == ESM::Armor::sRecordId && !getShieldMesh(*shield).empty())
if (shield != inv.end() && shield->getType() == ESM::Armor::sRecordId && !getSheathedShieldMesh(*shield).empty())
{
if(stats.getDrawState() != MWMechanics::DrawState_Weapon)
return false;
@ -201,7 +230,7 @@ void ActorAnimation::updateHolsteredShield(bool showCarriedLeft)
return;
}
std::string mesh = getShieldMesh(*shield);
std::string mesh = getSheathedShieldMesh(*shield);
if (mesh.empty())
return;
@ -255,7 +284,7 @@ bool ActorAnimation::useShieldAnimations() const
const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft);
if (weapon != inv.end() && shield != inv.end() &&
shield->getType() == ESM::Armor::sRecordId &&
!getShieldMesh(*shield).empty())
!getSheathedShieldMesh(*shield).empty())
{
auto type = weapon->getType();
if(type == ESM::Weapon::sRecordId)

@ -45,7 +45,8 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener
virtual void updateHolsteredWeapon(bool showHolsteredWeapons);
virtual void updateHolsteredShield(bool showCarriedLeft);
virtual void updateQuiver();
virtual std::string getShieldMesh(const MWWorld::ConstPtr& shield) const;
std::string getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const;
virtual std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const;
virtual std::string getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon);
virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename, bool enchantedGlow, osg::Vec4f* glowColor);
virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename)
@ -53,6 +54,7 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener
osg::Vec4f stubColor = osg::Vec4f(0,0,0,0);
return attachMesh(model, bonename, false, &stubColor);
};
osg::ref_ptr<osg::Node> attach(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool isLight);
PartHolderPtr mScabbard;
PartHolderPtr mHolsteredShield;

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

@ -7,8 +7,10 @@
#include <components/sceneutil/textkeymap.hpp>
#include <components/sceneutil/util.hpp>
#include <components/sceneutil/nodecallback.hpp>
#include <components/misc/stringops.hpp>
#include <vector>
#include <unordered_map>
namespace ESM
{
@ -157,6 +159,8 @@ public:
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:
class AnimationTime : public SceneUtil::ControllerSource
{
@ -250,8 +254,6 @@ protected:
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 bool mNodeMapCreated;

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

@ -190,7 +190,9 @@ namespace MWRender
mTexture->setInternalFormat(GL_RGBA);
mTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR);
mTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR);
mTexture->setUserValue("premultiplied alpha", true);
mTextureStateSet = new osg::StateSet;
mTextureStateSet->setAttribute(new osg::BlendFunc(osg::BlendFunc::ONE, osg::BlendFunc::ONE_MINUS_SRC_ALPHA));
mCamera = new osg::Camera;
// hints that the camera is not relative to the master camera

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

@ -126,7 +126,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot)
if (bonename != "Weapon Bone")
{
const NodeMap& nodeMap = getNodeMap();
NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename));
NodeMap::const_iterator found = nodeMap.find(bonename);
if (found == nodeMap.end())
bonename = "Weapon Bone";
}
@ -139,32 +139,13 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot)
bonename = "Shield Bone";
if (item.getType() == ESM::Armor::sRecordId)
{
// Shield body part model should be used if possible.
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
for (const auto& part : item.get<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;
}
}
itemModel = getShieldMesh(item, false);
}
}
try
{
osg::ref_ptr<const osg::Node> node = mResourceSystem->getSceneManager()->getTemplate(itemModel);
const NodeMap& nodeMap = getNodeMap();
NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename));
if (found == nodeMap.end())
throw std::runtime_error("Can't find attachment node " + bonename);
osg::ref_ptr<osg::Node> attached = SceneUtil::attach(node, mObjectRoot, bonename, found->second.get());
osg::ref_ptr<osg::Node> attached = attach(itemModel, bonename, bonename, item.getType() == ESM::Light::sRecordId);
scene.reset(new PartHolder(attached));

@ -562,8 +562,8 @@ bool LocalMap::isPositionExplored (float nX, float nY, int x, int y)
if (!segment.mFogOfWarImage)
return false;
nX = std::max(0.f, std::min(1.f, nX));
nY = std::max(0.f, std::min(1.f, nY));
nX = std::clamp(nX, 0.f, 1.f);
nY = std::clamp(nY, 0.f, 1.f);
int texU = static_cast<int>((sFogOfWarResolution - 1) * nX);
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;
uint8_t alpha = (clr >> 24);
alpha = std::min( alpha, (uint8_t) (std::max(0.f, std::min(1.f, (sqrDist/sqrExploreRadius)))*255) );
alpha = std::min( alpha, (uint8_t) (std::clamp((sqrDist/sqrExploreRadius)*255, 0.f, 1.f)));
uint32_t val = (uint32_t) (alpha << 24);
if ( *data != val)
{

@ -82,34 +82,6 @@ std::string getVampireHead(const std::string& race, bool female)
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();
}
std::string NpcAnimation::getShieldMesh(const MWWorld::ConstPtr& shield) const
std::string NpcAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const
{
std::string mesh = shield.getClass().getModel(shield);
const ESM::Armor *armor = shield.get<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());
std::string mesh = getShieldMesh(shield, !mNpc->isMale());
if (mesh.empty())
return std::string();
@ -678,7 +645,7 @@ void NpcAnimation::updateParts()
{
const ESM::Light *light = part.get<ESM::Light>()->mBase;
addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft,
1, "meshes\\"+light->mModel);
1, "meshes\\"+light->mModel, false, nullptr, true);
if (mObjectParts[ESM::PRT_Shield])
addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), light);
}
@ -708,16 +675,9 @@ void NpcAnimation::updateParts()
PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool enchantedGlow, osg::Vec4f* glowColor)
PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight)
{
osg::ref_ptr<const osg::Node> templateNode = mResourceSystem->getSceneManager()->getTemplate(model);
const NodeMap& nodeMap = getNodeMap();
NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename));
if (found == nodeMap.end())
throw std::runtime_error("Can't find attachment node " + bonename);
osg::ref_ptr<osg::Node> attached = SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second);
osg::ref_ptr<osg::Node> attached = attach(model, bonename, bonefilter, isLight);
if (enchantedGlow)
mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor);
@ -790,7 +750,7 @@ bool NpcAnimation::isFemalePart(const ESM::BodyPart* bodypart)
return bodypart->mData.mFlags & ESM::BodyPart::BPF_Female;
}
bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, bool enchantedGlow, osg::Vec4f* glowColor)
bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight)
{
if(priority <= mPartPriorities[type])
return false;
@ -813,7 +773,7 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g
if (weaponBonename != bonename)
{
const NodeMap& nodeMap = getNodeMap();
NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(weaponBonename));
NodeMap::const_iterator found = nodeMap.find(weaponBonename);
if (found != nodeMap.end())
bonename = weaponBonename;
}
@ -822,7 +782,7 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g
// PRT_Hair seems to be the only type that breaks consistency and uses a filter that's different from the attachment bone
const std::string bonefilter = (type == ESM::PRT_Hair) ? "hair" : bonename;
mObjectParts[type] = insertBoundedPart(mesh, bonename, bonefilter, enchantedGlow, glowColor);
mObjectParts[type] = insertBoundedPart(mesh, bonename, bonefilter, enchantedGlow, glowColor, isLight);
}
catch (std::exception& e)
{
@ -1011,13 +971,10 @@ void NpcAnimation::showCarriedLeft(bool show)
// For shields we must try to use the body part model
if (iter->getType() == ESM::Armor::sRecordId)
{
const ESM::Armor *armor = iter->get<ESM::Armor>()->mBase;
const std::vector<ESM::PartReference>& bodyparts = armor->mParts.mParts;
if (!bodyparts.empty())
mesh = getShieldBodypartMesh(bodyparts, !mNpc->isMale());
mesh = getShieldMesh(*iter, !mNpc->isMale());
}
if (mesh.empty() || addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1,
mesh, !iter->getClass().getEnchantment(*iter).empty(), &glowColor))
mesh, !iter->getClass().getEnchantment(*iter).empty(), &glowColor, iter->getType() == ESM::Light::sRecordId))
{
if (mesh.empty())
reserveIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1);

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

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

Loading…
Cancel
Save