1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-03-31 16:36:41 +00:00

Merge branch openmw:master into handtohand-tooltip

This commit is contained in:
trav 2024-10-05 17:24:17 +00:00
commit 3d2dd9201d
78 changed files with 867 additions and 578 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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);

View file

@ -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)
{

View file

@ -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)

View file

@ -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()

View file

@ -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;

View file

@ -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);
}
}
}

View file

@ -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);

View file

@ -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;

View file

@ -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");
}
};

View file

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

View file

@ -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;

View file

@ -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);

View file

@ -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));
}
return boundItemIDCache;
}
}
// Perform bound item check and assign the Flag_Bound bit if it passes
const ESM::RefId& tempItemID = item.getCellRef().getRefId();
bool MechanicsManager::isBoundItem(const MWWorld::Ptr& item)
{
static const std::set<ESM::RefId> boundItemIdCache = makeBoundItemIdCache();
if (boundItemIDCache.count(tempItemID) != 0)
return true;
return false;
return boundItemIdCache.find(item.getCellRef().getRefId()) != boundItemIdCache.end();
}
bool MechanicsManager::isAllowedToUse(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim)

View file

@ -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())

View file

@ -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;

View file

@ -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);
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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)

View file

@ -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)
if (!base.empty())
addAnimSource(base, smodel);
if (defaultSkeleton != base)
addAnimSource(defaultSkeleton, smodel);
if (isCustomModel)
addAnimSource(smodel, smodel);
const bool customArgonianSwim = !is1stPerson && !isWerewolf && isBeast && mNpc->mRace.contains("argonian");
if (customArgonianSwim)
addAnimSource(Settings::models().mXargonianswimkna.get().value(), smodel);
if (is1stPerson)
{
const std::string& base = Settings::models().mXbaseanim.get().value();
if (!isWerewolf)
addAnimSource(base, smodel);
if (!isBase)
{
addAnimSource(defaultSkeleton, smodel);
addAnimSource(smodel, smodel);
}
else if (base != defaultSkeleton)
{
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 (!isBase)
addAnimSource(smodel, smodel);
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)

View file

@ -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)
{

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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);

View file

@ -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
{
}
MWWorld::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)
{
}
MWWorld::LiveCellRefBase::~LiveCellRefBase()
{
MWBase::Environment::get().getWorldModel()->deregisterLiveCellRef(*this);
}
void MWWorld::LiveCellRefBase::loadImp(const ESM::ObjectState& state)
{
mRef = MWWorld::CellRef(state.mRef);
mData = RefData(state, mData.isDeletedByContentFile());
Ptr ptr(this);
if (state.mHasLocals)
LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM::CellRef& cref)
: mClass(&Class::get(type))
, mRef(cref)
, mData(cref)
{
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())
}
LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::Reference& 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)
{
}
LiveCellRefBase::LiveCellRefBase(LiveCellRefBase&& other) noexcept
: mClass(other.mClass)
, mRef(std::move(other.mRef))
, mData(std::move(other.mData))
, mWorldModel(std::exchange(other.mWorldModel, nullptr))
{
}
LiveCellRefBase::~LiveCellRefBase()
{
if (mWorldModel != nullptr)
mWorldModel->deregisterLiveCellRef(*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;
}
void LiveCellRefBase::loadImp(const ESM::ObjectState& state)
{
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
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
<< " 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);
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);
}
mClass->readAdditionalState(ptr, state);
if (!mRef.getSoul().empty()
&& !MWBase::Environment::get().getESMStore()->get<ESM::Creature>().search(mRef.getSoul()))
void LiveCellRefBase::saveImp(ESM::ObjectState& state) const
{
Log(Debug::Warning) << "Soul '" << mRef.getSoul() << "' not found, removing the soul from soul gem";
mRef.setSoul(ESM::RefId());
mRef.writeState(state);
ConstPtr ptr(this);
mData.write(state, mClass->getScript(ptr));
MWBase::Environment::get().getLuaManager()->saveLocalScripts(
Ptr(const_cast<LiveCellRefBase*>(this)), state.mLuaScripts);
mClass->writeAdditionalState(ptr, state);
}
MWBase::Environment::get().getLuaManager()->loadLocalScripts(ptr, state.mLuaScripts);
}
bool LiveCellRefBase::checkStateImp(const ESM::ObjectState& state)
{
return true;
}
void MWWorld::LiveCellRefBase::saveImp(ESM::ObjectState& state) const
{
mRef.writeState(state);
unsigned int LiveCellRefBase::getType() const
{
return mClass->getType();
}
ConstPtr ptr(this);
bool LiveCellRefBase::isDeleted() const
{
return mData.isDeletedByContentFile() || mRef.getCount(false) == 0;
}
mData.write(state, mClass->getScript(ptr));
MWBase::Environment::get().getLuaManager()->saveLocalScripts(
Ptr(const_cast<LiveCellRefBase*>(this)), state.mLuaScripts);
mClass->writeAdditionalState(ptr, state);
}
bool MWWorld::LiveCellRefBase::checkStateImp(const ESM::ObjectState& state)
{
return true;
}
unsigned int MWWorld::LiveCellRefBase::getType() const
{
return mClass->getType();
}
bool MWWorld::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;

