Merge branch openmw:master into handtohand-tooltip

pull/3236/head
trav 3 months ago
commit 3d2dd9201d

@ -83,9 +83,7 @@ jobs:
max-size: 1000M max-size: 1000M
- name: Configure - name: Configure
run: | run: CI/before_script.osx.sh
rm -fr build # remove the build directory
CI/before_script.osx.sh
- name: Build - name: Build
run: | run: |
cd build cd build

@ -501,7 +501,6 @@ Ubuntu_GCC_integration_tests_asan:
paths: paths:
- ccache/ - ccache/
script: script:
- rm -fr build # remove the build directory
- CI/before_install.osx.sh - CI/before_install.osx.sh
- export CCACHE_BASEDIR="$(pwd)" - export CCACHE_BASEDIR="$(pwd)"
- export CCACHE_DIR="$(pwd)/ccache" - export CCACHE_DIR="$(pwd)/ccache"
@ -521,7 +520,6 @@ Ubuntu_GCC_integration_tests_asan:
artifacts: artifacts:
paths: paths:
- build/OpenMW-*.dmg - build/OpenMW-*.dmg
- "build/**/*.log"
macOS14_Xcode15_arm64: macOS14_Xcode15_arm64:
extends: .MacOS extends: .MacOS

@ -191,6 +191,7 @@
Bug #8097: GetEffect doesn't detect 0 magnitude spells Bug #8097: GetEffect doesn't detect 0 magnitude spells
Bug #8124: Normal weapon resistance is applied twice for NPCs Bug #8124: Normal weapon resistance is applied twice for NPCs
Bug #8132: Actors without hello responses turn to face the player Bug #8132: Actors without hello responses turn to face the player
Bug #8171: Items with more than 100% health can be repaired
Feature #1415: Infinite fall failsafe Feature #1415: Infinite fall failsafe
Feature #2566: Handle NAM9 records for manual cell references Feature #2566: Handle NAM9 records for manual cell references
Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking

@ -4,14 +4,9 @@ export HOMEBREW_NO_EMOJI=1
export HOMEBREW_NO_INSTALL_CLEANUP=1 export HOMEBREW_NO_INSTALL_CLEANUP=1
export HOMEBREW_AUTOREMOVE=1 export HOMEBREW_AUTOREMOVE=1
# workaround for gitlab's pre-installed brew
# purge large and unnecessary packages that get in our way and have caused issues
brew uninstall ruby php openjdk node postgresql maven curl || true
brew tap --repair brew tap --repair
brew update --quiet brew update --quiet
# Some of these tools can come from places other than brew, so check before installing
brew install curl xquartz gd fontconfig freetype harfbuzz brotli brew install curl xquartz gd fontconfig freetype harfbuzz brotli
command -v ccache >/dev/null 2>&1 || brew install ccache command -v ccache >/dev/null 2>&1 || brew install ccache
@ -27,8 +22,9 @@ cmake --version
qmake --version qmake --version
if [[ "${MACOS_AMD64}" ]]; then if [[ "${MACOS_AMD64}" ]]; then
curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20221113.zip -o ~/openmw-deps.zip curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240802.zip -o ~/openmw-deps.zip
unzip -o ~/openmw-deps.zip -d /tmp > /dev/null
else else
curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240802_arm64.zip -o ~/openmw-deps.zip curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240818-arm64.tar.xz -o ~/openmw-deps.tar.xz
tar xf ~/openmw-deps.tar.xz -C /tmp > /dev/null
fi fi
unzip -o ~/openmw-deps.zip -d /tmp > /dev/null

@ -3,6 +3,7 @@
# Silence a git warning # Silence a git warning
git config --global advice.detachedHead false git config --global advice.detachedHead false
rm -fr build
mkdir build mkdir build
cd build cd build

@ -33,7 +33,8 @@ namespace
{ {
const ObjectId id(&shape); const ObjectId id(&shape);
osg::ref_ptr<Resource::BulletShape> bulletShape(new Resource::BulletShape); osg::ref_ptr<Resource::BulletShape> bulletShape(new Resource::BulletShape);
bulletShape->mFileName = "test.nif"; constexpr VFS::Path::NormalizedView test("test.nif");
bulletShape->mFileName = test;
bulletShape->mFileHash = "test_hash"; bulletShape->mFileHash = "test_hash";
ObjectTransform objectTransform; ObjectTransform objectTransform;
std::fill(std::begin(objectTransform.mPosition.pos), std::end(objectTransform.mPosition.pos), 0.1f); std::fill(std::begin(objectTransform.mPosition.pos), std::end(objectTransform.mPosition.pos), 0.1f);

@ -131,7 +131,8 @@ namespace NavMeshTool
osg::ref_ptr<const Resource::BulletShape> shape = [&] { osg::ref_ptr<const Resource::BulletShape> shape = [&] {
try try
{ {
return bulletShapeManager.getShape(Misc::ResourceHelpers::correctMeshPath(model)); return bulletShapeManager.getShape(
VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(model)));
} }
catch (const std::exception& e) catch (const std::exception& e)
{ {

@ -408,12 +408,12 @@ void CSMPrefs::State::declare()
declareShortcut(mValues->mKeyBindings.mScriptEditorUncomment, "Uncomment Selection"); declareShortcut(mValues->mKeyBindings.mScriptEditorUncomment, "Uncomment Selection");
declareCategory("Models"); declareCategory("Models");
declareString(mValues->mModels.mBaseanim, "base animations").setTooltip("3rd person base model with textkeys-data"); declareString(mValues->mModels.mBaseanim, "Base Animations").setTooltip("Third person base model and animations");
declareString(mValues->mModels.mBaseanimkna, "base animations, kna") declareString(mValues->mModels.mBaseanimkna, "Base Animations, Beast")
.setTooltip("3rd person beast race base model with textkeys-data"); .setTooltip("Third person beast race base model and animations");
declareString(mValues->mModels.mBaseanimfemale, "base animations, female") declareString(mValues->mModels.mBaseanimfemale, "Base Animations, Female")
.setTooltip("3rd person female base model with textkeys-data"); .setTooltip("Third person female base model and animations");
declareString(mValues->mModels.mWolfskin, "base animations, wolf").setTooltip("3rd person werewolf skin"); declareString(mValues->mModels.mWolfskin, "Base Animations, Werewolf").setTooltip("Third person werewolf skin");
} }
void CSMPrefs::State::declareCategory(const std::string& key) void CSMPrefs::State::declareCategory(const std::string& key)

@ -1,6 +1,7 @@
#include "stringsetting.hpp" #include "stringsetting.hpp"
#include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QMutexLocker> #include <QMutexLocker>
@ -26,17 +27,21 @@ CSMPrefs::StringSetting& CSMPrefs::StringSetting::setTooltip(const std::string&
CSMPrefs::SettingWidgets CSMPrefs::StringSetting::makeWidgets(QWidget* parent) CSMPrefs::SettingWidgets CSMPrefs::StringSetting::makeWidgets(QWidget* parent)
{ {
QLabel* label = new QLabel(getLabel(), parent);
mWidget = new QLineEdit(QString::fromStdString(getValue()), parent); mWidget = new QLineEdit(QString::fromStdString(getValue()), parent);
mWidget->setMinimumWidth(300);
if (!mTooltip.empty()) if (!mTooltip.empty())
{ {
QString tooltip = QString::fromUtf8(mTooltip.c_str()); QString tooltip = QString::fromUtf8(mTooltip.c_str());
label->setToolTip(tooltip);
mWidget->setToolTip(tooltip); mWidget->setToolTip(tooltip);
} }
connect(mWidget, &QLineEdit::textChanged, this, &StringSetting::textChanged); connect(mWidget, &QLineEdit::textChanged, this, &StringSetting::textChanged);
return SettingWidgets{ .mLabel = nullptr, .mInput = mWidget }; return SettingWidgets{ .mLabel = label, .mInput = mWidget };
} }
void CSMPrefs::StringSetting::updateWidget() void CSMPrefs::StringSetting::updateWidget()

@ -4,13 +4,13 @@
#include "rotationflags.hpp" #include "rotationflags.hpp"
#include <deque> #include <deque>
#include <map>
#include <set> #include <set>
#include <span> #include <span>
#include <string_view> #include <string_view>
#include <vector> #include <vector>
#include <components/misc/rng.hpp> #include <components/misc/rng.hpp>
#include <components/vfs/pathutil.hpp>
#include "../mwworld/doorstate.hpp" #include "../mwworld/doorstate.hpp"
#include "../mwworld/globalvariablename.hpp" #include "../mwworld/globalvariablename.hpp"
@ -515,7 +515,7 @@ namespace MWBase
/// Spawn a blood effect for \a ptr at \a worldPosition /// Spawn a blood effect for \a ptr at \a worldPosition
virtual void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0; virtual void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0;
virtual void spawnEffect(const std::string& model, const std::string& textureOverride, virtual void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride,
const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true) const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true)
= 0; = 0;

@ -794,18 +794,32 @@ namespace MWGui
if (!Settings::gui().mColorTopicEnable) if (!Settings::gui().mColorTopicEnable)
return; return;
const MyGUI::Colour& specialColour = Settings::gui().mColorTopicSpecific;
const MyGUI::Colour& oldColour = Settings::gui().mColorTopicExhausted;
for (const std::string& keyword : mKeywords) for (const std::string& keyword : mKeywords)
{ {
int flag = MWBase::Environment::get().getDialogueManager()->getTopicFlag(ESM::RefId::stringRefId(keyword)); int flag = MWBase::Environment::get().getDialogueManager()->getTopicFlag(ESM::RefId::stringRefId(keyword));
MyGUI::Button* button = mTopicsList->getItemWidget(keyword); MyGUI::Button* button = mTopicsList->getItemWidget(keyword);
const auto oldCaption = button->getCaption();
const MyGUI::IntSize oldSize = button->getSize();
bool changed = false;
if (flag & MWBase::DialogueManager::TopicType::Specific) if (flag & MWBase::DialogueManager::TopicType::Specific)
button->getSubWidgetText()->setTextColour(specialColour); {
button->changeWidgetSkin("MW_ListLine_Specific");
changed = true;
}
else if (flag & MWBase::DialogueManager::TopicType::Exhausted) else if (flag & MWBase::DialogueManager::TopicType::Exhausted)
button->getSubWidgetText()->setTextColour(oldColour); {
button->changeWidgetSkin("MW_ListLine_Exhausted");
changed = true;
}
if (changed)
{
button->setCaption(oldCaption);
button->getSubWidgetText()->setWordWrap(true);
button->getSubWidgetText()->setTextAlign(MyGUI::Align::Left);
button->setSize(oldSize);
}
} }
} }

@ -55,7 +55,7 @@ namespace MWGui
{ {
int maxDurability = iter->getClass().getItemMaxHealth(*iter); int maxDurability = iter->getClass().getItemMaxHealth(*iter);
int durability = iter->getClass().getItemHealth(*iter); int durability = iter->getClass().getItemHealth(*iter);
if (maxDurability == durability || maxDurability == 0) if (maxDurability <= durability || maxDurability == 0)
continue; continue;
int basePrice = iter->getClass().getValue(*iter); int basePrice = iter->getClass().getValue(*iter);

@ -288,7 +288,7 @@ namespace MWGui
if ((mFilter & Filter_OnlyRepairable) if ((mFilter & Filter_OnlyRepairable)
&& (!base.getClass().hasItemHealth(base) && (!base.getClass().hasItemHealth(base)
|| (base.getClass().getItemHealth(base) == base.getClass().getItemMaxHealth(base)) || (base.getClass().getItemHealth(base) >= base.getClass().getItemMaxHealth(base))
|| (base.getType() != ESM::Weapon::sRecordId && base.getType() != ESM::Armor::sRecordId))) || (base.getType() != ESM::Weapon::sRecordId && base.getType() != ESM::Armor::sRecordId)))
return false; return false;

@ -319,14 +319,14 @@ namespace MWLua
std::string texture = options->get_or<std::string>("particleTextureOverride", ""); std::string texture = options->get_or<std::string>("particleTextureOverride", "");
float scale = options->get_or("scale", 1.f); float scale = options->get_or("scale", 1.f);
context.mLuaManager->addAction( context.mLuaManager->addAction(
[world, model = std::string(model), texture = std::move(texture), worldPos, scale, [world, model = VFS::Path::Normalized(model), texture = std::move(texture), worldPos, scale,
magicVfx]() { world->spawnEffect(model, texture, worldPos, scale, magicVfx); }, magicVfx]() { world->spawnEffect(model, texture, worldPos, scale, magicVfx); },
"openmw.vfx.spawn"); "openmw.vfx.spawn");
} }
else else
{ {
context.mLuaManager->addAction( context.mLuaManager->addAction([world, model = VFS::Path::Normalized(model),
[world, model = std::string(model), worldPos]() { world->spawnEffect(model, "", worldPos); }, worldPos]() { world->spawnEffect(model, "", worldPos); },
"openmw.vfx.spawn"); "openmw.vfx.spawn");
} }
}; };

@ -238,6 +238,11 @@ namespace MWLua
} }
void LuaManager::synchronizedUpdate() void LuaManager::synchronizedUpdate()
{
mLua.protectedCall([&](LuaUtil::LuaView&) { synchronizedUpdateUnsafe(); });
}
void LuaManager::synchronizedUpdateUnsafe()
{ {
if (mNewGameStarted) if (mNewGameStarted)
{ {

@ -170,6 +170,7 @@ namespace MWLua
LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr,
std::optional<LuaUtil::ScriptIdsWithInitializationData> autoStartConf = std::nullopt); std::optional<LuaUtil::ScriptIdsWithInitializationData> autoStartConf = std::nullopt);
void reloadAllScriptsImpl(); void reloadAllScriptsImpl();
void synchronizedUpdateUnsafe();
bool mInitialized = false; bool mInitialized = false;
bool mGlobalScriptsStarted = false; bool mGlobalScriptsStarted = false;

@ -214,7 +214,7 @@ namespace
const ESM::Static* const fx const ESM::Static* const fx
= world->getStore().get<ESM::Static>().search(ESM::RefId::stringRefId("VFX_Soul_Trap")); = world->getStore().get<ESM::Static>().search(ESM::RefId::stringRefId("VFX_Soul_Trap"));
if (fx != nullptr) if (fx != nullptr)
world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), "", world->spawnEffect(VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(fx->mModel)), "",
creature.getRefData().getPosition().asVec3()); creature.getRefData().getPosition().asVec3());
MWBase::Environment::get().getSoundManager()->playSound3D( MWBase::Environment::get().getSoundManager()->playSound3D(
@ -1806,7 +1806,8 @@ namespace MWMechanics
ESM::RefId::stringRefId("VFX_Summon_End")); ESM::RefId::stringRefId("VFX_Summon_End"));
if (fx) if (fx)
MWBase::Environment::get().getWorld()->spawnEffect( MWBase::Environment::get().getWorld()->spawnEffect(
Misc::ResourceHelpers::correctMeshPath(fx->mModel), "", ptr.getRefData().getPosition().asVec3()); VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(fx->mModel)), "",
ptr.getRefData().getPosition().asVec3());
// Remove the summoned creature's summoned creatures as well // Remove the summoned creature's summoned creatures as well
MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);

@ -846,14 +846,12 @@ namespace MWMechanics
mAI = true; mAI = true;
} }
bool MechanicsManager::isBoundItem(const MWWorld::Ptr& item) namespace
{ {
static std::set<ESM::RefId> boundItemIDCache; std::set<ESM::RefId> makeBoundItemIdCache()
// If this is empty then we haven't executed the GMST cache logic yet; or there isn't any sMagicBound* GMST's
// for some reason
if (boundItemIDCache.empty())
{ {
std::set<ESM::RefId> boundItemIDCache;
// Build a list of known bound item ID's // Build a list of known bound item ID's
const MWWorld::Store<ESM::GameSetting>& gameSettings const MWWorld::Store<ESM::GameSetting>& gameSettings
= MWBase::Environment::get().getESMStore()->get<ESM::GameSetting>(); = MWBase::Environment::get().getESMStore()->get<ESM::GameSetting>();
@ -870,15 +868,16 @@ namespace MWMechanics
boundItemIDCache.insert(ESM::RefId::stringRefId(currentGMSTValue)); boundItemIDCache.insert(ESM::RefId::stringRefId(currentGMSTValue));
} }
}
// Perform bound item check and assign the Flag_Bound bit if it passes return boundItemIDCache;
const ESM::RefId& tempItemID = item.getCellRef().getRefId(); }
}
if (boundItemIDCache.count(tempItemID) != 0) bool MechanicsManager::isBoundItem(const MWWorld::Ptr& item)
return true; {
static const std::set<ESM::RefId> boundItemIdCache = makeBoundItemIdCache();
return false; return boundItemIdCache.find(item.getCellRef().getRefId()) != boundItemIdCache.end();
} }
bool MechanicsManager::isAllowedToUse(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) bool MechanicsManager::isAllowedToUse(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim)

@ -72,12 +72,13 @@ namespace MWMechanics
{ {
if (effectInfo.mData.mRange == ESM::RT_Target) if (effectInfo.mData.mRange == ESM::RT_Target)
world->spawnEffect( world->spawnEffect(
Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, 1.0f); VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel)), texture,
mHitPosition, 1.0f);
continue; continue;
} }
else else
world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, world->spawnEffect(VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel)),
static_cast<float>(effectInfo.mData.mArea * 2)); texture, mHitPosition, static_cast<float>(effectInfo.mData.mArea * 2));
// Play explosion sound (make sure to use NoTrack, since we will delete the projectile now) // Play explosion sound (make sure to use NoTrack, since we will delete the projectile now)
{ {
@ -539,7 +540,8 @@ namespace MWMechanics
} }
scale = std::max(scale, 1.f); scale = std::max(scale, 1.f);
MWBase::Environment::get().getWorld()->spawnEffect( MWBase::Environment::get().getWorld()->spawnEffect(
Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), effect->mParticle, pos, scale); VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel)),
effect->mParticle, pos, scale);
} }
if (animation && !mCaster.getClass().isActor()) if (animation && !mCaster.getClass().isActor())

@ -412,9 +412,9 @@ namespace MWPhysics
if (ptr.mRef->mData.mPhysicsPostponed) if (ptr.mRef->mData.mPhysicsPostponed)
return; return;
std::string animationMesh = mesh; const VFS::Path::Normalized animationMesh = ptr.getClass().useAnim()
if (ptr.getClass().useAnim()) ? Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS())
animationMesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()); : mesh;
osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance = mShapeManager->getInstance(animationMesh); osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance = mShapeManager->getInstance(animationMesh);
if (!shapeInstance || !shapeInstance->mCollisionShape) if (!shapeInstance || !shapeInstance->mCollisionShape)
return; return;
@ -562,7 +562,8 @@ namespace MWPhysics
void PhysicsSystem::addActor(const MWWorld::Ptr& ptr, const std::string& mesh) void PhysicsSystem::addActor(const MWWorld::Ptr& ptr, const std::string& mesh)
{ {
std::string animationMesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()); const VFS::Path::Normalized animationMesh
= Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS());
osg::ref_ptr<const Resource::BulletShape> shape = mShapeManager->getShape(animationMesh); osg::ref_ptr<const Resource::BulletShape> shape = mShapeManager->getShape(animationMesh);
// Try to get shape from basic model as fallback for creatures // Try to get shape from basic model as fallback for creatures
@ -570,7 +571,7 @@ namespace MWPhysics
{ {
if (animationMesh != mesh) if (animationMesh != mesh)
{ {
shape = mShapeManager->getShape(mesh); shape = mShapeManager->getShape(VFS::Path::toNormalized(mesh));
} }
} }
@ -590,7 +591,8 @@ namespace MWPhysics
int PhysicsSystem::addProjectile( int PhysicsSystem::addProjectile(
const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius) const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius)
{ {
osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance = mShapeManager->getInstance(mesh); osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance
= mShapeManager->getInstance(VFS::Path::toNormalized(mesh));
assert(shapeInstance); assert(shapeInstance);
float radius = computeRadius ? shapeInstance->mCollisionBox.mExtents.length() / 2.f : 1.f; float radius = computeRadius ? shapeInstance->mCollisionBox.mExtents.length() / 2.f : 1.f;

@ -67,31 +67,29 @@ namespace MWRender
} }
PartHolderPtr ActorAnimation::attachMesh( PartHolderPtr ActorAnimation::attachMesh(
const std::string& model, std::string_view bonename, bool enchantedGlow, osg::Vec4f* glowColor) VFS::Path::NormalizedView model, std::string_view bonename, const osg::Vec4f* glowColor)
{ {
osg::Group* parent = getBoneByName(bonename); osg::Group* parent = getBoneByName(bonename);
if (!parent) if (!parent)
return nullptr; return nullptr;
osg::ref_ptr<osg::Node> instance osg::ref_ptr<osg::Node> instance = mResourceSystem->getSceneManager()->getInstance(model, parent);
= mResourceSystem->getSceneManager()->getInstance(VFS::Path::toNormalized(model), parent);
const NodeMap& nodeMap = getNodeMap(); const NodeMap& nodeMap = getNodeMap();
NodeMap::const_iterator found = nodeMap.find(bonename); NodeMap::const_iterator found = nodeMap.find(bonename);
if (found == nodeMap.end()) if (found == nodeMap.end())
return {}; return {};
if (enchantedGlow) if (glowColor != nullptr)
mGlowUpdater = SceneUtil::addEnchantedGlow(instance, mResourceSystem, *glowColor); mGlowUpdater = SceneUtil::addEnchantedGlow(instance, mResourceSystem, *glowColor);
return std::make_unique<PartHolder>(instance); return std::make_unique<PartHolder>(instance);
} }
osg::ref_ptr<osg::Node> ActorAnimation::attach( osg::ref_ptr<osg::Node> ActorAnimation::attach(
const std::string& model, std::string_view bonename, std::string_view bonefilter, bool isLight) VFS::Path::NormalizedView model, std::string_view bonename, std::string_view bonefilter, bool isLight)
{ {
osg::ref_ptr<const osg::Node> templateNode osg::ref_ptr<const osg::Node> templateNode = mResourceSystem->getSceneManager()->getTemplate(model);
= mResourceSystem->getSceneManager()->getTemplate(VFS::Path::toNormalized(model));
const NodeMap& nodeMap = getNodeMap(); const NodeMap& nodeMap = getNodeMap();
auto found = nodeMap.find(bonename); auto found = nodeMap.find(bonename);
@ -218,20 +216,20 @@ namespace MWRender
return; return;
} }
std::string mesh = getSheathedShieldMesh(*shield); const VFS::Path::Normalized mesh = getSheathedShieldMesh(*shield);
if (mesh.empty()) if (mesh.empty())
return; return;
std::string_view boneName = "Bip01 AttachShield"; constexpr std::string_view boneName = "Bip01 AttachShield";
osg::Vec4f glowColor = shield->getClass().getEnchantmentColor(*shield); const bool isEnchanted = !shield->getClass().getEnchantment(*shield).empty();
const std::string holsteredName = addSuffixBeforeExtension(mesh, "_sh"); const osg::Vec4f glowColor = isEnchanted ? shield->getClass().getEnchantmentColor(*shield) : osg::Vec4f();
bool isEnchanted = !shield->getClass().getEnchantment(*shield).empty(); const VFS::Path::Normalized holsteredName = addSuffixBeforeExtension(mesh, "_sh");
// If we have no dedicated sheath model, use basic shield model as fallback. // If we have no dedicated sheath model, use basic shield model as fallback.
if (!mResourceSystem->getVFS()->exists(holsteredName)) if (!mResourceSystem->getVFS()->exists(holsteredName))
mHolsteredShield = attachMesh(mesh, boneName, isEnchanted, &glowColor); mHolsteredShield = attachMesh(mesh, boneName, isEnchanted ? &glowColor : nullptr);
else else
mHolsteredShield = attachMesh(holsteredName, boneName, isEnchanted, &glowColor); mHolsteredShield = attachMesh(holsteredName, boneName, isEnchanted ? &glowColor : nullptr);
if (!mHolsteredShield) if (!mHolsteredShield)
return; return;
@ -245,8 +243,7 @@ namespace MWRender
// file. // file.
if (shieldNode && !shieldNode->getNumChildren()) if (shieldNode && !shieldNode->getNumChildren())
{ {
osg::ref_ptr<osg::Node> fallbackNode osg::ref_ptr<osg::Node> fallbackNode = mResourceSystem->getSceneManager()->getInstance(mesh, shieldNode);
= mResourceSystem->getSceneManager()->getInstance(VFS::Path::toNormalized(mesh), shieldNode);
if (isEnchanted) if (isEnchanted)
SceneUtil::addEnchantedGlow(shieldNode, mResourceSystem, glowColor); SceneUtil::addEnchantedGlow(shieldNode, mResourceSystem, glowColor);
} }
@ -341,20 +338,24 @@ namespace MWRender
if (MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown) if (MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown)
showHolsteredWeapons = false; showHolsteredWeapons = false;
std::string mesh = weapon->getClass().getCorrectedModel(*weapon); const VFS::Path::Normalized mesh = weapon->getClass().getCorrectedModel(*weapon);
std::string_view boneName = getHolsteredWeaponBoneName(*weapon); if (mesh.empty())
if (mesh.empty() || boneName.empty()) return;
const std::string_view boneName = getHolsteredWeaponBoneName(*weapon);
if (boneName.empty())
return; return;
// If the scabbard is not found, use the weapon mesh as fallback. // If the scabbard is not found, use the weapon mesh as fallback.
const std::string scabbardName = addSuffixBeforeExtension(mesh, "_sh"); const VFS::Path::Normalized scabbardName = addSuffixBeforeExtension(mesh, "_sh");
bool isEnchanted = !weapon->getClass().getEnchantment(*weapon).empty(); const bool isEnchanted = !weapon->getClass().getEnchantment(*weapon).empty();
if (!mResourceSystem->getVFS()->exists(scabbardName)) if (!mResourceSystem->getVFS()->exists(scabbardName))
{ {
if (showHolsteredWeapons) if (showHolsteredWeapons)
{ {
osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); const osg::Vec4f glowColor
mScabbard = attachMesh(mesh, boneName, isEnchanted, &glowColor); = isEnchanted ? weapon->getClass().getEnchantmentColor(*weapon) : osg::Vec4f();
mScabbard = attachMesh(mesh, boneName, isEnchanted ? &glowColor : nullptr);
if (mScabbard) if (mScabbard)
resetControllers(mScabbard->getNode()); resetControllers(mScabbard->getNode());
} }
@ -384,7 +385,7 @@ namespace MWRender
if (!weaponNode->getNumChildren()) if (!weaponNode->getNumChildren())
{ {
osg::ref_ptr<osg::Node> fallbackNode osg::ref_ptr<osg::Node> fallbackNode
= mResourceSystem->getSceneManager()->getInstance(VFS::Path::toNormalized(mesh), weaponNode); = mResourceSystem->getSceneManager()->getInstance(mesh, weaponNode);
resetControllers(fallbackNode); resetControllers(fallbackNode);
} }

@ -5,6 +5,8 @@
#include <osg/ref_ptr> #include <osg/ref_ptr>
#include <components/vfs/pathutil.hpp>
#include "../mwworld/containerstore.hpp" #include "../mwworld/containerstore.hpp"
#include "animation.hpp" #include "animation.hpp"
@ -45,21 +47,18 @@ namespace MWRender
protected: protected:
osg::Group* getBoneByName(std::string_view boneName) const; osg::Group* getBoneByName(std::string_view boneName) const;
virtual void updateHolsteredWeapon(bool showHolsteredWeapons); void updateHolsteredWeapon(bool showHolsteredWeapons);
virtual void updateHolsteredShield(bool showCarriedLeft); void updateHolsteredShield(bool showCarriedLeft);
virtual void updateQuiver(); void updateQuiver();
std::string getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const; std::string getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const;
virtual std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const; virtual std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const;
virtual std::string_view getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); std::string_view getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon);
virtual PartHolderPtr attachMesh(
const std::string& model, std::string_view bonename, bool enchantedGlow, osg::Vec4f* glowColor); PartHolderPtr attachMesh(
virtual PartHolderPtr attachMesh(const std::string& model, std::string_view bonename) VFS::Path::NormalizedView model, std::string_view bonename, const osg::Vec4f* glowColor = nullptr);
{
osg::Vec4f stubColor = osg::Vec4f(0, 0, 0, 0);
return attachMesh(model, bonename, false, &stubColor);
}
osg::ref_ptr<osg::Node> attach( osg::ref_ptr<osg::Node> attach(
const std::string& model, std::string_view bonename, std::string_view bonefilter, bool isLight); VFS::Path::NormalizedView model, std::string_view bonename, std::string_view bonefilter, bool isLight);
PartHolderPtr mScabbard; PartHolderPtr mScabbard;
PartHolderPtr mHolsteredShield; PartHolderPtr mHolsteredShield;

@ -388,17 +388,20 @@ namespace
std::string_view mEffectId; std::string_view mEffectId;
}; };
osg::ref_ptr<osg::LightModel> getVFXLightModelInstance() namespace
{ {
static osg::ref_ptr<osg::LightModel> lightModel = nullptr; osg::ref_ptr<osg::LightModel> makeVFXLightModelInstance()
if (!lightModel)
{ {
lightModel = new osg::LightModel; osg::ref_ptr<osg::LightModel> lightModel = new osg::LightModel;
lightModel->setAmbientIntensity({ 1, 1, 1, 1 }); lightModel->setAmbientIntensity({ 1, 1, 1, 1 });
return lightModel;
} }
return lightModel; const osg::ref_ptr<osg::LightModel>& getVFXLightModelInstance()
{
static const osg::ref_ptr<osg::LightModel> lightModel = makeVFXLightModelInstance();
return lightModel;
}
} }
void assignBoneBlendCallbackRecursive(MWRender::BoneAnimBlendController* controller, osg::Node* parent, bool isRoot) void assignBoneBlendCallbackRecursive(MWRender::BoneAnimBlendController* controller, osg::Node* parent, bool isRoot)
@ -1508,10 +1511,10 @@ namespace MWRender
} }
animationPath.replace(animationPath.size() - 4, 4, "/"); animationPath.replace(animationPath.size() - 4, 4, "/");
for (const auto& name : resourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) for (const VFS::Path::Normalized& name : resourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath))
{ {
if (Misc::getFileExtension(name) == "nif") if (Misc::getFileExtension(name) == "nif")
loadBonesFromFile(node, VFS::Path::toNormalized(name), resourceSystem); loadBonesFromFile(node, name, resourceSystem);
} }
} }

