Merge branch openmw:master into handtohand-tooltip

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

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

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

@ -191,6 +191,7 @@
Bug #8097: GetEffect doesn't detect 0 magnitude spells
Bug #8124: Normal weapon resistance is applied twice for NPCs
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 #2566: Handle NAM9 records for manual cell references
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_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 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
command -v ccache >/dev/null 2>&1 || brew install ccache
@ -27,8 +22,9 @@ cmake --version
qmake --version
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
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
unzip -o ~/openmw-deps.zip -d /tmp > /dev/null

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

@ -33,7 +33,8 @@ namespace
{
const ObjectId id(&shape);
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";
ObjectTransform objectTransform;
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 = [&] {
try
{
return bulletShapeManager.getShape(Misc::ResourceHelpers::correctMeshPath(model));
return bulletShapeManager.getShape(
VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(model)));
}
catch (const std::exception& e)
{

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

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

@ -4,13 +4,13 @@
#include "rotationflags.hpp"
#include <deque>
#include <map>
#include <set>
#include <span>
#include <string_view>
#include <vector>
#include <components/misc/rng.hpp>
#include <components/vfs/pathutil.hpp>
#include "../mwworld/doorstate.hpp"
#include "../mwworld/globalvariablename.hpp"
@ -515,7 +515,7 @@ namespace MWBase
/// Spawn a blood effect for \a ptr at \a worldPosition
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)
= 0;

@ -794,18 +794,32 @@ namespace MWGui
if (!Settings::gui().mColorTopicEnable)
return;
const MyGUI::Colour& specialColour = Settings::gui().mColorTopicSpecific;
const MyGUI::Colour& oldColour = Settings::gui().mColorTopicExhausted;
for (const std::string& keyword : mKeywords)
{
int flag = MWBase::Environment::get().getDialogueManager()->getTopicFlag(ESM::RefId::stringRefId(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)
button->getSubWidgetText()->setTextColour(specialColour);
{
button->changeWidgetSkin("MW_ListLine_Specific");
changed = true;
}
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 durability = iter->getClass().getItemHealth(*iter);
if (maxDurability == durability || maxDurability == 0)
if (maxDurability <= durability || maxDurability == 0)
continue;
int basePrice = iter->getClass().getValue(*iter);

@ -288,7 +288,7 @@ namespace MWGui
if ((mFilter & Filter_OnlyRepairable)
&& (!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)))
return false;

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

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

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

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

@ -846,14 +846,12 @@ namespace MWMechanics
mAI = true;
}
bool MechanicsManager::isBoundItem(const MWWorld::Ptr& item)
namespace
{
static std::set<ESM::RefId> boundItemIDCache;
// 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> makeBoundItemIdCache()
{
std::set<ESM::RefId> boundItemIDCache;
// Build a list of known bound item ID's
const MWWorld::Store<ESM::GameSetting>& gameSettings
= MWBase::Environment::get().getESMStore()->get<ESM::GameSetting>();
@ -870,15 +868,16 @@ namespace MWMechanics
boundItemIDCache.insert(ESM::RefId::stringRefId(currentGMSTValue));
}
}
// Perform bound item check and assign the Flag_Bound bit if it passes
const ESM::RefId& tempItemID = item.getCellRef().getRefId();
return boundItemIDCache;
}
}
if (boundItemIDCache.count(tempItemID) != 0)
return true;
bool MechanicsManager::isBoundItem(const MWWorld::Ptr& item)
{
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)

@ -72,12 +72,13 @@ namespace MWMechanics
{
if (effectInfo.mData.mRange == ESM::RT_Target)
world->spawnEffect(
Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, 1.0f);
VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel)), texture,
mHitPosition, 1.0f);
continue;
}
else
world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition,
static_cast<float>(effectInfo.mData.mArea * 2));
world->spawnEffect(VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel)),
texture, mHitPosition, static_cast<float>(effectInfo.mData.mArea * 2));
// 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);
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())

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

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

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