View file

@ -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;

View file

@ -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);

View file

@ -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)

View file

@ -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

View file

@ -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;

View file

@ -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)

View file

@ -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); }

View file

@ -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

View file

@ -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();
}

View file

@ -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());
}
}
}

View file

@ -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);

View file

@ -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"))

View file

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

View file

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

View file

@ -132,53 +132,61 @@ namespace ESM
esm.writeHNOString("DESC", mDescription);
}
namespace
{
std::map<short, short> makeEffectsMap()
{
std::map<short, short> effects;
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[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[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[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;
return effects;
}
}
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 std::map<short, short> effects;
if (effects.empty())
{
effects[DisintegrateArmor] = Sanctuary;
effects[DisintegrateWeapon] = Sanctuary;
static const std::map<short, short> effects = makeEffectsMap();
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;
if (const auto it = effects.find(effect); it != effects.end())
return it->second;
effects[Burden] = ResistMagicka;
effects[Charm] = ResistMagicka;
effects[Silence] = ResistMagicka;
effects[Blind] = ResistMagicka;
effects[Sound] = 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[TurnUndead] = ResistMagicka;
effects[FireDamage] = ResistFire;
effects[FrostDamage] = ResistFrost;
effects[ShockDamage] = ResistShock;
effects[Vampirism] = ResistCommonDisease;
effects[Corprus] = ResistCorprusDisease;
effects[Poison] = ResistPoison;
effects[Paralyze] = ResistParalysis;
}
if (effects.find(effect) != effects.end())
return effects[effect];
else
return -1;
return -1;
}
short MagicEffect::getWeaknessEffect(short effect)

View file

@ -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;
}

View file

@ -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;
}

View file

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

View file

@ -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();)
{

View file

@ -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;
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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;

View file

@ -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>();
}

View file

@ -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;

View file

@ -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)
{

View file

@ -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)

View file

@ -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;

View file

@ -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)

View file

@ -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.

View file

@ -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);
}

View file

@ -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" };
};
}

View file

@ -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?

View file

@ -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)
{
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)
void updateWaterCullingView(
HeightCullCallback* callback, ViewData* vd, osgUtil::CullVisitor* cv, float cellworldsize, bool outofworld)
{
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;
}
drw->setStateSet(stateset);
drw->accept(*cv);
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);
}
callback->setLowZ(lowZ);
cv->popCurrentMask();
}
callback->setLowZ(lowZ);
cv->popCurrentMask();
}
void QuadTreeWorld::accept(osg::NodeVisitor& nv)

View file

@ -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

View file

@ -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;

View file

@ -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.

View file

@ -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"

View file

@ -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"

View file

@ -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."

View file

@ -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"/>

View file

@ -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

View file

@ -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();
---

View file

@ -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

View file

@ -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

View file

@ -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]