@ -533,7 +533,7 @@ namespace MWRender
: CharacterPreview( : CharacterPreview(
parent, resourceSystem, MWMechanics::getPlayer(), 512, 512, osg::Vec3f(0, 125, 8), osg::Vec3f(0, 0, 8)) parent, resourceSystem, MWMechanics::getPlayer(), 512, 512, osg::Vec3f(0, 125, 8), osg::Vec3f(0, 0, 8))
, mBase(*mCharacter.get<ESM::NPC>()->mBase) , mBase(*mCharacter.get<ESM::NPC>()->mBase)
, mRef(&mBase) , mRef(ESM::makeBlankCellRef(), &mBase)
, mPitchRadians(osg::DegreesToRadians(6.f)) , mPitchRadians(osg::DegreesToRadians(6.f))
{ {
mCharacter = MWWorld::Ptr(&mRef, nullptr); mCharacter = MWWorld::Ptr(&mRef, nullptr);

@ -111,7 +111,7 @@ namespace MWRender
MWWorld::ConstPtr item = *it; MWWorld::ConstPtr item = *it;
std::string_view bonename; std::string_view bonename;
std::string itemModel = item.getClass().getCorrectedModel(item); VFS::Path::Normalized itemModel = item.getClass().getCorrectedModel(item);
if (slot == MWWorld::InventoryStore::Slot_CarriedRight) if (slot == MWWorld::InventoryStore::Slot_CarriedRight)
{ {
if (item.getType() == ESM::Weapon::sRecordId) if (item.getType() == ESM::Weapon::sRecordId)

@ -484,15 +484,24 @@ namespace MWRender
bool is1stPerson = mViewMode == VM_FirstPerson; bool is1stPerson = mViewMode == VM_FirstPerson;
bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0;
std::string_view base;
if (!isWerewolf)
{
if (!is1stPerson)
base = Settings::models().mXbaseanim.get().value();
else
base = Settings::models().mXbaseanim1st.get().value();
}
const std::string defaultSkeleton = Misc::ResourceHelpers::correctActorModelPath( const std::string defaultSkeleton = Misc::ResourceHelpers::correctActorModelPath(
getActorSkeleton(is1stPerson, isFemale, isBeast, isWerewolf), mResourceSystem->getVFS()); getActorSkeleton(is1stPerson, isFemale, isBeast, isWerewolf), mResourceSystem->getVFS());
std::string smodel = defaultSkeleton; std::string smodel = defaultSkeleton;
bool isBase = !isWerewolf; bool isCustomModel = false;
if (!is1stPerson && !isWerewolf && !mNpc->mModel.empty()) if (!is1stPerson && !isWerewolf && !mNpc->mModel.empty())
{ {
std::string model = Misc::ResourceHelpers::correctMeshPath(mNpc->mModel); std::string model = Misc::ResourceHelpers::correctMeshPath(mNpc->mModel);
isBase = isDefaultActorSkeleton(model); isCustomModel = !isDefaultActorSkeleton(model);
smodel = Misc::ResourceHelpers::correctActorModelPath(model, mResourceSystem->getVFS()); smodel = Misc::ResourceHelpers::correctActorModelPath(model, mResourceSystem->getVFS());
} }
@ -500,33 +509,21 @@ namespace MWRender
updateParts(); updateParts();
if (!is1stPerson) if (!base.empty())
{ addAnimSource(base, smodel);
const std::string& base = Settings::models().mXbaseanim.get().value();
if (!isWerewolf)
addAnimSource(base, smodel);
if (!isBase) if (defaultSkeleton != base)
{ addAnimSource(defaultSkeleton, smodel);
addAnimSource(defaultSkeleton, smodel);
addAnimSource(smodel, smodel);
}
else if (base != defaultSkeleton)
{
addAnimSource(defaultSkeleton, smodel);
}
if (!isWerewolf && isBeast && mNpc->mRace.contains("argonian")) if (isCustomModel)
addAnimSource("meshes\\xargonian_swimkna.nif", smodel); addAnimSource(smodel, smodel);
}
else
{
if (!isWerewolf)
addAnimSource(Settings::models().mXbaseanim1st.get().value(), smodel);
if (!isBase) const bool customArgonianSwim = !is1stPerson && !isWerewolf && isBeast && mNpc->mRace.contains("argonian");
addAnimSource(smodel, smodel); if (customArgonianSwim)
addAnimSource(Settings::models().mXargonianswimkna.get().value(), smodel);
if (is1stPerson)
{
mObjectRoot->setNodeMask(Mask_FirstPerson); mObjectRoot->setNodeMask(Mask_FirstPerson);
mObjectRoot->addCullCallback(new OverrideFieldOfViewCallback(mFirstPersonFieldOfView)); mObjectRoot->addCullCallback(new OverrideFieldOfViewCallback(mFirstPersonFieldOfView));
} }
@ -681,11 +678,11 @@ namespace MWRender
PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, std::string_view bonename, PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, std::string_view bonename,
std::string_view bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) std::string_view bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight)
{ {
osg::ref_ptr<osg::Node> attached = attach(model, bonename, bonefilter, isLight); osg::ref_ptr<osg::Node> attached = attach(VFS::Path::toNormalized(model), bonename, bonefilter, isLight);
if (enchantedGlow) if (enchantedGlow)
mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor); mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor);
return std::make_unique<PartHolder>(attached); return std::make_unique<PartHolder>(std::move(attached));
} }
osg::Vec3f NpcAnimation::runAnimation(float timepassed) osg::Vec3f NpcAnimation::runAnimation(float timepassed)

@ -20,6 +20,7 @@
#include <components/esm3/loaddoor.hpp> #include <components/esm3/loaddoor.hpp>
#include <components/esm3/loadstat.hpp> #include <components/esm3/loadstat.hpp>
#include <components/esm3/readerscache.hpp> #include <components/esm3/readerscache.hpp>
#include <components/misc/pathhelpers.hpp>
#include <components/misc/resourcehelpers.hpp> #include <components/misc/resourcehelpers.hpp>
#include <components/misc/rng.hpp> #include <components/misc/rng.hpp>
#include <components/resource/scenemanager.hpp> #include <components/resource/scenemanager.hpp>
@ -639,7 +640,7 @@ namespace MWRender
continue; continue;
const int type = store.findStatic(ref.mRefId); const int type = store.findStatic(ref.mRefId);
std::string model = getModel(type, ref.mRefId, store); VFS::Path::Normalized model = getModel(type, ref.mRefId, store);
if (model.empty()) if (model.empty())
continue; continue;
model = Misc::ResourceHelpers::correctMeshPath(model); model = Misc::ResourceHelpers::correctMeshPath(model);
@ -647,10 +648,10 @@ namespace MWRender
if (activeGrid && type != ESM::REC_STAT) if (activeGrid && type != ESM::REC_STAT)
{ {
model = Misc::ResourceHelpers::correctActorModelPath(model, mSceneManager->getVFS()); model = Misc::ResourceHelpers::correctActorModelPath(model, mSceneManager->getVFS());
std::string kfname = Misc::StringUtils::lowerCase(model); if (Misc::getFileExtension(model) == "nif")
if (kfname.size() > 4 && kfname.ends_with(".nif"))
{ {
kfname.replace(kfname.size() - 4, 4, ".kf"); VFS::Path::Normalized kfname = model;
kfname.changeExtension("kf");
if (mSceneManager->getVFS()->exists(kfname)) if (mSceneManager->getVFS()->exists(kfname))
continue; continue;
} }
@ -671,7 +672,7 @@ namespace MWRender
->second; ->second;
} }
osg::ref_ptr<const osg::Node> cnode = mSceneManager->getTemplate(VFS::Path::toNormalized(model), false); osg::ref_ptr<const osg::Node> cnode = mSceneManager->getTemplate(model, false);
if (activeGrid) if (activeGrid)
{ {

@ -206,8 +206,14 @@ namespace MWRender
}; };
// PASS: Blot in all ripple spawners // PASS: Blot in all ripple spawners
mProgramBlobber->apply(state); state.pushStateSet(frameState.mStateset);
state.apply(frameState.mStateset); state.apply();
state.applyAttribute(mProgramBlobber);
for (const auto& [name, stack] : state.getUniformMap())
{
if (!stack.uniformVec.empty())
state.getLastAppliedProgramObject()->apply(*(stack.uniformVec.back().first));
}
if (mUseCompute) if (mUseCompute)
{ {
@ -225,8 +231,12 @@ namespace MWRender
} }
// PASS: Wave simulation // PASS: Wave simulation
mProgramSimulation->apply(state); state.applyAttribute(mProgramSimulation);
state.apply(frameState.mStateset); for (const auto& [name, stack] : state.getUniformMap())
{
if (!stack.uniformVec.empty())
state.getLastAppliedProgramObject()->apply(*(stack.uniformVec.back().first));
}
if (mUseCompute) if (mUseCompute)
{ {
@ -242,6 +252,8 @@ namespace MWRender
state.applyTextureAttribute(0, mTextures[1]); state.applyTextureAttribute(0, mTextures[1]);
osg::Geometry::drawImplementation(renderInfo); osg::Geometry::drawImplementation(renderInfo);
} }
state.popStateSet();
} }
osg::Texture* RipplesSurface::getColorTexture() const osg::Texture* RipplesSurface::getColorTexture() const

@ -11,6 +11,7 @@
#include <components/esm3/loadcell.hpp> #include <components/esm3/loadcell.hpp>
#include <components/loadinglistener/reporter.hpp> #include <components/loadinglistener/reporter.hpp>
#include <components/misc/constants.hpp> #include <components/misc/constants.hpp>
#include <components/misc/pathhelpers.hpp>
#include <components/misc/resourcehelpers.hpp> #include <components/misc/resourcehelpers.hpp>
#include <components/misc/strings/algorithm.hpp> #include <components/misc/strings/algorithm.hpp>
#include <components/misc/strings/lower.hpp> #include <components/misc/strings/lower.hpp>
@ -105,8 +106,8 @@ namespace MWWorld
} }
} }
std::string mesh; VFS::Path::Normalized mesh;
std::string kfname; VFS::Path::Normalized kfname;
for (std::string_view path : mMeshes) for (std::string_view path : mMeshes)
{ {
if (mAbort) if (mAbort)
@ -121,19 +122,15 @@ namespace MWWorld
if (!vfs.exists(mesh)) if (!vfs.exists(mesh))
continue; continue;
size_t slashpos = mesh.find_last_of("/\\"); if (Misc::getFileName(mesh).starts_with('x') && Misc::getFileExtension(mesh) == "nif")
if (slashpos != std::string::npos && slashpos != mesh.size() - 1)
{ {
if (Misc::StringUtils::toLower(mesh[slashpos + 1]) == 'x' kfname = mesh;
&& Misc::StringUtils::ciEndsWith(mesh, ".nif")) kfname.changeExtension("kf");
{ if (vfs.exists(kfname))
kfname = mesh; mPreloadedObjects.insert(mKeyframeManager->get(kfname));
kfname.replace(kfname.size() - 4, 4, ".kf");
if (vfs.exists(kfname))
mPreloadedObjects.insert(mKeyframeManager->get(kfname));
}
} }
mPreloadedObjects.insert(mSceneManager->getTemplate(VFS::Path::toNormalized(mesh)));
mPreloadedObjects.insert(mSceneManager->getTemplate(mesh));
if (mPreloadInstances) if (mPreloadInstances)
mPreloadedObjects.insert(mBulletShapeManager->cacheInstance(mesh)); mPreloadedObjects.insert(mBulletShapeManager->cacheInstance(mesh));
else else

@ -317,9 +317,9 @@ namespace
} }
// new reference // new reference
MWWorld::LiveCellRef<T> ref(record); MWWorld::LiveCellRef<T> ref(ESM::makeBlankCellRef(), record);
ref.load(state); ref.load(state);
collection.mList.push_back(ref); collection.mList.push_back(std::move(ref));
MWWorld::LiveCellRefBase* base = &collection.mList.back(); MWWorld::LiveCellRefBase* base = &collection.mList.back();
MWBase::Environment::get().getWorldModel()->registerPtr(MWWorld::Ptr(base, cellstore)); MWBase::Environment::get().getWorldModel()->registerPtr(MWWorld::Ptr(base, cellstore));
@ -426,9 +426,9 @@ namespace MWWorld
liveCellRef.mData.setDeletedByContentFile(true); liveCellRef.mData.setDeletedByContentFile(true);
if (iter != mList.end()) if (iter != mList.end())
*iter = liveCellRef; *iter = std::move(liveCellRef);
else else
mList.push_back(liveCellRef); mList.push_back(std::move(liveCellRef));
} }
else else
{ {
@ -455,7 +455,7 @@ namespace MWWorld
LiveCellRef<X> liveCellRef(ref, ptr); LiveCellRef<X> liveCellRef(ref, ptr);
if (!isEnabled(ref, esmStore)) if (!isEnabled(ref, esmStore))
liveCellRef.mData.disable(); liveCellRef.mData.disable();
list.push_back(liveCellRef); list.push_back(std::move(liveCellRef));
} }
template <typename X> template <typename X>

