diff --git a/CHANGELOG.md b/CHANGELOG.md index cbffc23bc7..e847ec69fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ------ diff --git a/apps/components_tests/lua/testutilpackage.cpp b/apps/components_tests/lua/testutilpackage.cpp index a61c0e0306..47041985f6 100644 --- a/apps/components_tests/lua/testutilpackage.cpp +++ b/apps/components_tests/lua/testutilpackage.cpp @@ -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 - 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(); } - 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(lua, "v.x"), 3); EXPECT_FLOAT_EQ(get(lua, "v.y"), 4); @@ -55,11 +66,9 @@ namespace EXPECT_TRUE(get(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(lua, "v.x"), 5); EXPECT_FLOAT_EQ(get(lua, "v.y"), 12); @@ -94,11 +103,9 @@ namespace get(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(lua, "v.x"), 5); EXPECT_FLOAT_EQ(get(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(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(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(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(lua, "v.x"), -0.5f); EXPECT_FLOAT_EQ(get(lua, "v.y"), 0.86602539f); @@ -203,6 +204,10 @@ namespace EXPECT_FLOAT_EQ(get(lua, "util.clamp(0.1, 0, 1.5)"), 0.1f); EXPECT_FLOAT_EQ(get(lua, "util.clamp(-0.1, 0, 1.5)"), 0); EXPECT_FLOAT_EQ(get(lua, "util.clamp(2.1, 0, 1.5)"), 1.5f); + EXPECT_FLOAT_EQ(get(lua, "util.round(2.1)"), 2.0f); + EXPECT_FLOAT_EQ(get(lua, "util.round(-2.1)"), -2.0f); + EXPECT_FLOAT_EQ(get(lua, "util.remap(5, 0, 10, 0, 100)"), 50.0f); + EXPECT_FLOAT_EQ(get(lua, "util.remap(-5, 0, 10, 0, 100)"), -50.0f); lua.safe_script("t = util.makeReadOnly({x = 1})"); EXPECT_FLOAT_EQ(get(lua, "t.x"), 1); EXPECT_ERROR(lua.safe_script("t.y = 2"), "userdata value"); diff --git a/apps/components_tests/misc/testresourcehelpers.cpp b/apps/components_tests/misc/testresourcehelpers.cpp index 05079ae875..b21ecb2e14 100644 --- a/apps/components_tests/misc/testresourcehelpers.cpp +++ b/apps/components_tests/misc/testresourcehelpers.cpp @@ -26,6 +26,15 @@ namespace std::unique_ptr 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 diff --git a/apps/openmw/mwlua/types/door.cpp b/apps/openmw/mwlua/types/door.cpp index 58a53a7124..5db6f9b875 100644 --- a/apps/openmw/mwlua/types/door.cpp +++ b/apps/openmw/mwlua/types/door.cpp @@ -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(); }); } } diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 91e24f946f..3d1a5ed84e 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -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: diff --git a/apps/openmw/mwsound/soundbuffer.cpp b/apps/openmw/mwsound/soundbuffer.cpp index 722d89f0eb..0c10ba5552 100644 --- a/apps/openmw/mwsound/soundbuffer.cpp +++ b/apps/openmw/mwsound/soundbuffer.cpp @@ -5,7 +5,10 @@ #include #include +#include +#include #include +#include #include #include @@ -99,7 +102,12 @@ namespace MWSound { if (mBufferNameMap.empty()) { - for (const ESM::Sound& sound : MWBase::Environment::get().getESMStore()->get()) + const MWWorld::ESMStore* esmstore = MWBase::Environment::get().getESMStore(); + for (const ESM::Sound& sound : esmstore->get()) + insertSound(sound.mId, sound); + for (const ESM4::Sound& sound : esmstore->get()) + insertSound(sound.mId, sound); + for (const ESM4::SoundReference& sound : esmstore->get()) 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) diff --git a/apps/openmw/mwsound/soundbuffer.hpp b/apps/openmw/mwsound/soundbuffer.hpp index f7e7639b2d..493577a9c3 100644 --- a/apps/openmw/mwsound/soundbuffer.hpp +++ b/apps/openmw/mwsound/soundbuffer.hpp @@ -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 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(); }; diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 6c71ae0052..0c37f243e8 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -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, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, - Store, Store, Store, Store, - Store, Store>; + Store, Store, Store, Store, + Store, Store, Store, Store>; private: template diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index f48f4e6e31..5f7e135847 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -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(); + 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& store = MWBase::Environment::get().getESMStore()->get(); - - 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()->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, 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()->mBase->mData.mType < test.get()->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()->mBase->mData.mType == test.get()->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()->mBase->mData.mType; + const int32_t newArmorType = test.get()->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()->mBase->mData.mType != ESM::Armor::Shield) - continue; - if (iter->getClass().canBeEquipped(*iter, getPtr()).first != 1) - continue; - std::pair, 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()->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_); diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index 0af6ee2b28..4e8f56f616 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -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; diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index b0684b1ab4..80bcdb056a 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -1349,6 +1349,8 @@ template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; +template class MWWorld::TypedDynamicStore; +template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 1426a797f7..c0ecbaf833 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -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 diff --git a/components/esm/records.hpp b/components/esm/records.hpp index 0b60b44cf0..0b76fab0ff 100644 --- a/components/esm/records.hpp +++ b/components/esm/records.hpp @@ -74,6 +74,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/components/lua/util.lua b/components/lua/util.lua new file mode 100644 index 0000000000..f37a2e52cc --- /dev/null +++ b/components/lua/util.lua @@ -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 diff --git a/components/lua/utilpackage.cpp b/components/lua/utilpackage.cpp index 85492ccf06..2b706e1cb8 100644 --- a/components/lua/utilpackage.cpp +++ b/components/lua/utilpackage.cpp @@ -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()] = value; + // Utility functions - util["clamp"] = [](double value, double from, double to) { return std::clamp(value, from, to); }; - // NOTE: `util["clamp"] = std::clamp` 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) { diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index 5279e2ad23..c3164b0dfe 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -33,14 +33,10 @@ bool Misc::ResourceHelpers::changeExtensionToDds(std::string& path) return changeExtension(path, ".dds"); } -std::string Misc::ResourceHelpers::correctResourcePath( - std::span 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 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)) { diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp index 9aaa89a861..fb355d6b94 100644 --- a/components/misc/resourcehelpers.hpp +++ b/components/misc/resourcehelpers.hpp @@ -1,12 +1,12 @@ #ifndef MISC_RESOURCEHELPERS_H #define MISC_RESOURCEHELPERS_H -#include - #include #include #include +#include + namespace VFS { class Manager; @@ -25,8 +25,8 @@ namespace Misc namespace ResourceHelpers { bool changeExtensionToDds(std::string& path); - std::string correctResourcePath( - std::span topLevelDirectories, std::string_view resPath, const VFS::Manager* vfs); + std::string correctResourcePath(std::span 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); diff --git a/files/data/scripts/omw/activationhandlers.lua b/files/data/scripts/omw/activationhandlers.lua index 419ffe6809..3850b207eb 100644 --- a/files/data/scripts/omw/activationhandlers.lua +++ b/files/data/scripts/omw/activationhandlers.lua @@ -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 diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index 6cef253268..2481c0ae75 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -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