1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-10-18 01:16:37 +00:00

Merge branch openmw:master into master

This commit is contained in:
Andy Lanzone 2025-07-08 19:46:37 -07:00
commit d46f034e5d
19 changed files with 293 additions and 156 deletions

View file

@ -1,7 +1,52 @@
0.50.0
------
Bug #2967: Inventory windows don't update when changing items by script
Bug #5331: Pathfinding works incorrectly when actor is moved from one interior cell to another
Bug #6039: Next Spell keybind fails while selected enchanted item has multiple copies
Bug #6573: Editor: Selection behaves incorrectly on high-DPI displays
Bug #6792: Birth sign info box has no line breaks
Bug #7622: Player's marksman weapons don't work on close actors underwater
Bug #7740: Magic items in the HUD aren't composited correctly
Bug #7799: Picking up ingredients while object paging active grid is on may cause a hiccup
Bug #8245: The console command ShowVars does not list global mwscripts
Bug #8265: Topics are linked incorrectly
Bug #8303: On target spells cast by non-actors should fire underwater
Bug #8318: Missing global variables are not handled gracefully in dialogue conditions
Bug #8340: Multi-effect enchantments are too expensive
Bug #8341: Repeat shader visitor passes discard parallax
Bug #8349: Travel to non-existent cell causes persistent black screen
Bug #8371: Silence affects powers
Bug #8375: Moon phase cycle doesn't match Morrowind
Bug #8383: Casting bound helm or boots on beast races doesn't cleanup properly
Bug #8385: Russian encoding broken with locale parameters and calendar
Bug #8408: OpenMW doesn't report all the potential resting hindrances
Bug #8414: Waterwalking works when collision is disabled
Bug #8431: Behaviour of removed items from a container is buggy
Bug #8432: Changing to and from an interior cell doesn't update collision
Bug #8436: Spell selection in a pinned spellbook window doesn't update
Bug #8437: Pinned inventory window's pin button doesn't look pressed
Bug #8446: Travel prices are strangely inconsistent
Bug #8459: Changing magic effect base cost doesn't change spell price
Bug #8466: Showmap "" reveals nameless cells
Bug #8485: Witchwither disease and probably other common diseases don't work correctly
Bug #8490: Normals on Water disappear when Water Shader is Enabled but Refraction is Disabled
Bug #8500: OpenMW Alarm behaviour doesn't match morrowind.exe
Bug #8519: Multiple bounty is sometimes assigned to player when detected during a pickpocketing action
Bug #8585: Dialogue topic list doesn't have enough padding
Bug #8587: Minor INI importer problems
Bug #8593: Render targets do not generate mipmaps
Bug #8598: Post processing shaders don't interact with the vfs correctly
Bug #8599: Non-ASCII paths in BSA files don't work
Feature #3769: Allow GetSpellEffects on enchantments
Feature #8112: Expose landscape record data to Lua
Feature #8113: Support extended selection in autodetected subdirectory dialog
Feature #8285: Expose list of active shaders in postprocessing API
Feature #8313: Show the character name in the savegame details
Feature #8320: Add access mwscript source text to lua api
Feature #8355: Lua: Window visibility checking in interfaces.UI
Feature #8580: Sort characters in the save loading menu
Feature #8597: Lua: Add more built-in event handlers
0.49.0
------

View file