@ -388,17 +388,20 @@ namespace
std::string_view mEffectId;
};
osg::ref_ptr<osg::LightModel> getVFXLightModelInstance()
namespace
{
static osg::ref_ptr<osg::LightModel> lightModel = nullptr;
if (!lightModel)
osg::ref_ptr<osg::LightModel> makeVFXLightModelInstance()
{
lightModel = new osg::LightModel;
osg::ref_ptr<osg::LightModel> lightModel = new osg::LightModel;
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)
@ -1508,10 +1511,10 @@ namespace MWRender
}
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")
loadBonesFromFile(node, VFS::Path::toNormalized(name), resourceSystem);
loadBonesFromFile(node, name, resourceSystem);
}
}

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

@ -111,7 +111,7 @@ namespace MWRender
MWWorld::ConstPtr item = *it;
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 (item.getType() == ESM::Weapon::sRecordId)

@ -484,15 +484,24 @@ namespace MWRender
bool is1stPerson = mViewMode == VM_FirstPerson;
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(
getActorSkeleton(is1stPerson, isFemale, isBeast, isWerewolf), mResourceSystem->getVFS());
std::string smodel = defaultSkeleton;
bool isBase = !isWerewolf;
bool isCustomModel = false;
if (!is1stPerson && !isWerewolf && !mNpc->mModel.empty())
{
std::string model = Misc::ResourceHelpers::correctMeshPath(mNpc->mModel);
isBase = isDefaultActorSkeleton(model);
isCustomModel = !isDefaultActorSkeleton(model);
smodel = Misc::ResourceHelpers::correctActorModelPath(model, mResourceSystem->getVFS());
}
@ -500,33 +509,21 @@ namespace MWRender
updateParts();
if (!is1stPerson)
{
const std::string& base = Settings::models().mXbaseanim.get().value();
if (!isWerewolf)
addAnimSource(base, smodel);
if (!base.empty())
addAnimSource(base, smodel);
if (!isBase)
{
addAnimSource(defaultSkeleton, smodel);
addAnimSource(smodel, smodel);
}
else if (base != defaultSkeleton)
{
addAnimSource(defaultSkeleton, smodel);
}
if (defaultSkeleton != base)
addAnimSource(defaultSkeleton, smodel);
if (!isWerewolf && isBeast && mNpc->mRace.contains("argonian"))
addAnimSource("meshes\\xargonian_swimkna.nif", smodel);
}
else
{
if (!isWerewolf)
addAnimSource(Settings::models().mXbaseanim1st.get().value(), smodel);
if (isCustomModel)
addAnimSource(smodel, smodel);
if (!isBase)
addAnimSource(smodel, smodel);
const bool customArgonianSwim = !is1stPerson && !isWerewolf && isBeast && mNpc->mRace.contains("argonian");
if (customArgonianSwim)
addAnimSource(Settings::models().mXargonianswimkna.get().value(), smodel);
if (is1stPerson)
{
mObjectRoot->setNodeMask(Mask_FirstPerson);
mObjectRoot->addCullCallback(new OverrideFieldOfViewCallback(mFirstPersonFieldOfView));
}
@ -681,11 +678,11 @@ namespace MWRender
PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, std::string_view bonename,
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)
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)