@ -101,9 +101,9 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::getState(
if (!record) if (!record)
return ContainerStoreIterator(this); return ContainerStoreIterator(this);
LiveCellRef<T> ref(record); LiveCellRef<T> ref(ESM::makeBlankCellRef(), record);
ref.load(state); ref.load(state);
collection.mList.push_back(ref); collection.mList.push_back(std::move(ref));
auto it = ContainerStoreIterator(this, --collection.mList.end()); auto it = ContainerStoreIterator(this, --collection.mList.end());
MWBase::Environment::get().getWorldModel()->registerPtr(*it); MWBase::Environment::get().getWorldModel()->registerPtr(*it);

@ -15,104 +15,122 @@
#include "ptr.hpp" #include "ptr.hpp"
#include "worldmodel.hpp" #include "worldmodel.hpp"
MWWorld::LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM::CellRef& cref) namespace MWWorld
: mClass(&Class::get(type))
, mRef(cref)
, mData(cref)
{ {
} LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM::CellRef& cref)
: mClass(&Class::get(type))
, mRef(cref)
, mData(cref)
{
}
MWWorld::LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::Reference& cref) LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::Reference& cref)
: mClass(&Class::get(type)) : mClass(&Class::get(type))
, mRef(cref) , mRef(cref)
, mData(cref) , mData(cref)
{ {
} }
MWWorld::LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::ActorCharacter& cref) LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::ActorCharacter& cref)
: mClass(&Class::get(type)) : mClass(&Class::get(type))
, mRef(cref) , mRef(cref)
, mData(cref) , mData(cref)
{ {
} }
MWWorld::LiveCellRefBase::~LiveCellRefBase() LiveCellRefBase::LiveCellRefBase(LiveCellRefBase&& other) noexcept
{ : mClass(other.mClass)
MWBase::Environment::get().getWorldModel()->deregisterLiveCellRef(*this); , mRef(std::move(other.mRef))
} , mData(std::move(other.mData))
, mWorldModel(std::exchange(other.mWorldModel, nullptr))
{
}
void MWWorld::LiveCellRefBase::loadImp(const ESM::ObjectState& state) LiveCellRefBase::~LiveCellRefBase()
{ {
mRef = MWWorld::CellRef(state.mRef); if (mWorldModel != nullptr)
mData = RefData(state, mData.isDeletedByContentFile()); mWorldModel->deregisterLiveCellRef(*this);
}
Ptr ptr(this); LiveCellRefBase& LiveCellRefBase::operator=(LiveCellRefBase&& other) noexcept
{
mClass = other.mClass;
mRef = std::move(other.mRef);
mData = std::move(other.mData);
mWorldModel = std::exchange(other.mWorldModel, nullptr);
return *this;
}
if (state.mHasLocals) void LiveCellRefBase::loadImp(const ESM::ObjectState& state)
{ {
const ESM::RefId& scriptId = mClass->getScript(ptr); mRef = CellRef(state.mRef);
// Make sure we still have a script. It could have been coming from a content file that is no longer active. mData = RefData(state, mData.isDeletedByContentFile());
if (!scriptId.empty())
Ptr ptr(this);
if (state.mHasLocals)
{ {
if (const ESM::Script* script const ESM::RefId& scriptId = mClass->getScript(ptr);
= MWBase::Environment::get().getESMStore()->get<ESM::Script>().search(scriptId)) // Make sure we still have a script. It could have been coming from a content file that is no longer active.
if (!scriptId.empty())
{ {
try if (const ESM::Script* script
{ = MWBase::Environment::get().getESMStore()->get<ESM::Script>().search(scriptId))
mData.setLocals(*script);
mData.getLocals().read(state.mLocals, scriptId);
}
catch (const std::exception& exception)
{ {
Log(Debug::Error) << "Error: failed to load state for local script " << scriptId try
<< " because an exception has been thrown: " << exception.what(); {
mData.setLocals(*script);
mData.getLocals().read(state.mLocals, scriptId);
}
catch (const std::exception& exception)
{
Log(Debug::Error) << "Error: failed to load state for local script " << scriptId
<< " because an exception has been thrown: " << exception.what();
}
} }
} }
} }
}
mClass->readAdditionalState(ptr, state); mClass->readAdditionalState(ptr, state);
if (!mRef.getSoul().empty() if (!mRef.getSoul().empty()
&& !MWBase::Environment::get().getESMStore()->get<ESM::Creature>().search(mRef.getSoul())) && !MWBase::Environment::get().getESMStore()->get<ESM::Creature>().search(mRef.getSoul()))
{ {
Log(Debug::Warning) << "Soul '" << mRef.getSoul() << "' not found, removing the soul from soul gem"; Log(Debug::Warning) << "Soul '" << mRef.getSoul() << "' not found, removing the soul from soul gem";
mRef.setSoul(ESM::RefId()); mRef.setSoul(ESM::RefId());
} }
MWBase::Environment::get().getLuaManager()->loadLocalScripts(ptr, state.mLuaScripts); MWBase::Environment::get().getLuaManager()->loadLocalScripts(ptr, state.mLuaScripts);
} }
void MWWorld::LiveCellRefBase::saveImp(ESM::ObjectState& state) const void LiveCellRefBase::saveImp(ESM::ObjectState& state) const
{ {
mRef.writeState(state); mRef.writeState(state);
ConstPtr ptr(this); ConstPtr ptr(this);
mData.write(state, mClass->getScript(ptr)); mData.write(state, mClass->getScript(ptr));
MWBase::Environment::get().getLuaManager()->saveLocalScripts( MWBase::Environment::get().getLuaManager()->saveLocalScripts(
Ptr(const_cast<LiveCellRefBase*>(this)), state.mLuaScripts); Ptr(const_cast<LiveCellRefBase*>(this)), state.mLuaScripts);
mClass->writeAdditionalState(ptr, state); mClass->writeAdditionalState(ptr, state);
} }
bool MWWorld::LiveCellRefBase::checkStateImp(const ESM::ObjectState& state) bool LiveCellRefBase::checkStateImp(const ESM::ObjectState& state)
{ {
return true; return true;
} }
unsigned int MWWorld::LiveCellRefBase::getType() const unsigned int LiveCellRefBase::getType() const
{ {
return mClass->getType(); return mClass->getType();
} }
bool MWWorld::LiveCellRefBase::isDeleted() const bool LiveCellRefBase::isDeleted() const
{ {
return mData.isDeletedByContentFile() || mRef.getCount(false) == 0; return mData.isDeletedByContentFile() || mRef.getCount(false) == 0;
} }
namespace MWWorld
{
std::string makeDynamicCastErrorMessage(const LiveCellRefBase* value, std::string_view recordType) std::string makeDynamicCastErrorMessage(const LiveCellRefBase* value, std::string_view recordType)
{ {
std::stringstream message; std::stringstream message;

@ -17,6 +17,7 @@ namespace MWWorld
class Ptr; class Ptr;
class ESMStore; class ESMStore;
class Class; class Class;
class WorldModel;
template <typename X> template <typename X>
struct LiveCellRef; struct LiveCellRef;
@ -29,17 +30,28 @@ namespace MWWorld
/** Information about this instance, such as 3D location and rotation /** Information about this instance, such as 3D location and rotation
* and individual type-dependent data. * and individual type-dependent data.
*/ */
MWWorld::CellRef mRef; CellRef mRef;
/** runtime-data */ /** runtime-data */
RefData mData; RefData mData;
LiveCellRefBase(unsigned int type, const ESM::CellRef& cref = ESM::CellRef()); WorldModel* mWorldModel = nullptr;
LiveCellRefBase(unsigned int type, const ESM::CellRef& cref);
LiveCellRefBase(unsigned int type, const ESM4::Reference& cref); LiveCellRefBase(unsigned int type, const ESM4::Reference& cref);
LiveCellRefBase(unsigned int type, const ESM4::ActorCharacter& cref); LiveCellRefBase(unsigned int type, const ESM4::ActorCharacter& cref);
LiveCellRefBase(const LiveCellRefBase& other) = default;
LiveCellRefBase(LiveCellRefBase&& other) noexcept;
/* Need this for the class to be recognized as polymorphic */ /* Need this for the class to be recognized as polymorphic */
virtual ~LiveCellRefBase(); virtual ~LiveCellRefBase();
LiveCellRefBase& operator=(const LiveCellRefBase& other) = default;
LiveCellRefBase& operator=(LiveCellRefBase&& other) noexcept;
virtual void load(const ESM::ObjectState& state) = 0; virtual void load(const ESM::ObjectState& state) = 0;
///< Load state into a LiveCellRef, that has already been initialised with base and class. ///< Load state into a LiveCellRef, that has already been initialised with base and class.
/// ///
@ -132,12 +144,6 @@ namespace MWWorld
{ {
} }
LiveCellRef(const X* b = nullptr)
: LiveCellRefBase(X::sRecordId)
, mBase(b)
{
}
// The object that this instance is based on. // The object that this instance is based on.
const X* mBase; const X* mBase;

@ -36,8 +36,20 @@
namespace MWWorld namespace MWWorld
{ {
namespace
{
ESM::CellRef makePlayerCellRef()
{
ESM::CellRef result;
result.blank();
result.mRefID = ESM::RefId::stringRefId("Player");
return result;
}
}
Player::Player(const ESM::NPC* player) Player::Player(const ESM::NPC* player)
: mCellStore(nullptr) : mPlayer(makePlayerCellRef(), player)
, mCellStore(nullptr)
, mLastKnownExteriorPosition(0, 0, 0) , mLastKnownExteriorPosition(0, 0, 0)
, mMarkedPosition(ESM::Position()) , mMarkedPosition(ESM::Position())
, mMarkedCell(nullptr) , mMarkedCell(nullptr)
@ -46,11 +58,6 @@ namespace MWWorld
, mPaidCrimeId(-1) , mPaidCrimeId(-1)
, mJumping(false) , mJumping(false)
{ {
ESM::CellRef cellRef;
cellRef.blank();
cellRef.mRefID = ESM::RefId::stringRefId("Player");
mPlayer = LiveCellRef<ESM::NPC>(cellRef, player);
ESM::Position playerPos = mPlayer.mData.getPosition(); ESM::Position playerPos = mPlayer.mData.getPosition();
playerPos.pos[0] = playerPos.pos[1] = playerPos.pos[2] = 0; playerPos.pos[0] = playerPos.pos[1] = playerPos.pos[2] = 0;
mPlayer.mData.setPosition(playerPos); mPlayer.mData.setPosition(playerPos);

@ -451,8 +451,7 @@ namespace MWWorld
} }
else if (!ESM::isEsm4Ext(worldspace)) else if (!ESM::isEsm4Ext(worldspace))
{ {
static std::vector<float> defaultHeight; static const std::vector<float> defaultHeight(verts * verts, ESM::Land::DEFAULT_HEIGHT);
defaultHeight.resize(verts * verts, ESM::Land::DEFAULT_HEIGHT);
mPhysics->addHeightField(defaultHeight.data(), cellX, cellY, worldsize, verts, mPhysics->addHeightField(defaultHeight.data(), cellX, cellY, worldsize, verts,
ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT, land.get()); ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT, land.get());
} }
@ -1126,19 +1125,19 @@ namespace MWWorld
void Scene::preload(const std::string& mesh, bool useAnim) void Scene::preload(const std::string& mesh, bool useAnim)
{ {
std::string meshPath = mesh; const VFS::Path::Normalized meshPath = useAnim
if (useAnim) ? Misc::ResourceHelpers::correctActorModelPath(mesh, mRendering.getResourceSystem()->getVFS())
meshPath = Misc::ResourceHelpers::correctActorModelPath(meshPath, mRendering.getResourceSystem()->getVFS()); : mesh;
if (!mRendering.getResourceSystem()->getSceneManager()->checkLoaded(meshPath, mRendering.getReferenceTime())) if (mRendering.getResourceSystem()->getSceneManager()->checkLoaded(meshPath, mRendering.getReferenceTime()))
{ return;
osg::ref_ptr<PreloadMeshItem> item(new PreloadMeshItem(
VFS::Path::toNormalized(meshPath), mRendering.getResourceSystem()->getSceneManager())); osg::ref_ptr<PreloadMeshItem> item(
mRendering.getWorkQueue()->addWorkItem(item); new PreloadMeshItem(meshPath, mRendering.getResourceSystem()->getSceneManager()));
const auto isDone = [](const osg::ref_ptr<SceneUtil::WorkItem>& v) { return v->isDone(); }; mRendering.getWorkQueue()->addWorkItem(item);
mWorkItems.erase(std::remove_if(mWorkItems.begin(), mWorkItems.end(), isDone), mWorkItems.end()); const auto isDone = [](const osg::ref_ptr<SceneUtil::WorkItem>& v) { return v->isDone(); };
mWorkItems.emplace_back(std::move(item)); mWorkItems.erase(std::remove_if(mWorkItems.begin(), mWorkItems.end(), isDone), mWorkItems.end());
} mWorkItems.emplace_back(std::move(item));
} }
void Scene::preloadCells(float dt) void Scene::preloadCells(float dt)

@ -3644,10 +3644,10 @@ namespace MWWorld
mRendering->spawnEffect(model, texture, worldPosition, 1.0f, false); mRendering->spawnEffect(model, texture, worldPosition, 1.0f, false);
} }
void World::spawnEffect(const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos, void World::spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride,
float scale, bool isMagicVFX) const osg::Vec3f& worldPos, float scale, bool isMagicVFX)
{ {
mRendering->spawnEffect(VFS::Path::toNormalized(model), textureOverride, worldPos, scale, isMagicVFX); mRendering->spawnEffect(model, textureOverride, worldPos, scale, isMagicVFX);
} }
struct ResetActorsVisitor struct ResetActorsVisitor

@ -7,6 +7,7 @@
#include <components/esm3/readerscache.hpp> #include <components/esm3/readerscache.hpp>
#include <components/misc/rng.hpp> #include <components/misc/rng.hpp>
#include <components/settings/settings.hpp> #include <components/settings/settings.hpp>
#include <components/vfs/pathutil.hpp>
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
@ -603,8 +604,8 @@ namespace MWWorld
/// Spawn a blood effect for \a ptr at \a worldPosition /// Spawn a blood effect for \a ptr at \a worldPosition
void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) override; void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) override;
void spawnEffect(const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos, void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride,
float scale = 1.f, bool isMagicVFX = true) override; const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true) override;
/// @see MWWorld::WeatherManager::isInStorm /// @see MWWorld::WeatherManager::isInStorm
bool isInStorm() const override; bool isInStorm() const override;

@ -3,6 +3,7 @@
#include <algorithm> #include <algorithm>
#include <cassert> #include <cassert>
#include <optional> #include <optional>
#include <stdexcept>
#include <components/debug/debuglog.hpp> #include <components/debug/debuglog.hpp>
#include <components/esm/defs.hpp> #include <components/esm/defs.hpp>
@ -339,6 +340,20 @@ namespace MWWorld
throw std::runtime_error(std::string("Can't find cell with name ") + std::string(name)); throw std::runtime_error(std::string("Can't find cell with name ") + std::string(name));
return *result; return *result;
} }
void WorldModel::registerPtr(const Ptr& ptr)
{
if (ptr.mRef == nullptr)
throw std::logic_error("Ptr with nullptr mRef is not allowed to be registered");
mPtrRegistry.insert(ptr);
ptr.mRef->mWorldModel = this;
}
void WorldModel::deregisterLiveCellRef(LiveCellRefBase& ref) noexcept
{
mPtrRegistry.remove(ref);
ref.mWorldModel = nullptr;
}
} }
MWWorld::Ptr MWWorld::WorldModel::getPtrByRefId(const ESM::RefId& name) MWWorld::Ptr MWWorld::WorldModel::getPtrByRefId(const ESM::RefId& name)

@ -77,9 +77,9 @@ namespace MWWorld
std::size_t getPtrRegistryRevision() const { return mPtrRegistry.getRevision(); } std::size_t getPtrRegistryRevision() const { return mPtrRegistry.getRevision(); }
void registerPtr(const Ptr& ptr) { mPtrRegistry.insert(ptr); } void registerPtr(const Ptr& ptr);
void deregisterLiveCellRef(const LiveCellRefBase& ref) noexcept { mPtrRegistry.remove(ref); } void deregisterLiveCellRef(LiveCellRefBase& ref) noexcept;
void assignSaveFileRefNum(ESM::CellRef& ref) { mPtrRegistry.assign(ref); } void assignSaveFileRefNum(ESM::CellRef& ref) { mPtrRegistry.assign(ref); }

@ -9,6 +9,7 @@ file(GLOB UNITTEST_SRC_FILES
mwworld/test_store.cpp mwworld/test_store.cpp
mwworld/testduration.cpp mwworld/testduration.cpp
mwworld/testtimestamp.cpp mwworld/testtimestamp.cpp
mwworld/testptr.cpp
mwdialogue/test_keywordsearch.cpp mwdialogue/test_keywordsearch.cpp

@ -1,11 +1,28 @@
#include <components/debug/debugging.hpp> #include <components/debug/debugging.hpp>
#include <components/misc/strings/conversion.hpp>
#include <components/settings/parser.hpp>
#include <components/settings/values.hpp>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <filesystem>
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
Log::sMinDebugLevel = Debug::getDebugLevel(); Log::sMinDebugLevel = Debug::getDebugLevel();
const std::filesystem::path settingsDefaultPath = std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "files"
/ Misc::StringUtils::stringToU8String("settings-default.cfg");
Settings::SettingsFileParser parser;
parser.loadSettingsFile(settingsDefaultPath, Settings::Manager::mDefaultSettings);
Settings::StaticValues::initDefaults();
Settings::Manager::mUserSettings = Settings::Manager::mDefaultSettings;
Settings::StaticValues::init();
testing::InitGoogleTest(&argc, argv); testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS(); return RUN_ALL_TESTS();
} }

@ -0,0 +1,82 @@
#include "apps/openmw/mwclass/npc.hpp"
#include "apps/openmw/mwworld/esmstore.hpp"
#include "apps/openmw/mwworld/livecellref.hpp"
#include "apps/openmw/mwworld/ptr.hpp"
#include "apps/openmw/mwworld/worldmodel.hpp"
#include <components/esm3/loadnpc.hpp>
#include <components/esm3/readerscache.hpp>
#include <gtest/gtest.h>
namespace MWWorld
{
namespace
{
using namespace testing;
TEST(MWWorldPtrTest, toStringShouldReturnHumanReadableTextRepresentationOfPtrWithNullRef)
{
Ptr ptr;
EXPECT_EQ(ptr.toString(), "null object");
}
TEST(MWWorldPtrTest, toStringShouldReturnHumanReadableTextRepresentationOfPtrWithDeletedRef)
{
MWClass::Npc::registerSelf();
ESM::NPC npc;
npc.blank();
npc.mId = ESM::RefId::stringRefId("Player");
ESMStore store;
store.insert(npc);
ESM::CellRef cellRef;
cellRef.blank();
cellRef.mRefID = npc.mId;
cellRef.mRefNum = ESM::RefNum{ .mIndex = 0x2a, .mContentFile = 0xd };
LiveCellRef<ESM::NPC> liveCellRef(cellRef, &npc);
liveCellRef.mData.setDeletedByContentFile(true);
Ptr ptr(&liveCellRef);
EXPECT_EQ(ptr.toString(), "deleted object0xd00002a (NPC, \"player\")");
}
TEST(MWWorldPtrTest, toStringShouldReturnHumanReadableTextRepresentationOfPtr)
{
MWClass::Npc::registerSelf();
ESM::NPC npc;
npc.blank();
npc.mId = ESM::RefId::stringRefId("Player");
ESMStore store;
store.insert(npc);
ESM::CellRef cellRef;
cellRef.blank();
cellRef.mRefID = npc.mId;
cellRef.mRefNum = ESM::RefNum{ .mIndex = 0x2a, .mContentFile = 0xd };
LiveCellRef<ESM::NPC> liveCellRef(cellRef, &npc);
Ptr ptr(&liveCellRef);
EXPECT_EQ(ptr.toString(), "object0xd00002a (NPC, \"player\")");
}
TEST(MWWorldPtrTest, underlyingLiveCellRefShouldBeDeregisteredOnDestruction)
{
MWClass::Npc::registerSelf();
ESM::NPC npc;
npc.blank();
npc.mId = ESM::RefId::stringRefId("Player");
ESMStore store;
store.insert(npc);
ESM::ReadersCache readersCache;
WorldModel worldModel(store, readersCache);
ESM::CellRef cellRef;
cellRef.blank();
cellRef.mRefID = npc.mId;
cellRef.mRefNum = ESM::FormId{ .mIndex = 0x2a, .mContentFile = 0xd };
{
LiveCellRef<ESM::NPC> liveCellRef(cellRef, &npc);
Ptr ptr(&liveCellRef);
worldModel.registerPtr(ptr);
ASSERT_EQ(worldModel.getPtr(cellRef.mRefNum), ptr);
}
EXPECT_EQ(worldModel.getPtr(cellRef.mRefNum), Ptr());
}
}
}