@ -9,22 +9,33 @@ namespace
{
using namespace testing;
struct LuaUtilPackageTest : Test
{
LuaUtil::LuaState mLuaState{ nullptr, nullptr };
LuaUtilPackageTest()
{
mLuaState.addInternalLibSearchPath(
std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "components" / "lua");
sol::state_view sol = mLuaState.unsafeState();
sol["util"] = LuaUtil::initUtilPackage(sol);
}
};
template <typename T>
T get(sol::state& lua, const std::string& luaCode)
T get(sol::state_view& lua, const std::string& luaCode)
{
return lua.safe_script("return " + luaCode).get<T>();
}
std::string getAsString(sol::state& lua, std::string luaCode)
std::string getAsString(sol::state_view& lua, std::string luaCode)
{
return LuaUtil::toString(lua.safe_script("return " + luaCode));
}
TEST(LuaUtilPackageTest, Vector2)
TEST_F(LuaUtilPackageTest, Vector2)
{
sol::state lua;
lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string);
lua["util"] = LuaUtil::initUtilPackage(lua);
sol::state_view lua = mLuaState.unsafeState();
lua.safe_script("v = util.vector2(3, 4)");
EXPECT_FLOAT_EQ(get<float>(lua, "v.x"), 3);
EXPECT_FLOAT_EQ(get<float>(lua, "v.y"), 4);
@ -55,11 +66,9 @@ namespace
EXPECT_TRUE(get<bool>(lua, "swizzle['01'] == util.vector2(0, 1) and swizzle['0y'] == util.vector2(0, 2)"));
}
TEST(LuaUtilPackageTest, Vector3)
TEST_F(LuaUtilPackageTest, Vector3)
{
sol::state lua;
lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string);
lua["util"] = LuaUtil::initUtilPackage(lua);
sol::state_view lua = mLuaState.unsafeState();
lua.safe_script("v = util.vector3(5, 12, 13)");
EXPECT_FLOAT_EQ(get<float>(lua, "v.x"), 5);
EXPECT_FLOAT_EQ(get<float>(lua, "v.y"), 12);
@ -94,11 +103,9 @@ namespace
get<bool>(lua, "swizzle['001'] == util.vector3(0, 0, 1) and swizzle['0yx'] == util.vector3(0, 2, 1)"));
}
TEST(LuaUtilPackageTest, Vector4)
TEST_F(LuaUtilPackageTest, Vector4)
{
sol::state lua;
lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string);
lua["util"] = LuaUtil::initUtilPackage(lua);
sol::state_view lua = mLuaState.unsafeState();
lua.safe_script("v = util.vector4(5, 12, 13, 15)");
EXPECT_FLOAT_EQ(get<float>(lua, "v.x"), 5);
EXPECT_FLOAT_EQ(get<float>(lua, "v.y"), 12);
@ -136,11 +143,9 @@ namespace
lua, "swizzle['0001'] == util.vector4(0, 0, 0, 1) and swizzle['0yx1'] == util.vector4(0, 2, 1, 1)"));
}
TEST(LuaUtilPackageTest, Color)
TEST_F(LuaUtilPackageTest, Color)
{
sol::state lua;
lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string);
lua["util"] = LuaUtil::initUtilPackage(lua);
sol::state_view lua = mLuaState.unsafeState();
lua.safe_script("brown = util.color.rgba(0.75, 0.25, 0, 1)");
EXPECT_EQ(get<std::string>(lua, "tostring(brown)"), "(0.75, 0.25, 0, 1)");
lua.safe_script("blue = util.color.rgb(0, 1, 0, 1)");
@ -155,11 +160,9 @@ namespace
EXPECT_TRUE(get<bool>(lua, "red:asRgb() == util.vector3(1, 0, 0)"));
}
TEST(LuaUtilPackageTest, Transform)
TEST_F(LuaUtilPackageTest, Transform)
{
sol::state lua;
lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string);
lua["util"] = LuaUtil::initUtilPackage(lua);
sol::state_view lua = mLuaState.unsafeState();
lua["T"] = lua["util"]["transform"];
lua["v"] = lua["util"]["vector3"];
EXPECT_ERROR(lua.safe_script("T.identity = nil"), "attempt to index");
@ -191,11 +194,9 @@ namespace
EXPECT_LT(get<float>(lua, "(rz_move_rx:inverse() * v(0, 1, 2) - v(1, 2, 3)):length()"), 1e-6);
}
TEST(LuaUtilPackageTest, UtilityFunctions)
TEST_F(LuaUtilPackageTest, UtilityFunctions)
{
sol::state lua;
lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string);
lua["util"] = LuaUtil::initUtilPackage(lua);
sol::state_view lua = mLuaState.unsafeState();
lua.safe_script("v = util.vector2(1, 0):rotate(math.rad(120))");
EXPECT_FLOAT_EQ(get<float>(lua, "v.x"), -0.5f);
EXPECT_FLOAT_EQ(get<float>(lua, "v.y"), 0.86602539f);
@ -203,6 +204,10 @@ namespace
EXPECT_FLOAT_EQ(get<float>(lua, "util.clamp(0.1, 0, 1.5)"), 0.1f);
EXPECT_FLOAT_EQ(get<float>(lua, "util.clamp(-0.1, 0, 1.5)"), 0);
EXPECT_FLOAT_EQ(get<float>(lua, "util.clamp(2.1, 0, 1.5)"), 1.5f);
EXPECT_FLOAT_EQ(get<float>(lua, "util.round(2.1)"), 2.0f);
EXPECT_FLOAT_EQ(get<float>(lua, "util.round(-2.1)"), -2.0f);
EXPECT_FLOAT_EQ(get<float>(lua, "util.remap(5, 0, 10, 0, 100)"), 50.0f);
EXPECT_FLOAT_EQ(get<float>(lua, "util.remap(-5, 0, 10, 0, 100)"), -50.0f);
lua.safe_script("t = util.makeReadOnly({x = 1})");
EXPECT_FLOAT_EQ(get<float>(lua, "t.x"), 1);
EXPECT_ERROR(lua.safe_script("t.y = 2"), "userdata value");