@ -20,6 +20,7 @@
#include <components/esm3/loaddoor.hpp>
#include <components/esm3/loadstat.hpp>
#include <components/esm3/readerscache.hpp>
#include <components/misc/pathhelpers.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/misc/rng.hpp>
#include <components/resource/scenemanager.hpp>
@ -639,7 +640,7 @@ namespace MWRender
continue;
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())
continue;
model = Misc::ResourceHelpers::correctMeshPath(model);
@ -647,10 +648,10 @@ namespace MWRender
if (activeGrid && type != ESM::REC_STAT)
{
model = Misc::ResourceHelpers::correctActorModelPath(model, mSceneManager->getVFS());
std::string kfname = Misc::StringUtils::lowerCase(model);
if (kfname.size() > 4 && kfname.ends_with(".nif"))
if (Misc::getFileExtension(model) == "nif")
{
kfname.replace(kfname.size() - 4, 4, ".kf");
VFS::Path::Normalized kfname = model;
kfname.changeExtension("kf");
if (mSceneManager->getVFS()->exists(kfname))
continue;
}
@ -671,7 +672,7 @@ namespace MWRender
->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)
{

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

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

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

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

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

@ -17,6 +17,7 @@ namespace MWWorld
class Ptr;
class ESMStore;
class Class;
class WorldModel;
template <typename X>
struct LiveCellRef;
@ -29,17 +30,28 @@ namespace MWWorld
/** Information about this instance, such as 3D location and rotation
* and individual type-dependent data.
*/
MWWorld::CellRef mRef;
CellRef mRef;
/** runtime-data */
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::ActorCharacter& cref);
LiveCellRefBase(const LiveCellRefBase& other) = default;
LiveCellRefBase(LiveCellRefBase&& other) noexcept;
/* Need this for the class to be recognized as polymorphic */
virtual ~LiveCellRefBase();
LiveCellRefBase& operator=(const LiveCellRefBase& other) = default;
LiveCellRefBase& operator=(LiveCellRefBase&& other) noexcept;
virtual void load(const ESM::ObjectState& state) = 0;
///< 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.
const X* mBase;

@ -36,8 +36,20 @@
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)
: mCellStore(nullptr)
: mPlayer(makePlayerCellRef(), player)
, mCellStore(nullptr)
, mLastKnownExteriorPosition(0, 0, 0)
, mMarkedPosition(ESM::Position())
, mMarkedCell(nullptr)
@ -46,11 +58,6 @@ namespace MWWorld
, mPaidCrimeId(-1)
, 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();
playerPos.pos[0] = playerPos.pos[1] = playerPos.pos[2] = 0;
mPlayer.mData.setPosition(playerPos);

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

@ -3644,10 +3644,10 @@ namespace MWWorld
mRendering->spawnEffect(model, texture, worldPosition, 1.0f, false);
}
void World::spawnEffect(const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos,
float scale, bool isMagicVFX)
void World::spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride,
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

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

@ -3,6 +3,7 @@
#include <algorithm>
#include <cassert>
#include <optional>
#include <stdexcept>
#include <components/debug/debuglog.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));
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)

@ -77,9 +77,9 @@ namespace MWWorld
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); }

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

@ -1,11 +1,28 @@
#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 <filesystem>
int main(int argc, char* argv[])
{
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);
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);
lastAppliedStack.push_back(group);
}
if (!(lastAppliedStack.back() == this))
if (lastAppliedStack.empty() || !(lastAppliedStack.back() == this))
{
push(state);
lastAppliedStack.push_back(this);

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

@ -287,4 +287,10 @@ namespace ESM
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);
CellRef makeBlankCellRef();
}
#endif

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

@ -320,16 +320,18 @@ namespace ESM4
std::filesystem::path path = strings / (prefix + language + suffix);
if (mVFS != nullptr)
{
std::string vfsPath = Files::pathToUnicodeString(path);
if (!mVFS->exists(vfsPath))
VFS::Path::Normalized vfsPath(Files::pathToUnicodeString(path));
Files::IStreamPtr stream = mVFS->find(vfsPath);
if (stream == nullptr)
{
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);
return;
}