@ -239,7 +239,7 @@ namespace Debug
group->push(state); group->push(state);
lastAppliedStack.push_back(group); lastAppliedStack.push_back(group);
} }
if (!(lastAppliedStack.back() == this)) if (lastAppliedStack.empty() || !(lastAppliedStack.back() == this))
{ {
push(state); push(state);
lastAppliedStack.push_back(this); lastAppliedStack.push_back(this);

@ -56,7 +56,7 @@ void ESM::LuaScriptsCfg::load(ESMReader& esm)
{ {
mScripts.emplace_back(); mScripts.emplace_back();
ESM::LuaScriptCfg& script = mScripts.back(); ESM::LuaScriptCfg& script = mScripts.back();
script.mScriptPath = esm.getHString(); script.mScriptPath = VFS::Path::Normalized(esm.getHString());
esm.getSubNameIs("LUAF"); esm.getSubNameIs("LUAF");
esm.getSubHeader(); esm.getSubHeader();
@ -161,7 +161,7 @@ void ESM::LuaScripts::load(ESMReader& esm)
{ {
while (esm.isNextSub("LUAS")) while (esm.isNextSub("LUAS"))
{ {
std::string name = esm.getHString(); VFS::Path::Normalized name(esm.getHString());
std::string data = loadLuaBinaryData(esm); std::string data = loadLuaBinaryData(esm);
std::vector<LuaTimer> timers; std::vector<LuaTimer> timers;
while (esm.isNextSub("LUAT")) while (esm.isNextSub("LUAT"))

@ -287,4 +287,10 @@ namespace ESM
loadDataImpl<false>(esm, isDeleted, cellRef); loadDataImpl<false>(esm, isDeleted, cellRef);
} }
CellRef makeBlankCellRef()
{
CellRef result;
result.blank();
return result;
}
} }

@ -105,6 +105,8 @@ namespace ESM
}; };
void skipLoadCellRef(ESMReader& esm, bool wideRefNum = false); void skipLoadCellRef(ESMReader& esm, bool wideRefNum = false);
CellRef makeBlankCellRef();
} }
#endif #endif

@ -132,53 +132,61 @@ namespace ESM
esm.writeHNOString("DESC", mDescription); esm.writeHNOString("DESC", mDescription);
} }
short MagicEffect::getResistanceEffect(short effect) namespace
{ {
// Source https://wiki.openmw.org/index.php?title=Research:Magic#Effect_attribute std::map<short, short> makeEffectsMap()
// <Effect, Effect providing resistance against first effect>
static std::map<short, short> effects;
if (effects.empty())
{ {
effects[DisintegrateArmor] = Sanctuary; std::map<short, short> effects;
effects[DisintegrateWeapon] = Sanctuary;
for (int i = DrainAttribute; i <= DamageSkill; ++i) effects[MagicEffect::Effects::DisintegrateArmor] = MagicEffect::Effects::Sanctuary;
effects[i] = ResistMagicka; effects[MagicEffect::Effects::DisintegrateWeapon] = MagicEffect::Effects::Sanctuary;
for (int i = AbsorbAttribute; i <= AbsorbSkill; ++i)
effects[i] = ResistMagicka; for (int i = MagicEffect::Effects::DrainAttribute; i <= MagicEffect::Effects::DamageSkill; ++i)
for (int i = WeaknessToFire; i <= WeaknessToNormalWeapons; ++i) effects[i] = MagicEffect::Effects::ResistMagicka;
effects[i] = ResistMagicka; for (int i = MagicEffect::Effects::AbsorbAttribute; i <= MagicEffect::Effects::AbsorbSkill; ++i)
effects[i] = MagicEffect::Effects::ResistMagicka;
for (int i = MagicEffect::Effects::WeaknessToFire; i <= MagicEffect::Effects::WeaknessToNormalWeapons; ++i)
effects[i] = MagicEffect::Effects::ResistMagicka;
effects[Burden] = ResistMagicka; effects[MagicEffect::Effects::Burden] = MagicEffect::Effects::ResistMagicka;
effects[Charm] = ResistMagicka; effects[MagicEffect::Effects::Charm] = MagicEffect::Effects::ResistMagicka;
effects[Silence] = ResistMagicka; effects[MagicEffect::Effects::Silence] = MagicEffect::Effects::ResistMagicka;
effects[Blind] = ResistMagicka; effects[MagicEffect::Effects::Blind] = MagicEffect::Effects::ResistMagicka;
effects[Sound] = ResistMagicka; effects[MagicEffect::Effects::Sound] = MagicEffect::Effects::ResistMagicka;
for (int i = 0; i < 2; ++i) for (int i = 0; i < 2; ++i)
{ {
effects[CalmHumanoid + i] = ResistMagicka; effects[MagicEffect::Effects::CalmHumanoid + i] = MagicEffect::Effects::ResistMagicka;
effects[FrenzyHumanoid + i] = ResistMagicka; effects[MagicEffect::Effects::FrenzyHumanoid + i] = MagicEffect::Effects::ResistMagicka;
effects[DemoralizeHumanoid + i] = ResistMagicka; effects[MagicEffect::Effects::DemoralizeHumanoid + i] = MagicEffect::Effects::ResistMagicka;
effects[RallyHumanoid + i] = ResistMagicka; effects[MagicEffect::Effects::RallyHumanoid + i] = MagicEffect::Effects::ResistMagicka;
} }
effects[TurnUndead] = ResistMagicka; effects[MagicEffect::Effects::TurnUndead] = MagicEffect::Effects::ResistMagicka;
effects[MagicEffect::Effects::FireDamage] = MagicEffect::Effects::ResistFire;
effects[MagicEffect::Effects::FrostDamage] = MagicEffect::Effects::ResistFrost;
effects[MagicEffect::Effects::ShockDamage] = MagicEffect::Effects::ResistShock;
effects[MagicEffect::Effects::Vampirism] = MagicEffect::Effects::ResistCommonDisease;
effects[MagicEffect::Effects::Corprus] = MagicEffect::Effects::ResistCorprusDisease;
effects[MagicEffect::Effects::Poison] = MagicEffect::Effects::ResistPoison;
effects[MagicEffect::Effects::Paralyze] = MagicEffect::Effects::ResistParalysis;
effects[FireDamage] = ResistFire; return effects;
effects[FrostDamage] = ResistFrost;
effects[ShockDamage] = ResistShock;
effects[Vampirism] = ResistCommonDisease;
effects[Corprus] = ResistCorprusDisease;
effects[Poison] = ResistPoison;
effects[Paralyze] = ResistParalysis;
} }
}
if (effects.find(effect) != effects.end()) short MagicEffect::getResistanceEffect(short effect)
return effects[effect]; {
else // Source https://wiki.openmw.org/index.php?title=Research:Magic#Effect_attribute
return -1;
// <Effect, Effect providing resistance against first effect>
static const std::map<short, short> effects = makeEffectsMap();
if (const auto it = effects.find(effect); it != effects.end())
return it->second;
return -1;
} }
short MagicEffect::getWeaknessEffect(short effect) short MagicEffect::getWeaknessEffect(short effect)