View file

@ -26,6 +26,15 @@ namespace
std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({});
constexpr VFS::Path::NormalizedView path("sound/foo.wav");
EXPECT_EQ(correctSoundPath(path, *mVFS), "sound/foo.mp3");
auto correctESM4SoundPath = [](auto path, auto* vfs) {
return Misc::ResourceHelpers::correctResourcePath({ { "sound" } }, path, vfs, ".mp3");
};
EXPECT_EQ(correctESM4SoundPath("foo.WAV", mVFS.get()), "sound\\foo.mp3");
EXPECT_EQ(correctESM4SoundPath("SOUND/foo.WAV", mVFS.get()), "sound\\foo.mp3");
EXPECT_EQ(correctESM4SoundPath("DATA\\SOUND\\foo.WAV", mVFS.get()), "sound\\foo.mp3");
EXPECT_EQ(correctESM4SoundPath("\\Data/Sound\\foo.WAV", mVFS.get()), "sound\\foo.mp3");
}
namespace

View file

@ -149,5 +149,9 @@ namespace MWLua
addModelProperty(record);
record["isAutomatic"] = sol::readonly_property(
[](const ESM4::Door& rec) -> bool { return rec.mDoorFlags & ESM4::Door::Flag_AutomaticDoor; });
record["openSound"] = sol::readonly_property(
[](const ESM4::Door& rec) -> std::string { return ESM::RefId(rec.mOpenSound).serializeText(); });
record["closeSound"] = sol::readonly_property(
[](const ESM4::Door& rec) -> std::string { return ESM::RefId(rec.mCloseSound).serializeText(); });
}
}

View file

@ -519,8 +519,31 @@ namespace MWMechanics
case ESM::MagicEffect::ExtraSpell:
if (target.getClass().hasInventoryStore(target))
{
auto& store = target.getClass().getInventoryStore(target);
store.unequipAll();
if (target != getPlayer())
{
auto& store = target.getClass().getInventoryStore(target);
for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot)
{
// Unequip everything except weapons, torches, and pants
switch (slot)
{
case MWWorld::InventoryStore::Slot_Ammunition:
case MWWorld::InventoryStore::Slot_CarriedRight:
case MWWorld::InventoryStore::Slot_Pants:
continue;
case MWWorld::InventoryStore::Slot_CarriedLeft:
{
auto carried = store.getSlot(slot);
if (carried == store.end()
|| carried.getType() != MWWorld::ContainerStore::Type_Armor)
continue;
[[fallthrough]];
}
default:
store.unequipSlot(slot);
}
}
}
}
else
invalid = true;
@ -1083,7 +1106,7 @@ namespace MWMechanics
}
break;
case ESM::MagicEffect::ExtraSpell:
if (magnitudes.getOrDefault(effect.mEffectId).getMagnitude() <= 0.f)
if (magnitudes.getOrDefault(effect.mEffectId).getMagnitude() <= 0.f && target != getPlayer())
target.getClass().getInventoryStore(target).autoEquip();
break;
case ESM::MagicEffect::TurnUndead:

View file