@ -113,6 +113,7 @@ namespace LuaUi
ContentView content(LuaUtil::cast<sol::table>(contentObj));
result.resize(content.size());
size_t minSize = std::min(children.size(), content.size());
std::vector<WidgetExtension*> toDestroy;
for (size_t i = 0; i < minSize; i++)
{
WidgetExtension* ext = children[i];
@ -121,7 +122,7 @@ namespace LuaUi
{
WidgetExtension* root = pluckElementRoot(child, depth);
if (ext != root)
destroyChild(ext);
toDestroy.emplace_back(ext);
result[i] = root;
}
else
@ -133,14 +134,12 @@ namespace LuaUi
}
else
{
destroyChild(ext);
toDestroy.emplace_back(ext);
ext = createWidget(newLayout, false, depth);
}
result[i] = ext;
}
}
for (size_t i = minSize; i < children.size(); i++)
destroyChild(children[i]);
for (size_t i = minSize; i < content.size(); i++)
{
sol::object child = content.at(i);
@ -149,6 +148,11 @@ namespace LuaUi
else
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;
}
@ -217,7 +221,9 @@ namespace LuaUi
std::string setLayer(WidgetExtension* ext, const sol::table& layout)
{
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());
if (!newLayer.empty() && !MyGUI::LayerManager::getInstance().isExist(newLayer))
throw std::logic_error(std::string("Layer ") + newLayer + " doesn't exist");
@ -278,9 +284,20 @@ namespace LuaUi
WidgetExtension* parent = mRoot->getParent();
auto children = parent->children();
auto it = std::find(children.begin(), children.end(), mRoot);
mRoot = createWidget(layout(), true, 0);
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);
mRoot->updateCoord();
}
@ -300,6 +317,10 @@ namespace LuaUi
{
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);
mRoot = nullptr;
}

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

@ -179,7 +179,7 @@ namespace LuaUi
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();)
{

@ -228,7 +228,7 @@ bool Misc::ResourceHelpers::isHiddenMarker(const ESM::RefId& id)
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)
resPath.insert(w, pattern);
@ -237,7 +237,7 @@ namespace
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 resPath;
}

@ -95,7 +95,7 @@ namespace osgMyGUI
if (!mImageManager)
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->setWrap(osg::Texture::WRAP_S, 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();
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;
}
@ -93,7 +93,7 @@ namespace NifBullet
}
else
{
warn("Invalid Bounding Box node bounds in file " + mShape->mFileName);
warn("Invalid Bounding Box node bounds in file " + mShape->mFileName.value());
}
return true;
}

@ -1,7 +1,6 @@
#ifndef OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H
#define OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H
#include <array>
#include <map>
#include <memory>
@ -12,6 +11,8 @@
#include <BulletCollision/CollisionShapes/btBvhTriangleMeshShape.h>
#include <BulletCollision/CollisionShapes/btScaledBvhTriangleMeshShape.h>
#include <components/vfs/pathutil.hpp>
class btCollisionShape;
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.
std::map<int, int> mAnimatedShapes;
std::string mFileName;
VFS::Path::Normalized mFileName;
std::string mFileHash;
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<osg::Object> obj = mCache->getRefFromObjectCache(normalized);
if (obj)
shape = osg::ref_ptr<BulletShape>(static_cast<BulletShape*>(obj.get()));
if (Misc::getFileExtension(name.value()) == "nif")
{
NifBullet::BulletNifLoader loader;
shape = loader.load(*mNifFileManager->get(name));
}
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;
shape = loader.load(*mNifFileManager->get(normalized));
NodeToShapeVisitor visitor;
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
osg::ref_ptr<const osg::Node> constNode(mSceneManager->getTemplate(normalized));
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
NodeToShapeVisitor visitor;
node->accept(visitor);
shape = visitor.getShape();
if (!shape)
{
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);
}
return osg::ref_ptr<BulletShape>();
}
mCache->addEntryToObjectCache(normalized, shape);
if (shape != nullptr)
{
shape->mFileName = name;
constNode->getUserValue(Misc::OsgUserValues::sFileHash, shape->mFileHash);
}
}
mCache->addEntryToObjectCache(name.value(), 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(normalized);
if (instance)
mInstanceCache->addEntryToObjectCache(normalized, instance.get());
osg::ref_ptr<BulletShapeInstance> instance = createInstance(name);
if (instance != nullptr)
mInstanceCache->addEntryToObjectCache(name, instance.get());
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);
osg::ref_ptr<osg::Object> obj = mInstanceCache->takeFromObjectCache(normalized);
if (obj.get())
if (osg::ref_ptr<osg::Object> obj = mInstanceCache->takeFromObjectCache(name))
return static_cast<BulletShapeInstance*>(obj.get());
else
return createInstance(normalized);
return createInstance(name);
}
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 (shape)
if (osg::ref_ptr<const BulletShape> shape = getShape(name))
return makeInstance(std::move(shape));
return osg::ref_ptr<BulletShapeInstance>();
}