@ -320,16 +320,18 @@ namespace ESM4
std::filesystem::path path = strings / (prefix + language + suffix); std::filesystem::path path = strings / (prefix + language + suffix);
if (mVFS != nullptr) if (mVFS != nullptr)
{ {
std::string vfsPath = Files::pathToUnicodeString(path); VFS::Path::Normalized vfsPath(Files::pathToUnicodeString(path));
if (!mVFS->exists(vfsPath)) Files::IStreamPtr stream = mVFS->find(vfsPath);
if (stream == nullptr)
{ {
path = strings / (prefix + altLanguage + suffix); path = strings / (prefix + altLanguage + suffix);
vfsPath = Files::pathToUnicodeString(path); vfsPath = VFS::Path::Normalized(Files::pathToUnicodeString(path));
stream = mVFS->find(vfsPath);
} }
if (mVFS->exists(vfsPath)) if (stream != nullptr)
{ {
const Files::IStreamPtr stream = mVFS->get(vfsPath);
buildLStringIndex(stringType, *stream); buildLStringIndex(stringType, *stream);
return; return;
} }

@ -113,6 +113,7 @@ namespace LuaUi
ContentView content(LuaUtil::cast<sol::table>(contentObj)); ContentView content(LuaUtil::cast<sol::table>(contentObj));
result.resize(content.size()); result.resize(content.size());
size_t minSize = std::min(children.size(), content.size()); size_t minSize = std::min(children.size(), content.size());
std::vector<WidgetExtension*> toDestroy;
for (size_t i = 0; i < minSize; i++) for (size_t i = 0; i < minSize; i++)
{ {
WidgetExtension* ext = children[i]; WidgetExtension* ext = children[i];
@ -121,7 +122,7 @@ namespace LuaUi
{ {
WidgetExtension* root = pluckElementRoot(child, depth); WidgetExtension* root = pluckElementRoot(child, depth);
if (ext != root) if (ext != root)
destroyChild(ext); toDestroy.emplace_back(ext);
result[i] = root; result[i] = root;
} }
else else
@ -133,14 +134,12 @@ namespace LuaUi
} }
else else
{ {
destroyChild(ext); toDestroy.emplace_back(ext);
ext = createWidget(newLayout, false, depth); ext = createWidget(newLayout, false, depth);
} }
result[i] = ext; result[i] = ext;
} }
} }
for (size_t i = minSize; i < children.size(); i++)
destroyChild(children[i]);
for (size_t i = minSize; i < content.size(); i++) for (size_t i = minSize; i < content.size(); i++)
{ {
sol::object child = content.at(i); sol::object child = content.at(i);
@ -149,6 +148,11 @@ namespace LuaUi
else else
result[i] = createWidget(child.as<sol::table>(), false, depth); result[i] = createWidget(child.as<sol::table>(), false, depth);
} }
// Don't destroy anything until element creation has had a chance to throw
for (size_t i = minSize; i < children.size(); i++)
destroyChild(children[i]);
for (WidgetExtension* ext : toDestroy)
destroyChild(ext);
return result; return result;
} }
@ -217,7 +221,9 @@ namespace LuaUi
std::string setLayer(WidgetExtension* ext, const sol::table& layout) std::string setLayer(WidgetExtension* ext, const sol::table& layout)
{ {
MyGUI::ILayer* layerNode = ext->widget()->getLayer(); MyGUI::ILayer* layerNode = ext->widget()->getLayer();
std::string currentLayer = layerNode ? layerNode->getName() : std::string(); std::string_view currentLayer;
if (layerNode)
currentLayer = layerNode->getName();
std::string newLayer = layout.get_or(LayoutKeys::layer, std::string()); std::string newLayer = layout.get_or(LayoutKeys::layer, std::string());
if (!newLayer.empty() && !MyGUI::LayerManager::getInstance().isExist(newLayer)) if (!newLayer.empty() && !MyGUI::LayerManager::getInstance().isExist(newLayer))
throw std::logic_error(std::string("Layer ") + newLayer + " doesn't exist"); throw std::logic_error(std::string("Layer ") + newLayer + " doesn't exist");
@ -278,9 +284,20 @@ namespace LuaUi
WidgetExtension* parent = mRoot->getParent(); WidgetExtension* parent = mRoot->getParent();
auto children = parent->children(); auto children = parent->children();
auto it = std::find(children.begin(), children.end(), mRoot); auto it = std::find(children.begin(), children.end(), mRoot);
mRoot = createWidget(layout(), true, 0);
assert(it != children.end()); assert(it != children.end());
*it = mRoot; try
{
mRoot = createWidget(layout(), true, 0);
*it = mRoot;
}
catch (...)
{
// Remove mRoot from its parent's children even if we couldn't replace it
children.erase(it);
parent->setChildren(children);
mRoot = nullptr;
throw;
}
parent->setChildren(children); parent->setChildren(children);
mRoot->updateCoord(); mRoot->updateCoord();
} }
@ -300,6 +317,10 @@ namespace LuaUi
{ {
if (mRoot != nullptr) if (mRoot != nullptr)
{ {
// If someone decided to destroy an element used as another element's content, we need to detach it
// first so the parent doesn't end up holding a stale pointer
if (WidgetExtension* parent = mRoot->getParent())
parent->detachChildrenIf([&](WidgetExtension* child) { return child == mRoot; });
destroyRoot(mRoot); destroyRoot(mRoot);
mRoot = nullptr; mRoot = nullptr;
} }

@ -126,6 +126,7 @@ namespace LuaUi
{ {
mParent = nullptr; mParent = nullptr;
widget()->detachFromWidget(); widget()->detachFromWidget();
widget()->detachFromLayer();
} }
WidgetExtension* WidgetExtension::findDeep(std::string_view flagName) WidgetExtension* WidgetExtension::findDeep(std::string_view flagName)

@ -179,7 +179,7 @@ namespace LuaUi
void updateVisible(); void updateVisible();
void detachChildrenIf(auto&& predicate, std::vector<WidgetExtension*> children) void detachChildrenIf(auto&& predicate, std::vector<WidgetExtension*>& children)
{ {
for (auto it = children.begin(); it != children.end();) for (auto it = children.begin(); it != children.end();)
{ {

@ -228,7 +228,7 @@ bool Misc::ResourceHelpers::isHiddenMarker(const ESM::RefId& id)
namespace namespace
{ {
std::string getLODMeshNameImpl(std::string resPath, const VFS::Manager* vfs, std::string_view pattern) std::string getLODMeshNameImpl(std::string resPath, std::string_view pattern)
{ {
if (auto w = Misc::findExtension(resPath); w != std::string::npos) if (auto w = Misc::findExtension(resPath); w != std::string::npos)
resPath.insert(w, pattern); resPath.insert(w, pattern);
@ -237,7 +237,7 @@ namespace
std::string getBestLODMeshName(std::string const& resPath, const VFS::Manager* vfs, std::string_view pattern) std::string getBestLODMeshName(std::string const& resPath, const VFS::Manager* vfs, std::string_view pattern)
{ {
if (const auto& result = getLODMeshNameImpl(resPath, vfs, pattern); vfs->exists(result)) if (std::string result = getLODMeshNameImpl(resPath, pattern); vfs->exists(result))
return result; return result;
return resPath; return resPath;
} }

@ -95,7 +95,7 @@ namespace osgMyGUI
if (!mImageManager) if (!mImageManager)
throw std::runtime_error("No imagemanager set"); throw std::runtime_error("No imagemanager set");
osg::ref_ptr<osg::Image> image(mImageManager->getImage(VFS::Path::toNormalized(fname))); osg::ref_ptr<osg::Image> image(mImageManager->getImage(VFS::Path::Normalized(fname)));
mTexture = new osg::Texture2D(image); mTexture = new osg::Texture2D(image);
mTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE);
mTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE);

@ -53,7 +53,7 @@ namespace NifBullet
mShape->mFileName = nif.getFilename(); mShape->mFileName = nif.getFilename();
if (roots.empty()) if (roots.empty())
{ {
warn("Found no root nodes in NIF file " + mShape->mFileName); warn("Found no root nodes in NIF file " + mShape->mFileName.value());
return mShape; return mShape;
} }
@ -93,7 +93,7 @@ namespace NifBullet
} }
else else
{ {
warn("Invalid Bounding Box node bounds in file " + mShape->mFileName); warn("Invalid Bounding Box node bounds in file " + mShape->mFileName.value());
} }
return true; return true;
} }

@ -1,7 +1,6 @@
#ifndef OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H #ifndef OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H
#define OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H #define OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H
#include <array>
#include <map> #include <map>
#include <memory> #include <memory>
@ -12,6 +11,8 @@
#include <BulletCollision/CollisionShapes/btBvhTriangleMeshShape.h> #include <BulletCollision/CollisionShapes/btBvhTriangleMeshShape.h>
#include <BulletCollision/CollisionShapes/btScaledBvhTriangleMeshShape.h> #include <BulletCollision/CollisionShapes/btScaledBvhTriangleMeshShape.h>
#include <components/vfs/pathutil.hpp>
class btCollisionShape; class btCollisionShape;
namespace NifBullet namespace NifBullet
@ -56,7 +57,7 @@ namespace Resource
// we store the node's record index mapped to the child index of the shape in the btCompoundShape. // we store the node's record index mapped to the child index of the shape in the btCompoundShape.
std::map<int, int> mAnimatedShapes; std::map<int, int> mAnimatedShapes;
std::string mFileName; VFS::Path::Normalized mFileName;
std::string mFileHash; std::string mFileHash;
VisualCollisionType mVisualCollisionType = VisualCollisionType::None; VisualCollisionType mVisualCollisionType = VisualCollisionType::None;

@ -106,93 +106,83 @@ namespace Resource
{ {
} }
BulletShapeManager::~BulletShapeManager() {} BulletShapeManager::~BulletShapeManager() = default;
osg::ref_ptr<const BulletShape> BulletShapeManager::getShape(const std::string& name) osg::ref_ptr<const BulletShape> BulletShapeManager::getShape(VFS::Path::NormalizedView name)
{ {
const VFS::Path::Normalized normalized(name); if (osg::ref_ptr<osg::Object> obj = mCache->getRefFromObjectCache(name))
return osg::ref_ptr<BulletShape>(static_cast<BulletShape*>(obj.get()));
osg::ref_ptr<BulletShape> shape; osg::ref_ptr<BulletShape> shape;
osg::ref_ptr<osg::Object> obj = mCache->getRefFromObjectCache(normalized);
if (obj) if (Misc::getFileExtension(name.value()) == "nif")
shape = osg::ref_ptr<BulletShape>(static_cast<BulletShape*>(obj.get())); {
NifBullet::BulletNifLoader loader;
shape = loader.load(*mNifFileManager->get(name));
}
else else
{ {
if (Misc::getFileExtension(normalized) == "nif") // TODO: support .bullet shape files
osg::ref_ptr<const osg::Node> constNode(mSceneManager->getTemplate(name));
// const-trickery required because there is no const version of NodeVisitor
osg::ref_ptr<osg::Node> node(const_cast<osg::Node*>(constNode.get()));
// Check first if there's a custom collision node
unsigned int visitAllNodesMask = 0xffffffff;
SceneUtil::FindByNameVisitor nameFinder("Collision");
nameFinder.setTraversalMask(visitAllNodesMask);
nameFinder.setNodeMaskOverride(visitAllNodesMask);
node->accept(nameFinder);
if (nameFinder.mFoundNode)
{ {
NifBullet::BulletNifLoader loader; NodeToShapeVisitor visitor;
shape = loader.load(*mNifFileManager->get(normalized)); visitor.setTraversalMask(visitAllNodesMask);
visitor.setNodeMaskOverride(visitAllNodesMask);
nameFinder.mFoundNode->accept(visitor);
shape = visitor.getShape();
} }
else
// Generate a collision shape from the mesh
if (!shape)
{ {
// TODO: support .bullet shape files NodeToShapeVisitor visitor;
node->accept(visitor);
osg::ref_ptr<const osg::Node> constNode(mSceneManager->getTemplate(normalized)); shape = visitor.getShape();
osg::ref_ptr<osg::Node> node(const_cast<osg::Node*>(
constNode.get())); // const-trickery required because there is no const version of NodeVisitor
// Check first if there's a custom collision node
unsigned int visitAllNodesMask = 0xffffffff;
SceneUtil::FindByNameVisitor nameFinder("Collision");
nameFinder.setTraversalMask(visitAllNodesMask);
nameFinder.setNodeMaskOverride(visitAllNodesMask);
node->accept(nameFinder);
if (nameFinder.mFoundNode)
{
NodeToShapeVisitor visitor;
visitor.setTraversalMask(visitAllNodesMask);
visitor.setNodeMaskOverride(visitAllNodesMask);
nameFinder.mFoundNode->accept(visitor);
shape = visitor.getShape();
}
// Generate a collision shape from the mesh
if (!shape) if (!shape)
{ return osg::ref_ptr<BulletShape>();
NodeToShapeVisitor visitor;
node->accept(visitor);
shape = visitor.getShape();
if (!shape)
return osg::ref_ptr<BulletShape>();
}
if (shape != nullptr)
{
shape->mFileName = normalized;
constNode->getUserValue(Misc::OsgUserValues::sFileHash, shape->mFileHash);
}
} }
mCache->addEntryToObjectCache(normalized, shape); if (shape != nullptr)
{
shape->mFileName = name;
constNode->getUserValue(Misc::OsgUserValues::sFileHash, shape->mFileHash);
}
} }
mCache->addEntryToObjectCache(name.value(), shape);
return shape; return shape;
} }
osg::ref_ptr<BulletShapeInstance> BulletShapeManager::cacheInstance(const std::string& name) osg::ref_ptr<BulletShapeInstance> BulletShapeManager::cacheInstance(VFS::Path::NormalizedView name)
{ {
const std::string normalized = VFS::Path::normalizeFilename(name); osg::ref_ptr<BulletShapeInstance> instance = createInstance(name);
if (instance != nullptr)
osg::ref_ptr<BulletShapeInstance> instance = createInstance(normalized); mInstanceCache->addEntryToObjectCache(name, instance.get());
if (instance)
mInstanceCache->addEntryToObjectCache(normalized, instance.get());
return instance; return instance;
} }
osg::ref_ptr<BulletShapeInstance> BulletShapeManager::getInstance(const std::string& name) osg::ref_ptr<BulletShapeInstance> BulletShapeManager::getInstance(VFS::Path::NormalizedView name)
{ {
const std::string normalized = VFS::Path::normalizeFilename(name); if (osg::ref_ptr<osg::Object> obj = mInstanceCache->takeFromObjectCache(name))
osg::ref_ptr<osg::Object> obj = mInstanceCache->takeFromObjectCache(normalized);
if (obj.get())
return static_cast<BulletShapeInstance*>(obj.get()); return static_cast<BulletShapeInstance*>(obj.get());
else return createInstance(name);
return createInstance(normalized);
} }
osg::ref_ptr<BulletShapeInstance> BulletShapeManager::createInstance(const std::string& name) osg::ref_ptr<BulletShapeInstance> BulletShapeManager::createInstance(VFS::Path::NormalizedView name)
{ {
osg::ref_ptr<const BulletShape> shape = getShape(name); if (osg::ref_ptr<const BulletShape> shape = getShape(name))
if (shape)
return makeInstance(std::move(shape)); return makeInstance(std::move(shape));
return osg::ref_ptr<BulletShapeInstance>(); return osg::ref_ptr<BulletShapeInstance>();
} }

@ -1,11 +1,10 @@
#ifndef OPENMW_COMPONENTS_BULLETSHAPEMANAGER_H #ifndef OPENMW_COMPONENTS_BULLETSHAPEMANAGER_H
#define OPENMW_COMPONENTS_BULLETSHAPEMANAGER_H #define OPENMW_COMPONENTS_BULLETSHAPEMANAGER_H
#include <map>
#include <string>
#include <osg/ref_ptr> #include <osg/ref_ptr>
#include <components/vfs/pathutil.hpp>
#include "bulletshape.hpp" #include "bulletshape.hpp"
#include "resourcemanager.hpp" #include "resourcemanager.hpp"
@ -30,16 +29,16 @@ namespace Resource
~BulletShapeManager(); ~BulletShapeManager();
/// @note May return a null pointer if the object has no shape. /// @note May return a null pointer if the object has no shape.
osg::ref_ptr<const BulletShape> getShape(const std::string& name); osg::ref_ptr<const BulletShape> getShape(VFS::Path::NormalizedView name);
/// Create an instance of the given shape and cache it for later use, so that future calls to getInstance() can /// Create an instance of the given shape and cache it for later use, so that future calls to getInstance() can
/// simply return the cached instance instead of having to create a new one. /// simply return the cached instance instead of having to create a new one.
/// @note The returned ref_ptr may be kept by the caller to ensure that the instance stays in cache for as long /// @note The returned ref_ptr may be kept by the caller to ensure that the instance stays in cache for as long
/// as needed. /// as needed.
osg::ref_ptr<BulletShapeInstance> cacheInstance(const std::string& name); osg::ref_ptr<BulletShapeInstance> cacheInstance(VFS::Path::NormalizedView name);
/// @note May return a null pointer if the object has no shape. /// @note May return a null pointer if the object has no shape.
osg::ref_ptr<BulletShapeInstance> getInstance(const std::string& name); osg::ref_ptr<BulletShapeInstance> getInstance(VFS::Path::NormalizedView name);
/// @see ResourceManager::updateCache /// @see ResourceManager::updateCache
void updateCache(double referenceTime) override; void updateCache(double referenceTime) override;
@ -49,7 +48,7 @@ namespace Resource
void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; void reportStats(unsigned int frameNumber, osg::Stats* stats) const override;
private: private:
osg::ref_ptr<BulletShapeInstance> createInstance(const std::string& name); osg::ref_ptr<BulletShapeInstance> createInstance(VFS::Path::NormalizedView name);
osg::ref_ptr<MultiObjectCache> mInstanceCache; osg::ref_ptr<MultiObjectCache> mInstanceCache;
SceneManager* mSceneManager; SceneManager* mSceneManager;

@ -16,10 +16,6 @@
#include <osg/ref_ptr> #include <osg/ref_ptr>
#include <algorithm> #include <algorithm>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility> #include <utility>
#include <vector> #include <vector>
@ -97,7 +93,7 @@ namespace Resource
for (CellRef& cellRef : cellRefs) for (CellRef& cellRef : cellRefs)
{ {
std::string model(getModel(esmData, cellRef.mRefId, cellRef.mType)); VFS::Path::Normalized model(getModel(esmData, cellRef.mRefId, cellRef.mType));
if (model.empty()) if (model.empty())
continue; continue;
@ -107,7 +103,8 @@ namespace Resource
osg::ref_ptr<const Resource::BulletShape> shape = [&] { osg::ref_ptr<const Resource::BulletShape> shape = [&] {
try try
{ {
return bulletShapeManager.getShape("meshes/" + model); constexpr VFS::Path::NormalizedView prefix("meshes");
return bulletShapeManager.getShape(prefix / model);
} }
catch (const std::exception& e) catch (const std::exception& e)
{ {

@ -6,11 +6,6 @@
namespace Resource namespace Resource
{ {
MultiObjectCache::MultiObjectCache() {}
MultiObjectCache::~MultiObjectCache() {}
void MultiObjectCache::removeUnreferencedObjectsInCache() void MultiObjectCache::removeUnreferencedObjectsInCache()
{ {
std::vector<osg::ref_ptr<osg::Object>> objectsToRemove; std::vector<osg::ref_ptr<osg::Object>> objectsToRemove;
@ -44,7 +39,7 @@ namespace Resource
_objectCache.clear(); _objectCache.clear();
} }
void MultiObjectCache::addEntryToObjectCache(const std::string& filename, osg::Object* object) void MultiObjectCache::addEntryToObjectCache(VFS::Path::NormalizedView filename, osg::Object* object)
{ {
if (!object) if (!object)
{ {
@ -52,23 +47,23 @@ namespace Resource
return; return;
} }
std::lock_guard<std::mutex> lock(_objectCacheMutex); std::lock_guard<std::mutex> lock(_objectCacheMutex);
_objectCache.insert(std::make_pair(filename, object)); _objectCache.emplace(filename, object);
} }
osg::ref_ptr<osg::Object> MultiObjectCache::takeFromObjectCache(const std::string& fileName) osg::ref_ptr<osg::Object> MultiObjectCache::takeFromObjectCache(VFS::Path::NormalizedView fileName)
{ {
std::lock_guard<std::mutex> lock(_objectCacheMutex); std::lock_guard<std::mutex> lock(_objectCacheMutex);
++mGet; ++mGet;
ObjectCacheMap::iterator found = _objectCache.find(fileName); const auto it = _objectCache.find(fileName);
if (found == _objectCache.end()) if (it != _objectCache.end())
return osg::ref_ptr<osg::Object>();
else
{ {
osg::ref_ptr<osg::Object> object = std::move(found->second); osg::ref_ptr<osg::Object> object = std::move(it->second);
_objectCache.erase(found); _objectCache.erase(it);
++mHit; ++mHit;
return object; return object;
} }
return nullptr;
} }
void MultiObjectCache::releaseGLObjects(osg::State* state) void MultiObjectCache::releaseGLObjects(osg::State* state)

@ -3,11 +3,12 @@
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <string>
#include <osg/Referenced> #include <osg/Referenced>
#include <osg/ref_ptr> #include <osg/ref_ptr>
#include <components/vfs/pathutil.hpp>
#include "cachestats.hpp" #include "cachestats.hpp"
namespace osg namespace osg
@ -23,18 +24,15 @@ namespace Resource
class MultiObjectCache : public osg::Referenced class MultiObjectCache : public osg::Referenced
{ {
public: public:
MultiObjectCache();
~MultiObjectCache();
void removeUnreferencedObjectsInCache(); void removeUnreferencedObjectsInCache();
/** Remove all objects from the cache. */ /** Remove all objects from the cache. */
void clear(); void clear();
void addEntryToObjectCache(const std::string& filename, osg::Object* object); void addEntryToObjectCache(VFS::Path::NormalizedView filename, osg::Object* object);
/** Take an Object from cache. Return nullptr if no object found. */ /** Take an Object from cache. Return nullptr if no object found. */
osg::ref_ptr<osg::Object> takeFromObjectCache(const std::string& fileName); osg::ref_ptr<osg::Object> takeFromObjectCache(VFS::Path::NormalizedView fileName);
/** call releaseGLObjects on all objects attached to the object cache.*/ /** call releaseGLObjects on all objects attached to the object cache.*/
void releaseGLObjects(osg::State* state); void releaseGLObjects(osg::State* state);
@ -42,7 +40,7 @@ namespace Resource
CacheStats getStats() const; CacheStats getStats() const;
protected: protected:
typedef std::multimap<std::string, osg::ref_ptr<osg::Object>> ObjectCacheMap; typedef std::multimap<VFS::Path::Normalized, osg::ref_ptr<osg::Object>, std::less<>> ObjectCacheMap;
ObjectCacheMap _objectCache; ObjectCacheMap _objectCache;
mutable std::mutex _objectCacheMutex; mutable std::mutex _objectCacheMutex;

@ -597,9 +597,9 @@ namespace Resource
mShaderManager->setShaderPath(path); mShaderManager->setShaderPath(path);
} }
bool SceneManager::checkLoaded(const std::string& name, double timeStamp) bool SceneManager::checkLoaded(VFS::Path::NormalizedView name, double timeStamp)
{ {
return mCache->checkInObjectCache(VFS::Path::normalizeFilename(name), timeStamp); return mCache->checkInObjectCache(name, timeStamp);
} }
void SceneManager::setUpNormalsRTForStateSet(osg::StateSet* stateset, bool enabled) void SceneManager::setUpNormalsRTForStateSet(osg::StateSet* stateset, bool enabled)

@ -152,7 +152,7 @@ namespace Resource
void setShaderPath(const std::filesystem::path& path); void setShaderPath(const std::filesystem::path& path);
/// Check if a given scene is loaded and if so, update its usage timestamp to prevent it from being unloaded /// Check if a given scene is loaded and if so, update its usage timestamp to prevent it from being unloaded
bool checkLoaded(const std::string& name, double referenceTime); bool checkLoaded(VFS::Path::NormalizedView name, double referenceTime);
/// Get a read-only copy of this scene "template" /// Get a read-only copy of this scene "template"
/// @note If the given filename does not exist or fails to load, an error marker mesh will be used instead. /// @note If the given filename does not exist or fails to load, an error marker mesh will be used instead.

@ -82,18 +82,32 @@ namespace SceneUtil
std::string_view mFilter; std::string_view mFilter;
}; };
void mergeUserData(const osg::UserDataContainer* source, osg::Object* target) namespace
{ {
if (!source)
return;
if (!target->getUserDataContainer()) void mergeUserData(const osg::UserDataContainer* source, osg::Object* target)
target->setUserDataContainer(osg::clone(source, osg::CopyOp::SHALLOW_COPY));
else
{ {
for (unsigned int i = 0; i < source->getNumUserObjects(); ++i) if (!source)
target->getUserDataContainer()->addUserObject( return;
osg::clone(source->getUserObject(i), osg::CopyOp::SHALLOW_COPY));
if (!target->getUserDataContainer())
target->setUserDataContainer(osg::clone(source, osg::CopyOp::SHALLOW_COPY));
else
{
for (unsigned int i = 0; i < source->getNumUserObjects(); ++i)
target->getUserDataContainer()->addUserObject(
osg::clone(source->getUserObject(i), osg::CopyOp::SHALLOW_COPY));
}
}
osg::ref_ptr<osg::StateSet> makeFrontFaceStateSet()
{
osg::ref_ptr<osg::FrontFace> frontFace = new osg::FrontFace;
frontFace->setMode(osg::FrontFace::CLOCKWISE);
osg::ref_ptr<osg::StateSet> frontFaceStateSet = new osg::StateSet;
frontFaceStateSet->setAttributeAndModes(frontFace, osg::StateAttribute::ON);
return frontFaceStateSet;
} }
} }
@ -159,14 +173,8 @@ namespace SceneUtil
// Note: for absolute correctness we would need to check the current front face for every mesh then // Note: for absolute correctness we would need to check the current front face for every mesh then
// invert it However MW isn't doing this either, so don't. Assuming all meshes are using backface // invert it However MW isn't doing this either, so don't. Assuming all meshes are using backface
// culling is more efficient. // culling is more efficient.
static osg::ref_ptr<osg::StateSet> frontFaceStateSet; static const osg::ref_ptr<osg::StateSet> frontFaceStateSet = makeFrontFaceStateSet();
if (!frontFaceStateSet)
{
frontFaceStateSet = new osg::StateSet;
osg::FrontFace* frontFace = new osg::FrontFace;
frontFace->setMode(osg::FrontFace::CLOCKWISE);
frontFaceStateSet->setAttributeAndModes(frontFace, osg::StateAttribute::ON);
}
trans->setStateSet(frontFaceStateSet); trans->setStateSet(frontFaceStateSet);
} }

@ -33,7 +33,11 @@ namespace Settings
SettingValue<bool> mKeyboardNavigation{ mIndex, "GUI", "keyboard navigation" }; SettingValue<bool> mKeyboardNavigation{ mIndex, "GUI", "keyboard navigation" };
SettingValue<bool> mColorTopicEnable{ mIndex, "GUI", "color topic enable" }; SettingValue<bool> mColorTopicEnable{ mIndex, "GUI", "color topic enable" };
SettingValue<MyGUI::Colour> mColorTopicSpecific{ mIndex, "GUI", "color topic specific" }; SettingValue<MyGUI::Colour> mColorTopicSpecific{ mIndex, "GUI", "color topic specific" };
SettingValue<MyGUI::Colour> mColorTopicSpecificOver{ mIndex, "GUI", "color topic specific over" };
SettingValue<MyGUI::Colour> mColorTopicSpecificPressed{ mIndex, "GUI", "color topic specific pressed" };
SettingValue<MyGUI::Colour> mColorTopicExhausted{ mIndex, "GUI", "color topic exhausted" }; SettingValue<MyGUI::Colour> mColorTopicExhausted{ mIndex, "GUI", "color topic exhausted" };
SettingValue<MyGUI::Colour> mColorTopicExhaustedOver{ mIndex, "GUI", "color topic exhausted over" };
SettingValue<MyGUI::Colour> mColorTopicExhaustedPressed{ mIndex, "GUI", "color topic exhausted pressed" };
}; };
} }

@ -1,7 +1,7 @@
#ifndef COMPONENTS_TERRAIN_DEFS_HPP #ifndef COMPONENTS_TERRAIN_DEFS_HPP
#define COMPONENTS_TERRAIN_DEFS_HPP #define COMPONENTS_TERRAIN_DEFS_HPP
#include <string> #include <components/vfs/pathutil.hpp>
namespace Terrain namespace Terrain
{ {
@ -16,8 +16,8 @@ namespace Terrain
struct LayerInfo struct LayerInfo
{ {
std::string mDiffuseMap; VFS::Path::Normalized mDiffuseMap;
std::string mNormalMap; VFS::Path::Normalized mNormalMap;
bool mParallax; // Height info in normal map alpha channel? bool mParallax; // Height info in normal map alpha channel?
bool mSpecular; // Specular info in diffuse map alpha channel? bool mSpecular; // Specular info in diffuse map alpha channel?

@ -404,63 +404,68 @@ namespace Terrain
} }
} }
void updateWaterCullingView( namespace
HeightCullCallback* callback, ViewData* vd, osgUtil::CullVisitor* cv, float cellworldsize, bool outofworld)
{ {
if (!(cv->getTraversalMask() & callback->getCullMask())) osg::ref_ptr<osg::StateSet> makeStateSet()
return;
float lowZ = std::numeric_limits<float>::max();
float highZ = callback->getHighZ();
if (cv->getEyePoint().z() <= highZ || outofworld)
{ {
callback->setLowZ(-std::numeric_limits<float>::max()); osg::ref_ptr<osg::StateSet> stateSet = new osg::StateSet;
return; stateSet->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
stateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF);
stateSet->setAttributeAndModes(
new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE),
osg::StateAttribute::ON);
osg::ref_ptr<osg::Material> material = new osg::Material;
material->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 1, 1));
material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 1));
material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 1));
stateSet->setAttributeAndModes(material, osg::StateAttribute::ON);
stateSet->setRenderBinDetails(100, "RenderBin");
return stateSet;
} }
cv->pushCurrentMask();
static bool debug = getenv("OPENMW_WATER_CULLING_DEBUG") != nullptr; void updateWaterCullingView(
for (unsigned int i = 0; i < vd->getNumEntries(); ++i) HeightCullCallback* callback, ViewData* vd, osgUtil::CullVisitor* cv, float cellworldsize, bool outofworld)
{ {
ViewDataEntry& entry = vd->getEntry(i); if (!(cv->getTraversalMask() & callback->getCullMask()))
osg::BoundingBox bb return;
= static_cast<TerrainDrawable*>(entry.mRenderingNode->asGroup()->getChild(0))->getWaterBoundingBox(); float lowZ = std::numeric_limits<float>::max();
if (!bb.valid()) float highZ = callback->getHighZ();
continue; if (cv->getEyePoint().z() <= highZ || outofworld)
osg::Vec3f ofs(
entry.mNode->getCenter().x() * cellworldsize, entry.mNode->getCenter().y() * cellworldsize, 0.f);
bb._min += ofs;
bb._max += ofs;
bb._min.z() = highZ;
bb._max.z() = highZ;
if (cv->isCulled(bb))
continue;
lowZ = bb._min.z();
if (!debug)
break;
osg::Box* b = new osg::Box;
b->set(bb.center(), bb._max - bb.center());
osg::ShapeDrawable* drw = new osg::ShapeDrawable(b);
static osg::ref_ptr<osg::StateSet> stateset = nullptr;
if (!stateset)
{ {
stateset = new osg::StateSet; callback->setLowZ(-std::numeric_limits<float>::max());
stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); return;
stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); }
stateset->setAttributeAndModes( cv->pushCurrentMask();
new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE), static bool debug = getenv("OPENMW_WATER_CULLING_DEBUG") != nullptr;
osg::StateAttribute::ON); for (unsigned int i = 0; i < vd->getNumEntries(); ++i)
osg::Material* m = new osg::Material; {
m->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 1, 1)); ViewDataEntry& entry = vd->getEntry(i);
m->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 1)); osg::BoundingBox bb = static_cast<TerrainDrawable*>(entry.mRenderingNode->asGroup()->getChild(0))
m->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 1)); ->getWaterBoundingBox();
stateset->setAttributeAndModes(m, osg::StateAttribute::ON); if (!bb.valid())
stateset->setRenderBinDetails(100, "RenderBin"); continue;
osg::Vec3f ofs(
entry.mNode->getCenter().x() * cellworldsize, entry.mNode->getCenter().y() * cellworldsize, 0.f);
bb._min += ofs;
bb._max += ofs;
bb._min.z() = highZ;
bb._max.z() = highZ;
if (cv->isCulled(bb))
continue;
lowZ = bb._min.z();
if (!debug)
break;
osg::Box* b = new osg::Box;
b->set(bb.center(), bb._max - bb.center());
osg::ShapeDrawable* drw = new osg::ShapeDrawable(b);
static const osg::ref_ptr<osg::StateSet> stateset = makeStateSet();
drw->setStateSet(stateset);
drw->accept(*cv);
} }
drw->setStateSet(stateset); callback->setLowZ(lowZ);
drw->accept(*cv); cv->popCurrentMask();
} }
callback->setLowZ(lowZ);
cv->popCurrentMask();
} }
void QuadTreeWorld::accept(osg::NodeVisitor& nv) void QuadTreeWorld::accept(osg::NodeVisitor& nv)