@ -5,7 +5,10 @@
#include <components/debug/debuglog.hpp>
#include <components/esm3/loadsoun.hpp>
#include <components/esm4/loadsndr.hpp>
#include <components/esm4/loadsoun.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/settings/values.hpp>
#include <components/vfs/pathutil.hpp>
@ -99,7 +102,12 @@ namespace MWSound
{
if (mBufferNameMap.empty())
{
for (const ESM::Sound& sound : MWBase::Environment::get().getESMStore()->get<ESM::Sound>())
const MWWorld::ESMStore* esmstore = MWBase::Environment::get().getESMStore();
for (const ESM::Sound& sound : esmstore->get<ESM::Sound>())
insertSound(sound.mId, sound);
for (const ESM4::Sound& sound : esmstore->get<ESM4::Sound>())
insertSound(sound.mId, sound);
for (const ESM4::SoundReference& sound : esmstore->get<ESM4::SoundReference>())
insertSound(sound.mId, sound);
}
@ -190,6 +198,28 @@ namespace MWSound
return &sfx;
}
SoundBuffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM4::Sound& sound)
{
std::string path = Misc::ResourceHelpers::correctResourcePath(
{ { "sound" } }, sound.mSoundFile, MWBase::Environment::get().getResourceSystem()->getVFS(), ".mp3");
float volume = 1, min = 1, max = 255; // TODO: needs research
SoundBuffer& sfx = mSoundBuffers.emplace_back(VFS::Path::Normalized(std::move(path)), volume, min, max);
mBufferNameMap.emplace(soundId, &sfx);
return &sfx;
}
SoundBuffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM4::SoundReference& sound)
{
std::string path = Misc::ResourceHelpers::correctResourcePath(
{ { "sound" } }, sound.mSoundFile, MWBase::Environment::get().getResourceSystem()->getVFS(), ".mp3");
float volume = 1, min = 1, max = 255; // TODO: needs research
// TODO: sound.mSoundId can link to another SoundReference, probably we will need to add additional lookups to
// ESMStore.
SoundBuffer& sfx = mSoundBuffers.emplace_back(VFS::Path::Normalized(std::move(path)), volume, min, max);
mBufferNameMap.emplace(soundId, &sfx);
return &sfx;
}
void SoundBufferPool::unloadUnused()
{
while (!mUnusedBuffers.empty() && mBufferCacheSize > mBufferCacheMin)

View file

@ -15,6 +15,12 @@ namespace ESM
struct Sound;
}
namespace ESM4
{
struct Sound;
struct SoundReference;
}
namespace VFS
{
class Manager;
@ -112,8 +118,10 @@ namespace MWSound
// NOTE: unused buffers are stored in front-newest order.
std::deque<SoundBuffer*> mUnusedBuffers;
inline SoundBuffer* insertSound(const ESM::RefId& soundId, const ESM::Sound& sound);
inline SoundBuffer* insertSound(std::string_view fileName);
SoundBuffer* insertSound(const ESM::RefId& soundId, const ESM::Sound& sound);
SoundBuffer* insertSound(const ESM::RefId& soundId, const ESM4::Sound& sound);
SoundBuffer* insertSound(const ESM::RefId& soundId, const ESM4::SoundReference& sound);
SoundBuffer* insertSound(std::string_view fileName);
inline void unloadUnused();
};

View file

@ -105,6 +105,8 @@ namespace ESM4
struct Potion;
struct Race;
struct Reference;
struct Sound;
struct SoundReference;
struct Static;
struct StaticCollection;
struct Terminal;
@ -146,8 +148,8 @@ namespace MWWorld
Store<ESM4::Land>, Store<ESM4::LandTexture>, Store<ESM4::LevelledCreature>, Store<ESM4::LevelledItem>,
Store<ESM4::LevelledNpc>, Store<ESM4::Light>, Store<ESM4::MiscItem>, Store<ESM4::MovableStatic>,
Store<ESM4::Npc>, Store<ESM4::Outfit>, Store<ESM4::Potion>, Store<ESM4::Race>, Store<ESM4::Reference>,
Store<ESM4::Static>, Store<ESM4::StaticCollection>, Store<ESM4::Terminal>, Store<ESM4::Tree>,
Store<ESM4::Weapon>, Store<ESM4::World>>;
Store<ESM4::Sound>, Store<ESM4::SoundReference>, Store<ESM4::Static>, Store<ESM4::StaticCollection>,
Store<ESM4::Terminal>, Store<ESM4::Tree>, Store<ESM4::Weapon>, Store<ESM4::World>>;
private:
template <typename T>

View file