@ -1,11 +1,10 @@
#ifndef OPENMW_COMPONENTS_BULLETSHAPEMANAGER_H
#define OPENMW_COMPONENTS_BULLETSHAPEMANAGER_H
#include <map>
#include <string>
#include <osg/ref_ptr>
#include <components/vfs/pathutil.hpp>
#include "bulletshape.hpp"
#include "resourcemanager.hpp"
@ -30,16 +29,16 @@ namespace Resource
~BulletShapeManager();
/// @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
/// 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
/// 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.
osg::ref_ptr<BulletShapeInstance> getInstance(const std::string& name);
osg::ref_ptr<BulletShapeInstance> getInstance(VFS::Path::NormalizedView name);
/// @see ResourceManager::updateCache
void updateCache(double referenceTime) override;
@ -49,7 +48,7 @@ namespace Resource
void reportStats(unsigned int frameNumber, osg::Stats* stats) const override;
private:
osg::ref_ptr<BulletShapeInstance> createInstance(const std::string& name);
osg::ref_ptr<BulletShapeInstance> createInstance(VFS::Path::NormalizedView name);
osg::ref_ptr<MultiObjectCache> mInstanceCache;
SceneManager* mSceneManager;

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

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

@ -3,11 +3,12 @@
#include <map>
#include <mutex>
#include <string>
#include <osg/Referenced>
#include <osg/ref_ptr>
#include <components/vfs/pathutil.hpp>
#include "cachestats.hpp"
namespace osg
@ -23,18 +24,15 @@ namespace Resource
class MultiObjectCache : public osg::Referenced
{
public:
MultiObjectCache();
~MultiObjectCache();
void removeUnreferencedObjectsInCache();
/** Remove all objects from the cache. */
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. */
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.*/
void releaseGLObjects(osg::State* state);
@ -42,7 +40,7 @@ namespace Resource
CacheStats getStats() const;
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;
mutable std::mutex _objectCacheMutex;

@ -597,9 +597,9 @@ namespace Resource
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)