@ -35,23 +35,21 @@ namespace Terrain
mCache->call(f); mCache->call(f);
} }
osg::ref_ptr<osg::Texture2D> TextureManager::getTexture(const std::string& name) osg::ref_ptr<osg::Texture2D> TextureManager::getTexture(VFS::Path::NormalizedView name)
{ {
// don't bother with case folding, since there is only one way of referring to terrain textures we can assume // don't bother with case folding, since there is only one way of referring to terrain textures we can assume
// the case is always the same // the case is always the same
osg::ref_ptr<osg::Object> obj = mCache->getRefFromObjectCache(name); osg::ref_ptr<osg::Object> obj = mCache->getRefFromObjectCache(name);
if (obj)
if (obj != nullptr)
return static_cast<osg::Texture2D*>(obj.get()); return static_cast<osg::Texture2D*>(obj.get());
else
{ osg::ref_ptr<osg::Texture2D> texture(new osg::Texture2D(mSceneManager->getImageManager()->getImage(name)));
osg::ref_ptr<osg::Texture2D> texture( texture->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT);
new osg::Texture2D(mSceneManager->getImageManager()->getImage(VFS::Path::toNormalized(name)))); texture->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT);
texture->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); mSceneManager->applyFilterSettings(texture);
texture->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); mCache->addEntryToObjectCache(name.value(), texture.get());
mSceneManager->applyFilterSettings(texture); return texture;
mCache->addEntryToObjectCache(name, texture.get());
return texture;
}
} }
void TextureManager::reportStats(unsigned int frameNumber, osg::Stats* stats) const void TextureManager::reportStats(unsigned int frameNumber, osg::Stats* stats) const