@ -378,110 +378,111 @@ void MWWorld::InventoryStore::autoEquipWeapon(TSlots& slots_)
void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_)
{
// Only NPCs can wear armor for now.
// For creatures we equip only shields.
const Ptr& actor = getPtr();
if (!actor.getClass().isNpc())
// Creatures only want shields and don't benefit from armor rating or unarmored skill
const MWWorld::Class& actorCls = actor.getClass();
const bool actorIsNpc = actorCls.isNpc();
int equipmentTypes = ContainerStore::Type_Armor;
float unarmoredRating = 0.f;
if (actorIsNpc)
{
autoEquipShield(slots_);
return;
equipmentTypes |= ContainerStore::Type_Clothing;
const auto& store = MWBase::Environment::get().getESMStore()->get<ESM::GameSetting>();
const float fUnarmoredBase1 = store.find("fUnarmoredBase1")->mValue.getFloat();
const float fUnarmoredBase2 = store.find("fUnarmoredBase2")->mValue.getFloat();
const float unarmoredSkill = actorCls.getSkill(actor, ESM::Skill::Unarmored);
unarmoredRating = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill);
unarmoredRating = std::max(unarmoredRating, 0.f);
}
const MWWorld::Store<ESM::GameSetting>& store = MWBase::Environment::get().getESMStore()->get<ESM::GameSetting>();
static float fUnarmoredBase1 = store.find("fUnarmoredBase1")->mValue.getFloat();
static float fUnarmoredBase2 = store.find("fUnarmoredBase2")->mValue.getFloat();
float unarmoredSkill = actor.getClass().getSkill(actor, ESM::Skill::Unarmored);
float unarmoredRating = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill);
for (ContainerStoreIterator iter(begin(ContainerStore::Type_Clothing | ContainerStore::Type_Armor)); iter != end();
++iter)
for (ContainerStoreIterator iter(begin(equipmentTypes)); iter != end(); ++iter)
{
Ptr test = *iter;
const MWWorld::Class& testCls = test.getClass();
const bool isArmor = iter.getType() == ContainerStore::Type_Armor;
switch (test.getClass().canBeEquipped(test, actor).first)
// Discard armor that is worse than unarmored for NPCs and non-shields for creatures
if (isArmor)
{
case 0:
continue;
default:
break;
if (actorIsNpc)
{
if (testCls.getEffectiveArmorRating(test, actor) <= unarmoredRating)
continue;
}
else
{
if (test.get<ESM::Armor>()->mBase->mData.mType != ESM::Armor::Shield)
continue;
}
}
if (iter.getType() == ContainerStore::Type_Armor
&& test.getClass().getEffectiveArmorRating(test, actor) <= std::max(unarmoredRating, 0.f))
{
// Don't equip the item if it cannot be equipped
if (testCls.canBeEquipped(test, actor).first == 0)
continue;
}
std::pair<std::vector<int>, bool> itemsSlots = iter->getClass().getEquipmentSlots(*iter);
const auto [itemSlots, canStack] = testCls.getEquipmentSlots(test);
// checking if current item pointed by iter can be equipped
for (int slot : itemsSlots.first)
for (const int slot : itemSlots)
{
// if true then it means slot is equipped already
// check if slot may require swapping if current item is more valuable
if (slots_.at(slot) != end())
{
Ptr old = *slots_.at(slot);
const MWWorld::Class& oldCls = old.getClass();
unsigned int oldType = old.getType();
if (iter.getType() == ContainerStore::Type_Armor)
if (!isArmor)
{
if (old.getType() == ESM::Armor::sRecordId)
{
if (old.get<ESM::Armor>()->mBase->mData.mType < test.get<ESM::Armor>()->mBase->mData.mType)
continue;
// Armor should replace clothing and weapons, but clothing should only replace clothing
if (oldType != ESM::Clothing::sRecordId)
continue;
if (old.get<ESM::Armor>()->mBase->mData.mType == test.get<ESM::Armor>()->mBase->mData.mType)
{
if (old.getClass().getEffectiveArmorRating(old, actor)
>= test.getClass().getEffectiveArmorRating(test, actor))
// old armor had better armor rating
continue;
}
}
// suitable armor should replace already equipped clothing
}
else if (iter.getType() == ContainerStore::Type_Clothing)
{
// if left ring is equipped
// If the left ring slot is filled, don't swap if the right ring is cheaper
if (slot == Slot_LeftRing)
{
// if there is a place for right ring dont swap it
if (slots_.at(Slot_RightRing) == end())
{
continue;
}
else // if right ring is equipped too
{
Ptr rightRing = *slots_.at(Slot_RightRing);
// we want to swap cheaper ring only if both are equipped
if (old.getClass().getValue(old) >= rightRing.getClass().getValue(rightRing))
Ptr rightRing = *slots_.at(Slot_RightRing);
if (rightRing.getClass().getValue(rightRing) <= oldCls.getValue(old))
continue;
}
if (testCls.getValue(test) <= oldCls.getValue(old))
continue;
}
else if (oldType == ESM::Armor::sRecordId)
{
const int32_t oldArmorType = old.get<ESM::Armor>()->mBase->mData.mType;
const int32_t newArmorType = test.get<ESM::Armor>()->mBase->mData.mType;
if (oldArmorType == newArmorType)
{
// For NPCs, compare armor rating; for creatures, compare condition
if (actorIsNpc)
{
const float rating = testCls.getEffectiveArmorRating(test, actor);
const float oldRating = oldCls.getEffectiveArmorRating(old, actor);
if (rating <= oldRating)
continue;
}
else
{
if (testCls.getItemHealth(test) <= oldCls.getItemHealth(old))
continue;
}
}
if (old.getType() == ESM::Clothing::sRecordId)
{
// check value
if (old.getClass().getValue(old) >= test.getClass().getValue(test))
// old clothing was more valuable
continue;
}
else
// suitable clothing should NOT replace already equipped armor
else if (oldArmorType < newArmorType)
continue;
}
}
if (!itemsSlots.second) // if itemsSlots.second is true, item can stay stacked when equipped
// unstack the item if required
if (!canStack && test.getCellRef().getCount() > 1)
{
// unstack item pointed to by iterator if required
if (iter->getCellRef().getCount() > 1)
{
unstack(*iter);
}
unstack(test);
}
// if we are here it means item can be equipped or swapped
@ -491,27 +492,6 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_)
}
}
void MWWorld::InventoryStore::autoEquipShield(TSlots& slots_)
{
for (ContainerStoreIterator iter(begin(ContainerStore::Type_Armor)); iter != end(); ++iter)
{
if (iter->get<ESM::Armor>()->mBase->mData.mType != ESM::Armor::Shield)
continue;
if (iter->getClass().canBeEquipped(*iter, getPtr()).first != 1)
continue;
std::pair<std::vector<int>, bool> shieldSlots = iter->getClass().getEquipmentSlots(*iter);
int slot = shieldSlots.first[0];
const ContainerStoreIterator& shield = slots_[slot];
if (shield != end() && shield.getType() == Type_Armor
&& shield->get<ESM::Armor>()->mBase->mData.mType == ESM::Armor::Shield)
{
if (shield->getClass().getItemHealth(*shield) >= iter->getClass().getItemHealth(*iter))
continue;
}
slots_[slot] = iter;
}
}
void MWWorld::InventoryStore::autoEquip()
{
TSlots slots_;
@ -522,8 +502,6 @@ void MWWorld::InventoryStore::autoEquip()
// Autoequip clothing, armor and weapons.
// Equipping lights is handled in Actors::updateEquippedLight based on environment light.
// Note: creatures ignore equipment armor rating and only equip shields
// Use custom logic for them - select shield based on its health instead of armor rating
autoEquipWeapon(slots_);
autoEquipArmor(slots_);

View file

@ -69,7 +69,6 @@ namespace MWWorld
void autoEquipWeapon(TSlots& slots_);
void autoEquipArmor(TSlots& slots_);
void autoEquipShield(TSlots& slots_);
// selected magic item (for using enchantments of type "Cast once" or "Cast when used")
ContainerStoreIterator mSelectedEnchantItem;

View file

@ -1349,6 +1349,8 @@ template class MWWorld::TypedDynamicStore<ESM4::Npc>;
template class MWWorld::TypedDynamicStore<ESM4::Outfit>;
template class MWWorld::TypedDynamicStore<ESM4::Potion>;
template class MWWorld::TypedDynamicStore<ESM4::Race>;
template class MWWorld::TypedDynamicStore<ESM4::Sound>;
template class MWWorld::TypedDynamicStore<ESM4::SoundReference>;
template class MWWorld::TypedDynamicStore<ESM4::Static>;
template class MWWorld::TypedDynamicStore<ESM4::StaticCollection>;
template class MWWorld::TypedDynamicStore<ESM4::Terminal>;

View file

@ -61,6 +61,7 @@ add_component_dir (lua
luastate scriptscontainer asyncpackage utilpackage serialization configuration l10n storage utf8
shapes/box inputactions yamlloader scripttracker luastateptr
)
copy_resource_file("lua/util.lua" "${OPENMW_RESOURCES_ROOT}" "resources/lua_libs/util.lua")
add_component_dir (l10n
messagebundles manager

View file

@ -74,6 +74,8 @@
#include <components/esm4/loadrace.hpp>
#include <components/esm4/loadrefr.hpp>
#include <components/esm4/loadscol.hpp>
#include <components/esm4/loadsndr.hpp>
#include <components/esm4/loadsoun.hpp>
#include <components/esm4/loadstat.hpp>
#include <components/esm4/loadterm.hpp>
#include <components/esm4/loadtree.hpp>

21
components/lua/util.lua Normal file
View file

@ -0,0 +1,21 @@
local M = {}
function M.remap(value, min, max, newMin, newMax)
return newMin + (value - min) * (newMax - newMin) / (max - min)
end
function M.round(value)
return value >= 0 and math.floor(value + 0.5) or math.ceil(value - 0.5)
end
function M.clamp(value, low, high)
return value < low and low or (value > high and high or value)
end
function M.normalizeAngle(angle)
local fullTurns = angle / (2 * math.pi) + 0.5
return (fullTurns - math.floor(fullTurns) - 0.5) * (2 * math.pi)
end
return M

View file

@ -352,16 +352,14 @@ namespace LuaUtil
return std::make_tuple(angles.z(), angles.y(), angles.x());
};
sol::function luaUtilLoader = lua["loadInternalLib"]("util");
sol::table utils = luaUtilLoader();
for (const auto& [key, value] : utils)
util[key.as<std::string>()] = value;
// Utility functions
util["clamp"] = [](double value, double from, double to) { return std::clamp(value, from, to); };
// NOTE: `util["clamp"] = std::clamp<float>` causes error 'AddressSanitizer: stack-use-after-scope'
util["normalizeAngle"] = &Misc::normalizeAngle;
util["makeReadOnly"] = [](const sol::table& tbl) { return makeReadOnly(tbl, /*strictIndex=*/false); };
util["makeStrictReadOnly"] = [](const sol::table& tbl) { return makeReadOnly(tbl, /*strictIndex=*/true); };
util["remap"] = [](double value, double min, double max, double newMin, double newMax) {
return newMin + (value - min) * (newMax - newMin) / (max - min);
};
util["round"] = [](double value) { return round(value); };
if (lua["bit32"] != sol::nil)
{

View file

@ -33,14 +33,10 @@ bool Misc::ResourceHelpers::changeExtensionToDds(std::string& path)
return changeExtension(path, ".dds");
}
std::string Misc::ResourceHelpers::correctResourcePath(
std::span<const std::string_view> topLevelDirectories, std::string_view resPath, const VFS::Manager* vfs)
// If `ext` is not empty we first search file with extension `ext`, then if not found fallback to original extension.
std::string Misc::ResourceHelpers::correctResourcePath(std::span<const std::string_view> topLevelDirectories,
std::string_view resPath, const VFS::Manager* vfs, std::string_view ext)
{
/* Bethesda at some point converted all their BSA
* textures from tga to dds for increased load speed, but all
* texture file name references were kept as .tga.
*/
std::string correctedPath = Misc::StringUtils::lowerCase(resPath);
// Flatten slashes
@ -80,14 +76,14 @@ std::string Misc::ResourceHelpers::correctResourcePath(
std::string origExt = correctedPath;
// since we know all (GOTY edition or less) textures end
// in .dds, we change the extension
bool changedToDds = changeExtensionToDds(correctedPath);
// replace extension if `ext` is specified (used for .tga -> .dds, .wav -> .mp3)
bool isExtChanged = !ext.empty() && changeExtension(correctedPath, ext);
if (vfs->exists(correctedPath))
return correctedPath;
// if it turns out that the above wasn't true in all cases (not for vanilla, but maybe mods)
// verify, and revert if false (this call succeeds quickly, but fails slowly)
if (changedToDds && vfs->exists(origExt))
// fall back to original extension
if (isExtChanged && vfs->exists(origExt))
return origExt;
// fall back to a resource in the top level directory if it exists
@ -98,7 +94,7 @@ std::string Misc::ResourceHelpers::correctResourcePath(
if (vfs->exists(fallback))
return fallback;
if (changedToDds)
if (isExtChanged)
{
fallback = topLevelDirectories.front();
fallback += '\\';
@ -110,19 +106,23 @@ std::string Misc::ResourceHelpers::correctResourcePath(
return correctedPath;
}
// Note: Bethesda at some point converted all their BSA textures from tga to dds for increased load speed,
// but all texture file name references were kept as .tga. So we pass ext=".dds" to all helpers
// looking for textures.
std::string Misc::ResourceHelpers::correctTexturePath(std::string_view resPath, const VFS::Manager* vfs)
{
return correctResourcePath({ { "textures", "bookart" } }, resPath, vfs);
return correctResourcePath({ { "textures", "bookart" } }, resPath, vfs, ".dds");
}
std::string Misc::ResourceHelpers::correctIconPath(std::string_view resPath, const VFS::Manager* vfs)
{
return correctResourcePath({ { "icons" } }, resPath, vfs);
return correctResourcePath({ { "icons" } }, resPath, vfs, ".dds");
}
std::string Misc::ResourceHelpers::correctBookartPath(std::string_view resPath, const VFS::Manager* vfs)
{
return correctResourcePath({ { "bookart", "textures" } }, resPath, vfs);
return correctResourcePath({ { "bookart", "textures" } }, resPath, vfs, ".dds");
}
std::string Misc::ResourceHelpers::correctBookartPath(
@ -199,6 +199,12 @@ std::string_view Misc::ResourceHelpers::meshPathForESM3(std::string_view resPath
VFS::Path::Normalized Misc::ResourceHelpers::correctSoundPath(
VFS::Path::NormalizedView resPath, const VFS::Manager& vfs)
{
// Note: likely should be replaced with
// return correctResourcePath({ { "sound" } }, resPath, vfs, ".mp3");
// but there is a slight difference in behaviour:
// - `correctResourcePath(..., ".mp3")` first checks `.mp3`, then tries the original extension
// - the implementation below first tries the original extension, then falls back to `.mp3`.
// Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav.
if (!vfs.exists(resPath))
{

View file

@ -1,12 +1,12 @@
#ifndef MISC_RESOURCEHELPERS_H
#define MISC_RESOURCEHELPERS_H
#include <components/vfs/pathutil.hpp>
#include <span>
#include <string>
#include <string_view>
#include <components/vfs/pathutil.hpp>
namespace VFS
{
class Manager;
@ -25,8 +25,8 @@ namespace Misc
namespace ResourceHelpers
{
bool changeExtensionToDds(std::string& path);
std::string correctResourcePath(
std::span<const std::string_view> topLevelDirectories, std::string_view resPath, const VFS::Manager* vfs);
std::string correctResourcePath(std::span<const std::string_view> topLevelDirectories, std::string_view resPath,
const VFS::Manager* vfs, std::string_view ext = {});
std::string correctTexturePath(std::string_view resPath, const VFS::Manager* vfs);
std::string correctIconPath(std::string_view resPath, const VFS::Manager* vfs);
std::string correctBookartPath(std::string_view resPath, const VFS::Manager* vfs);

View file

@ -1,4 +1,5 @@
local async = require('openmw.async')
local core = require('openmw.core')
local types = require('openmw.types')
local world = require('openmw.world')
@ -6,8 +7,9 @@ local EnableObject = async:registerTimerCallback('EnableObject', function(obj) o
local function ESM4DoorActivation(door, actor)
-- TODO: Implement lockpicking minigame
-- TODO: Play door opening animation and sound
-- TODO: Play door opening animation
local Door4 = types.ESM4Door
core.sound.playSound3d(Door4.record(door).openSound, actor)
if Door4.isTeleport(door) then
actor:teleport(Door4.destCell(door), Door4.destPosition(door), Door4.destRotation(door))
else

View file

@ -2450,5 +2450,7 @@
-- @field #string id Record id
-- @field #string name Human-readable name
-- @field #string model VFS path to the model
-- @field #string openSound FormId of the door opening sound
-- @field #string closeSound FormId of the door closing sound
return nil