@ -152,7 +152,7 @@ namespace Resource
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
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"
/// @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;
};
void mergeUserData(const osg::UserDataContainer* source, osg::Object* target)
namespace
{
if (!source)
return;
if (!target->getUserDataContainer())
target->setUserDataContainer(osg::clone(source, osg::CopyOp::SHALLOW_COPY));
else
void mergeUserData(const osg::UserDataContainer* source, osg::Object* target)
{
for (unsigned int i = 0; i < source->getNumUserObjects(); ++i)
target->getUserDataContainer()->addUserObject(
osg::clone(source->getUserObject(i), osg::CopyOp::SHALLOW_COPY));
if (!source)
return;
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
// invert it However MW isn't doing this either, so don't. Assuming all meshes are using backface
// culling is more efficient.
static osg::ref_ptr<osg::StateSet> frontFaceStateSet;
if (!frontFaceStateSet)
{
frontFaceStateSet = new osg::StateSet;
osg::FrontFace* frontFace = new osg::FrontFace;
frontFace->setMode(osg::FrontFace::CLOCKWISE);
frontFaceStateSet->setAttributeAndModes(frontFace, osg::StateAttribute::ON);
}
static const osg::ref_ptr<osg::StateSet> frontFaceStateSet = makeFrontFaceStateSet();
trans->setStateSet(frontFaceStateSet);
}

@ -33,7 +33,11 @@ namespace Settings
SettingValue<bool> mKeyboardNavigation{ mIndex, "GUI", "keyboard navigation" };
SettingValue<bool> mColorTopicEnable{ mIndex, "GUI", "color topic enable" };
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> 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
#define COMPONENTS_TERRAIN_DEFS_HPP
#include <string>
#include <components/vfs/pathutil.hpp>
namespace Terrain
{
@ -16,8 +16,8 @@ namespace Terrain
struct LayerInfo
{
std::string mDiffuseMap;
std::string mNormalMap;
VFS::Path::Normalized mDiffuseMap;
VFS::Path::Normalized mNormalMap;
bool mParallax; // Height info in normal map alpha channel?
bool mSpecular; // Specular info in diffuse map alpha channel?

@ -404,63 +404,68 @@ namespace Terrain
}
}
void updateWaterCullingView(
HeightCullCallback* callback, ViewData* vd, osgUtil::CullVisitor* cv, float cellworldsize, bool outofworld)
namespace
{
if (!(cv->getTraversalMask() & callback->getCullMask()))
return;
float lowZ = std::numeric_limits<float>::max();
float highZ = callback->getHighZ();
if (cv->getEyePoint().z() <= highZ || outofworld)
osg::ref_ptr<osg::StateSet> makeStateSet()
{
callback->setLowZ(-std::numeric_limits<float>::max());
return;
osg::ref_ptr<osg::StateSet> stateSet = new osg::StateSet;
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;
for (unsigned int i = 0; i < vd->getNumEntries(); ++i)
void updateWaterCullingView(
HeightCullCallback* callback, ViewData* vd, osgUtil::CullVisitor* cv, float cellworldsize, bool outofworld)
{
ViewDataEntry& entry = vd->getEntry(i);
osg::BoundingBox bb
= static_cast<TerrainDrawable*>(entry.mRenderingNode->asGroup()->getChild(0))->getWaterBoundingBox();
if (!bb.valid())
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 osg::ref_ptr<osg::StateSet> stateset = nullptr;
if (!stateset)
if (!(cv->getTraversalMask() & callback->getCullMask()))
return;
float lowZ = std::numeric_limits<float>::max();
float highZ = callback->getHighZ();
if (cv->getEyePoint().z() <= highZ || outofworld)
{
stateset = new osg::StateSet;
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::Material* m = new osg::Material;
m->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 1, 1));
m->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 1));
m->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, 1));
stateset->setAttributeAndModes(m, osg::StateAttribute::ON);
stateset->setRenderBinDetails(100, "RenderBin");
callback->setLowZ(-std::numeric_limits<float>::max());
return;
}
cv->pushCurrentMask();
static bool debug = getenv("OPENMW_WATER_CULLING_DEBUG") != nullptr;
for (unsigned int i = 0; i < vd->getNumEntries(); ++i)
{
ViewDataEntry& entry = vd->getEntry(i);
osg::BoundingBox bb = static_cast<TerrainDrawable*>(entry.mRenderingNode->asGroup()->getChild(0))
->getWaterBoundingBox();
if (!bb.valid())
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);
drw->accept(*cv);
callback->setLowZ(lowZ);
cv->popCurrentMask();
}
callback->setLowZ(lowZ);
cv->popCurrentMask();
}
void QuadTreeWorld::accept(osg::NodeVisitor& nv)

@ -35,23 +35,21 @@ namespace Terrain
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
// the case is always the same
osg::ref_ptr<osg::Object> obj = mCache->getRefFromObjectCache(name);
if (obj)
if (obj != nullptr)
return static_cast<osg::Texture2D*>(obj.get());
else
{
osg::ref_ptr<osg::Texture2D> texture(
new osg::Texture2D(mSceneManager->getImageManager()->getImage(VFS::Path::toNormalized(name))));
texture->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT);
texture->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT);
mSceneManager->applyFilterSettings(texture);
mCache->addEntryToObjectCache(name, texture.get());
return texture;
}
osg::ref_ptr<osg::Texture2D> texture(new osg::Texture2D(mSceneManager->getImageManager()->getImage(name)));
texture->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT);
texture->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT);
mSceneManager->applyFilterSettings(texture);
mCache->addEntryToObjectCache(name.value(), texture.get());
return texture;
}
void TextureManager::reportStats(unsigned int frameNumber, osg::Stats* stats) const

@ -1,9 +1,8 @@
#ifndef OPENMW_COMPONENTS_TERRAIN_TEXTUREMANAGER_H
#define OPENMW_COMPONENTS_TERRAIN_TEXTUREMANAGER_H
#include <string>
#include <components/resource/resourcemanager.hpp>
#include <components/vfs/pathutil.hpp>
namespace Resource
{
@ -25,7 +24,7 @@ namespace Terrain
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;

@ -157,8 +157,8 @@ color topic specific
--------------------
:Type: RGBA floating point
:Range: 0.0 to 1.0
:Default: empty
:Range: 0.0 to 1.0 for each channel
: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.
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.
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
---------------------
:Type: RGBA floating point
:Range: 0.0 to 1.0
:Default: empty
:Range: 0.0 to 1.0 for each channel
:Default: 0.3 0.3 0.3 1 (grey)
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 alpha value is currently ignored.
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"
settingsPageDescription: "Configuration de la caméra dOpenMW"
Camera: "OpenMW : Caméra"
settingsPageDescription: "Paramètres de la caméra dOpenMW"
thirdPersonSettings: "Vue à la troisième personne"

@ -1,19 +1,19 @@
ControlsPage: "OpenMW : Contrôles"
ControlsPageDescription: "Paramètres additionnels de contrôle"
ControlsPageDescription: "Paramètres additionnels des contrôles d'OpenMW"
MovementSettings: "Mouvements"
alwaysRun: "Course permanente"
alwaysRunDescription: |
Actif : Le personnage se déplace par défaut en courant ; \n\n
Inactif : Le personnage se déplace par défaut en marchant.\n\n
La touche Maj. inverse temporairement ce paramètre.\n\n
Actif : Le personnage se déplace par défaut en courant ;
Inactif : Le personnage se déplace par défaut en marchant.
La touche Maj. inverse temporairement ce paramètre.
La touche Verr Maj inverse ce paramètre lorsqu'elle est verrouillée.
toggleSneak: "Mode discrétion maintenu"
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
Il n'est plus nécessaire de maintenir une touche appuyée pour que le mode discrétion soit actif.\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.
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"

@ -1,7 +1,7 @@
#Music: "OpenMW Music"
#settingsPageDescription: "OpenMW Music settings"
Music: "OpenMW : Musique"
settingsPageDescription: "Paramètre de la musique d'OpenMW"
#musicSettings: "Music configuration"
musicSettings: "Configuration de la musique"
#CombatMusicEnabled: "Play combat music"
#CombatMusicEnabledDescription: "Whether to switch to combat music when there are actors in combat."
CombatMusicEnabled: "Jouer la musique de 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>
</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">
<Widget type="Widget" position="0 0 516 516" name="Root">
<Property key="NeedKey" value="true"/>

@ -24,7 +24,7 @@ local function onUpdate()
-- Early-out for actors without targets and without combat state
-- TODO: use events or engine handlers to detect when targets change
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
end

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

@ -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
-- [AnimationController](interface_animation.html) interface rather than invoking this API directly.
-- @module animation
@ -55,7 +55,7 @@
-- @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.
-- @function [parent=#animation] skipAnimationThisFrame
-- @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.
-- @function [parent=#animation] cancel
-- @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.
-- @module async
-- @usage local async = require('openmw.async')
@ -16,7 +16,7 @@
---
-- Calls callback(arg) in `delay` simulation seconds.
-- Callback must be registered in advance.
-- The callback must be registered in advance.
-- @function [parent=#async] newSimulationTimer
-- @param self
-- @param #number delay
@ -25,7 +25,7 @@
---
-- Calls callback(arg) in `delay` game seconds.
-- Callback must be registered in advance.
-- The callback must be registered in advance.
-- @function [parent=#async] newGameTimer
-- @param self
-- @param #number delay
@ -49,7 +49,7 @@
-- @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
-- @param self
-- @param #function func

@ -218,14 +218,14 @@ keyboard navigation = true
color topic enable = false
# 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 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
# 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 over = 0.55 0.55 0.55 1
color topic exhausted pressed = 0.45 0.45 0.45 1
[HUD]

Loading…
Cancel
Save