@ -1,9 +1,8 @@
#ifndef OPENMW_COMPONENTS_TERRAIN_TEXTUREMANAGER_H #ifndef OPENMW_COMPONENTS_TERRAIN_TEXTUREMANAGER_H
#define OPENMW_COMPONENTS_TERRAIN_TEXTUREMANAGER_H #define OPENMW_COMPONENTS_TERRAIN_TEXTUREMANAGER_H
#include <string>
#include <components/resource/resourcemanager.hpp> #include <components/resource/resourcemanager.hpp>
#include <components/vfs/pathutil.hpp>
namespace Resource namespace Resource
{ {
@ -25,7 +24,7 @@ namespace Terrain
void updateTextureFiltering(); void updateTextureFiltering();
osg::ref_ptr<osg::Texture2D> getTexture(const std::string& name); osg::ref_ptr<osg::Texture2D> getTexture(VFS::Path::NormalizedView name);
void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; void reportStats(unsigned int frameNumber, osg::Stats* stats) const override;

@ -157,8 +157,8 @@ color topic specific
-------------------- --------------------
:Type: RGBA floating point :Type: RGBA floating point
:Range: 0.0 to 1.0 :Range: 0.0 to 1.0 for each channel
:Default: empty :Default: 0.45 0.5 0.8 1 (blue)
This setting overrides the colour of dialogue topics that have a response unique to the actors speaking. This setting overrides the colour of dialogue topics that have a response unique to the actors speaking.
The value is composed of four floating point values representing the red, green, blue and alpha channels. The value is composed of four floating point values representing the red, green, blue and alpha channels.
@ -166,15 +166,67 @@ The alpha value is currently ignored.
A topic response is considered unique if its Actor filter field contains the speaking actor's object ID and hasn't yet been read. A topic response is considered unique if its Actor filter field contains the speaking actor's object ID and hasn't yet been read.
color topic specific over
-------------------------
:Type: RGBA floating point
:Range: 0.0 to 1.0 for each channel
:Default: 0.6 0.6 0.85 1 (blue)
This setting provides an "over" colour to dialogue topics that meet the color topic specific criteria.
The value is composed of four floating point values representing the red, green, blue and alpha channels.
The alpha value is currently ignored.
A dialogue topic is considered "over" if it is the active GUI element through keyboard or mouse events.
color topic specific pressed
----------------------------
:Type: RGBA floating point
:Range: 0.0 to 1.0 for each channel
:Default: 0.3 0.35 0.75 1 (blue)
This setting provides an "pressed" colour to dialogue topics that meet the color topic specific criteria.
The value is composed of four floating point values representing the red, green, blue and alpha channels.
The alpha value is currently ignored.
A dialogue topic is considered "pressed" if it is the active GUI element and it receives a sustained keyboard or mouse event.
color topic exhausted color topic exhausted
--------------------- ---------------------
:Type: RGBA floating point :Type: RGBA floating point
:Range: 0.0 to 1.0 :Range: 0.0 to 1.0 for each channel
:Default: empty :Default: 0.3 0.3 0.3 1 (grey)
This setting overrides the colour of dialogue topics which have been "exhausted" by the player. This setting overrides the colour of dialogue topics which have been "exhausted" by the player.
The value is composed of four floating point values representing the red, green, blue and alpha channels. The value is composed of four floating point values representing the red, green, blue and alpha channels.
The alpha value is currently ignored. The alpha value is currently ignored.
A topic is considered "exhausted" if the response the player is about to see has already been seen. A topic is considered "exhausted" if the response the player is about to see has already been seen.
color topic exhausted over
--------------------------
:Type: RGBA floating point
:Range: 0.0 to 1.0 for each channel
:Default: 0.55 0.55 0.55 1 (grey)
This setting provides an "over" colour to dialogue topics that meet the color topic exhausted criteria.
The value is composed of four floating point values representing the red, green, blue and alpha channels.
The alpha value is currently ignored.
A dialogue topic is considered "over" if it is the active GUI element through keyboard or mouse events.
color topic exhausted pressed
-----------------------------
:Type: RGBA floating point
:Range: 0.0 to 1.0 for each channel
:Default: 0.45 0.45 0.45 1 (grey)
This setting provides a "pressed" colour to dialogue topics that meet the color topic exhausted criteria.
The value is composed of four floating point values representing the red, green, blue and alpha channels.
The alpha value is currently ignored.
A dialogue topic is considered "pressed" if it is the active GUI element and it receives a sustained keyboard or mouse event.

@ -1,5 +1,5 @@
Camera: "Caméra dOpenMW" Camera: "OpenMW : Caméra"
settingsPageDescription: "Configuration de la caméra dOpenMW" settingsPageDescription: "Paramètres de la caméra dOpenMW"
thirdPersonSettings: "Vue à la troisième personne" thirdPersonSettings: "Vue à la troisième personne"

@ -1,19 +1,19 @@
ControlsPage: "OpenMW : Contrôles" ControlsPage: "OpenMW : Contrôles"
ControlsPageDescription: "Paramètres additionnels de contrôle" ControlsPageDescription: "Paramètres additionnels des contrôles d'OpenMW"
MovementSettings: "Mouvements" MovementSettings: "Mouvements"
alwaysRun: "Course permanente" alwaysRun: "Course permanente"
alwaysRunDescription: | alwaysRunDescription: |
Actif : Le personnage se déplace par défaut en courant ; \n\n Actif : Le personnage se déplace par défaut en courant ;
Inactif : Le personnage se déplace par défaut en marchant.\n\n Inactif : Le personnage se déplace par défaut en marchant.
La touche Maj. inverse temporairement ce paramètre.\n\n La touche Maj. inverse temporairement ce paramètre.
La touche Verr Maj inverse ce paramètre lorsqu'elle est verrouillée. La touche Verr Maj inverse ce paramètre lorsqu'elle est verrouillée.
toggleSneak: "Mode discrétion maintenu" toggleSneak: "Mode discrétion maintenu"
toggleSneakDescription: | toggleSneakDescription: |
Une simple pression de la touche associée (Ctrl par défaut) active le mode discrétion, une seconde pression désactive le mode discrétion.\n\n Une simple pression de la touche associée (Ctrl par défaut) active le mode discrétion, une seconde pression désactive le mode discrétion.
Il n'est plus nécessaire de maintenir une touche appuyée pour que le mode discrétion soit actif.\n\n Il n'est plus nécessaire de maintenir une touche appuyée pour que le mode discrétion soit actif.
Certains joueurs ayant une utilisation intensive du mode discrétion considèrent qu'il est plus aisé de contrôler leur personnage ainsi. Certains joueurs ayant une utilisation intensive du mode discrétion considèrent qu'il est plus aisé de contrôler leur personnage ainsi.
smoothControllerMovement: "Mouvements à la manette adoucis" smoothControllerMovement: "Mouvements à la manette adoucis"

@ -1,7 +1,7 @@
#Music: "OpenMW Music" Music: "OpenMW : Musique"
#settingsPageDescription: "OpenMW Music settings" settingsPageDescription: "Paramètre de la musique d'OpenMW"
#musicSettings: "Music configuration" musicSettings: "Configuration de la musique"
#CombatMusicEnabled: "Play combat music" CombatMusicEnabled: "Jouer la musique de combat"
#CombatMusicEnabledDescription: "Whether to switch to combat music when there are actors in combat." CombatMusicEnabledDescription: "Si activé, le jeu bascule vers la musique de combat dès qu'un personnage est en combat."

@ -124,6 +124,28 @@
</BasisSkin> </BasisSkin>
</Resource> </Resource>
<Resource type="ResourceSkin" name="MW_ListLine_Specific" size="5 5">
<Property key="FontName" value="Default"/>
<Property key="TextAlign" value="Left VCenter"/>
<BasisSkin type="SimpleText" offset="2 0 1 5" align="Stretch">
<State name="normal" colour="#{setting=GUI,color topic specific}"/>
<State name="highlighted" colour="#{setting=GUI,color topic specific over}"/>
<State name="pushed" colour="#{setting=GUI,color topic specific pressed}"/>
</BasisSkin>
</Resource>
<Resource type="ResourceSkin" name="MW_ListLine_Exhausted" size="5 5">
<Property key="FontName" value="Default"/>
<Property key="TextAlign" value="Left VCenter"/>
<BasisSkin type="SimpleText" offset="2 0 1 5" align="Stretch">
<State name="normal" colour="#{setting=GUI,color topic exhausted}"/>
<State name="highlighted" colour="#{setting=GUI,color topic exhausted over}"/>
<State name="pushed" colour="#{setting=GUI,color topic exhausted pressed}"/>
</BasisSkin>
</Resource>
<Resource type="ResourceLayout" name="MW_List" size="516 516" align="Left Top"> <Resource type="ResourceLayout" name="MW_List" size="516 516" align="Left Top">
<Widget type="Widget" position="0 0 516 516" name="Root"> <Widget type="Widget" position="0 0 516 516" name="Root">
<Property key="NeedKey" value="true"/> <Property key="NeedKey" value="true"/>

@ -24,7 +24,7 @@ local function onUpdate()
-- Early-out for actors without targets and without combat state -- Early-out for actors without targets and without combat state
-- TODO: use events or engine handlers to detect when targets change -- TODO: use events or engine handlers to detect when targets change
local isStanceNothing = types.Actor.getStance(self) == types.Actor.STANCE.Nothing local isStanceNothing = types.Actor.getStance(self) == types.Actor.STANCE.Nothing
if isStanceNothing and next(targets) == nil then if isStanceNothing and next(targets) == nil and not AI.isFleeing() then
return return
end end

@ -1,6 +1,6 @@
--- ---
-- `openmw.ambient` controls background sounds, specific to given player (2D-sounds). -- `openmw.ambient` controls background 2D sounds specific to a given player.
-- Can be used only by menu scripts and local scripts, that are attached to a player. -- Can be used only by menu scripts and local scripts that are attached to a player.
-- @module ambient -- @module ambient
-- @usage local ambient = require('openmw.ambient') -- @usage local ambient = require('openmw.ambient')
@ -12,11 +12,11 @@
-- @param #string soundId ID of Sound record to play -- @param #string soundId ID of Sound record to play
-- @param #table options An optional table with additional optional arguments. Can contain: -- @param #table options An optional table with additional optional arguments. Can contain:
-- --
-- * `timeOffset` - a floating point number >= 0, to skip some time (in seconds) from beginning of sound file (default: 0); -- * `timeOffset` - a floating point number >= 0, to skip some time (in seconds) from the beginning of the sound (default: 0);
-- * `volume` - a floating point number >= 0, to set a sound volume (default: 1); -- * `volume` - a floating point number >= 0, to set the sound's volume (default: 1);
-- * `pitch` - a floating point number >= 0, to set a sound pitch (default: 1); -- * `pitch` - a floating point number >= 0, to set the sound's pitch (default: 1);
-- * `scale` - a boolean, to set if sound pitch should be scaled by simulation time scaling (default: true); -- * `scale` - a boolean, to set if the sound's pitch should be scaled by simulation time scaling (default: true);
-- * `loop` - a boolean, to set if sound should be repeated when it ends (default: false); -- * `loop` - a boolean, to set if the sound should be repeated when it ends (default: false);
-- @usage local params = { -- @usage local params = {
-- timeOffset=0.1 -- timeOffset=0.1
-- volume=0.3, -- volume=0.3,
@ -29,14 +29,14 @@
--- ---
-- Play a 2D sound file -- Play a 2D sound file
-- @function [parent=#ambient] playSoundFile -- @function [parent=#ambient] playSoundFile
-- @param #string fileName Path to sound file in VFS -- @param #string fileName Path to a sound file in VFS
-- @param #table options An optional table with additional optional arguments. Can contain: -- @param #table options An optional table with additional optional arguments. Can contain:
-- --
-- * `timeOffset` - a floating point number >= 0, to skip some time (in seconds) from beginning of sound file (default: 0); -- * `timeOffset` - a floating point number >= 0, to skip some time (in seconds) from the beginning of the sound file (default: 0);
-- * `volume` - a floating point number >= 0, to set a sound volume (default: 1); -- * `volume` - a floating point number >= 0, to set the sound's volume (default: 1);
-- * `pitch` - a floating point number >= 0, to set a sound pitch (default: 1); -- * `pitch` - a floating point number >= 0, to set the sound's pitch (default: 1);
-- * `scale` - a boolean, to set if sound pitch should be scaled by simulation time scaling (default: true); -- * `scale` - a boolean, to set if the sound's pitch should be scaled by simulation time scaling (default: true);
-- * `loop` - a boolean, to set if sound should be repeated when it ends (default: false); -- * `loop` - a boolean, to set if the sound should be repeated when it ends (default: false);
-- @usage local params = { -- @usage local params = {
-- timeOffset=0.1 -- timeOffset=0.1
-- volume=0.3, -- volume=0.3,
@ -55,37 +55,37 @@
--- ---
-- Stop a sound file -- Stop a sound file
-- @function [parent=#ambient] stopSoundFile -- @function [parent=#ambient] stopSoundFile
-- @param #string fileName Path to sound file in VFS -- @param #string fileName Path to a sound file in VFS
-- @usage ambient.stopSoundFile("Sound\\test.mp3"); -- @usage ambient.stopSoundFile("Sound\\test.mp3");
--- ---
-- Check if sound is playing -- Check if a sound is playing
-- @function [parent=#ambient] isSoundPlaying -- @function [parent=#ambient] isSoundPlaying
-- @param #string soundId ID of Sound record to check -- @param #string soundId ID of Sound record to check
-- @return #boolean -- @return #boolean
-- @usage local isPlaying = ambient.isSoundPlaying("shock bolt"); -- @usage local isPlaying = ambient.isSoundPlaying("shock bolt");
--- ---
-- Check if sound file is playing -- Check if a sound file is playing
-- @function [parent=#ambient] isSoundFilePlaying -- @function [parent=#ambient] isSoundFilePlaying
-- @param #string fileName Path to sound file in VFS -- @param #string fileName Path to a sound file in VFS
-- @return #boolean -- @return #boolean
-- @usage local isPlaying = ambient.isSoundFilePlaying("Sound\\test.mp3"); -- @usage local isPlaying = ambient.isSoundFilePlaying("Sound\\test.mp3");
--- ---
-- Play a sound file as a music track -- Play a sound file as a music track
-- @function [parent=#ambient] streamMusic -- @function [parent=#ambient] streamMusic
-- @param #string fileName Path to file in VFS -- @param #string fileName Path to a file in VFS
-- @param #table options An optional table with additional optional arguments. Can contain: -- @param #table options An optional table with additional optional arguments. Can contain:
-- --
-- * `fadeOut` - a floating point number >= 0, time (in seconds) to fade out current track before playing this one (default 1.0); -- * `fadeOut` - a floating point number >= 0, time (in seconds) to fade out the current track before playing this one (default 1.0);
-- @usage local params = { -- @usage local params = {
-- fadeOut=2.0 -- fadeOut=2.0
-- }; -- };
-- ambient.streamMusic("Music\\Test\\Test.mp3", params) -- ambient.streamMusic("Music\\Test\\Test.mp3", params)
--- ---
-- Stop to play current music -- Stop the currently playing music
-- @function [parent=#ambient] stopMusic -- @function [parent=#ambient] stopMusic
-- @usage ambient.stopMusic(); -- @usage ambient.stopMusic();
@ -98,7 +98,7 @@
--- ---
-- Play an ambient voiceover. -- Play an ambient voiceover.
-- @function [parent=#ambient] say -- @function [parent=#ambient] say
-- @param #string fileName Path to sound file in VFS -- @param #string fileName Path to a sound file in VFS
-- @param #string text Subtitle text (optional) -- @param #string text Subtitle text (optional)
-- @usage -- play voiceover and print messagebox -- @usage -- play voiceover and print messagebox
-- ambient.say("Sound\\Vo\\Misc\\voice.mp3", "Subtitle text") -- ambient.say("Sound\\Vo\\Misc\\voice.mp3", "Subtitle text")
@ -108,7 +108,7 @@
--- ---
-- Stop an ambient voiceover -- Stop an ambient voiceover
-- @function [parent=#ambient] stopSay -- @function [parent=#ambient] stopSay
-- @param #string fileName Path to sound file in VFS -- @param #string fileName Path to a sound file in VFS
-- @usage ambient.stopSay(); -- @usage ambient.stopSay();
--- ---

@ -1,5 +1,5 @@
--- ---
-- `openmw.animation` defines functions that allow control of character animations -- `openmw.animation` defines functions that allow control of character animations.
-- Note that for some methods, such as @{openmw.animation#playBlended} you should use the associated methods on the -- Note that for some methods, such as @{openmw.animation#playBlended} you should use the associated methods on the
-- [AnimationController](interface_animation.html) interface rather than invoking this API directly. -- [AnimationController](interface_animation.html) interface rather than invoking this API directly.
-- @module animation -- @module animation
@ -55,7 +55,7 @@
-- @return #boolean -- @return #boolean
--- ---
-- Skips animations for one frame, equivalent to mwscript's SkipAnim -- Skips animations for one frame, equivalent to mwscript's SkipAnim.
-- Can be used only in local scripts on self. -- Can be used only in local scripts on self.
-- @function [parent=#animation] skipAnimationThisFrame -- @function [parent=#animation] skipAnimationThisFrame
-- @param openmw.core#GameObject actor -- @param openmw.core#GameObject actor
@ -98,7 +98,7 @@
--- ---
-- Cancels and removes the animation group from the list of active animations -- Cancels and removes the animation group from the list of active animations.
-- Can be used only in local scripts on self. -- Can be used only in local scripts on self.
-- @function [parent=#animation] cancel -- @function [parent=#animation] cancel
-- @param openmw.core#GameObject actor -- @param openmw.core#GameObject actor

@ -1,5 +1,5 @@
--- ---
-- `openmw.async` contains timers and coroutine utils. All functions require -- `openmw.async` contains timers and coroutine utilities. All functions require
-- the package itself as a first argument. -- the package itself as a first argument.
-- @module async -- @module async
-- @usage local async = require('openmw.async') -- @usage local async = require('openmw.async')
@ -16,7 +16,7 @@
--- ---
-- Calls callback(arg) in `delay` simulation seconds. -- Calls callback(arg) in `delay` simulation seconds.
-- Callback must be registered in advance. -- The callback must be registered in advance.
-- @function [parent=#async] newSimulationTimer -- @function [parent=#async] newSimulationTimer
-- @param self -- @param self
-- @param #number delay -- @param #number delay
@ -25,7 +25,7 @@
--- ---
-- Calls callback(arg) in `delay` game seconds. -- Calls callback(arg) in `delay` game seconds.
-- Callback must be registered in advance. -- The callback must be registered in advance.
-- @function [parent=#async] newGameTimer -- @function [parent=#async] newGameTimer
-- @param self -- @param self
-- @param #number delay -- @param #number delay
@ -49,7 +49,7 @@
-- @param #function func -- @param #function func
--- ---
-- Wraps Lua function with `Callback` object that can be used in async API calls. -- Wraps a Lua function with a `Callback` object that can be used in async API calls.
-- @function [parent=#async] callback -- @function [parent=#async] callback
-- @param self -- @param self
-- @param #function func -- @param #function func

@ -218,14 +218,14 @@ keyboard navigation = true
color topic enable = false color topic enable = false
# The color of dialogue topic keywords that gives unique actor responses # The color of dialogue topic keywords that gives unique actor responses
# Format R G B A or empty for no special formatting
# Default to blue
color topic specific = 0.45 0.5 0.8 1 color topic specific = 0.45 0.5 0.8 1
color topic specific over = 0.6 0.6 0.85 1
color topic specific pressed = 0.3 0.35 0.75 1
# The color of dialogue topic keywords that gives already read responses # The color of dialogue topic keywords that gives already read responses
# Format R G B A or empty for no special formatting
# Default to grey
color topic exhausted = 0.3 0.3 0.3 1 color topic exhausted = 0.3 0.3 0.3 1
color topic exhausted over = 0.55 0.55 0.55 1
color topic exhausted pressed = 0.45 0.45 0.45 1
[HUD] [HUD]

Loading…
Cancel
Save