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:
commit
d46f034e5d
19 changed files with 293 additions and 156 deletions
45
CHANGELOG.md
45
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
|
||||
------
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(); });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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_);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
21
components/lua/util.lua
Normal 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
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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))
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue