diff --git a/CHANGELOG.md b/CHANGELOG.md index 830ab93094..e18045aeb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.51.0 +------ + + 0.50.0 ------ @@ -71,6 +75,7 @@ Bug #8650: Some plants turn invisible when being called types.Container.inventory(cont):isResolved() Bug #8680: Dead ancestor ghosts stop being dust when you rest near them Bug #8686: openmw-cs: Crash when smoothing terrain of a not-yet-created cell. + Bug #8710: Absorb Skill breaks on creatures Feature #2522: Support quick item transfer Feature #3740: Gamepad GUI Mode Feature #3769: Allow GetSpellEffects on enchantments diff --git a/CMakeLists.txt b/CMakeLists.txt index 2efc8ac522..dc7420caa4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,9 +80,9 @@ endif() message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) -set(OPENMW_VERSION_MINOR 50) +set(OPENMW_VERSION_MINOR 51) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 95) +set(OPENMW_LUA_API_REVISION 96) set(OPENMW_POSTPROCESSING_API_REVISION 3) set(OPENMW_VERSION_COMMITHASH "") diff --git a/README.md b/README.md index f65f74e9d8..3b79c5ecef 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ OpenMW is an open-source open-world RPG game engine that supports playing Morrow OpenMW also comes with OpenMW-CS, a replacement for Bethesda's Construction Set. -* Version: 0.50.0 +* Version: 0.51.0 * License: GPLv3 (see [LICENSE](https://gitlab.com/OpenMW/openmw/-/raw/master/LICENSE) for more information) * Website: https://www.openmw.org * IRC: #openmw on irc.libera.chat diff --git a/apps/components_tests/CMakeLists.txt b/apps/components_tests/CMakeLists.txt index 7595681313..890998a32c 100644 --- a/apps/components_tests/CMakeLists.txt +++ b/apps/components_tests/CMakeLists.txt @@ -84,6 +84,7 @@ file(GLOB UNITTEST_SRC_FILES esmterrain/testgridsampling.cpp resource/testobjectcache.cpp + resource/testresourcesystem.cpp vfs/testpathutil.cpp diff --git a/apps/components_tests/resource/testresourcesystem.cpp b/apps/components_tests/resource/testresourcesystem.cpp new file mode 100644 index 0000000000..c863ba72e9 --- /dev/null +++ b/apps/components_tests/resource/testresourcesystem.cpp @@ -0,0 +1,32 @@ +#include +#include +#include +#include + +#include +#include + +#include + +namespace +{ + using namespace testing; + + TEST(ResourceResourceSystem, scenemanager_getinstance_should_be_thread_safe) + { + const VFS::Manager vfsManager; + const ToUTF8::Utf8Encoder encoder(ToUTF8::WINDOWS_1252); + Resource::ResourceSystem resourceSystem(&vfsManager, 1.0, &encoder.getStatelessEncoder()); + Resource::SceneManager* sceneManager = resourceSystem.getSceneManager(); + + constexpr VFS::Path::NormalizedView noSuchPath("meshes/whatever.nif"); + std::vector threads; + + for (int i = 0; i < 50; ++i) + { + threads.emplace_back([=]() { sceneManager->getInstance(noSuchPath); }); + } + for (std::thread& thread : threads) + thread.join(); + } +} diff --git a/apps/openmw/mwclass/lockpick.cpp b/apps/openmw/mwclass/lockpick.cpp index 1c78b3dfef..a8bf90ff03 100644 --- a/apps/openmw/mwclass/lockpick.cpp +++ b/apps/openmw/mwclass/lockpick.cpp @@ -146,6 +146,7 @@ namespace MWClass { // Do not allow equip tools from inventory during attack if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc) + && !MWBase::Environment::get().getMechanicsManager()->isCastingSpell(npc) && MWBase::Environment::get().getWindowManager()->isGuiMode()) return { 0, "#{sCantEquipWeapWarning}" }; diff --git a/apps/openmw/mwclass/probe.cpp b/apps/openmw/mwclass/probe.cpp index 110614bffd..a46a2aefee 100644 --- a/apps/openmw/mwclass/probe.cpp +++ b/apps/openmw/mwclass/probe.cpp @@ -144,6 +144,7 @@ namespace MWClass { // Do not allow equip tools from inventory during attack if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc) + && !MWBase::Environment::get().getMechanicsManager()->isCastingSpell(npc) && MWBase::Environment::get().getWindowManager()->isGuiMode()) return { 0, "#{sCantEquipWeapWarning}" }; diff --git a/apps/openmw/mwclass/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index 41fd745459..475505dca1 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -270,10 +270,25 @@ namespace MWClass std::pair Weapon::canBeEquipped(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const { + int type = ptr.get()->mBase->mData.mType; + // Do not allow equip weapons from inventory during attack if (npc.isInCell() && MWBase::Environment::get().getWindowManager()->isGuiMode() && MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc)) - return { 0, "#{sCantEquipWeapWarning}" }; + { + int activeWeaponType = ESM::Weapon::None; + MWMechanics::getActiveWeapon(npc, &activeWeaponType); + if (activeWeaponType > ESM::Weapon::None || activeWeaponType == ESM::Weapon::HandToHand) + { + auto* activeWeapon = MWMechanics::getWeaponType(activeWeaponType); + bool isAmmo = MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Class::Ammo; + bool activeWeapUsesAmmo = activeWeapon->mWeaponClass == ESM::WeaponType::Class::Ranged; + bool sameAmmoType = activeWeapon->mAmmoType == type; + // special case for ammo equipping + if ((activeWeapUsesAmmo && !sameAmmoType) || !isAmmo) + return { 0, "#{sCantEquipWeapWarning}" }; + } + } if (hasItemHealth(ptr) && getItemHealth(ptr) == 0) return { 0, "#{sInventoryMessage1}" }; @@ -283,7 +298,6 @@ namespace MWClass if (slots.first.empty()) return { 0, {} }; - int type = ptr.get()->mBase->mData.mType; if (MWMechanics::getWeaponType(type)->mFlags & ESM::WeaponType::TwoHanded) { return { 2, {} }; diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index e041971cfe..6b9deffbf9 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -325,10 +325,10 @@ namespace MWGui // If we unequip weapon during attack, it can lead to unexpected behaviour if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(mPtr)) { - bool isWeapon = item.mBase.getType() == ESM::Weapon::sRecordId; MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); - - if (isWeapon && invStore.isEquipped(item.mBase)) + MWWorld::ContainerStoreIterator weapIt = invStore.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + bool weapActive = mPtr.getClass().getCreatureStats(mPtr).getDrawState() == MWMechanics::DrawState::Weapon; + if (weapActive && weapIt != invStore.end() && *weapIt == item.mBase) { MWBase::Environment::get().getWindowManager()->messageBox("#{sCantEquipWeapWarning}"); return; @@ -756,6 +756,8 @@ namespace MWGui void InventoryWindow::onFrame(float dt) { + updateEncumbranceBar(); + if (mUpdateNextFrame) { if (mTrading) @@ -766,7 +768,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->getTradeWindow()->updateOffer(); } - updateEncumbranceBar(); mDragAndDrop->update(); mItemView->update(); notifyContentChanged(); @@ -969,25 +970,25 @@ namespace MWGui mControllerButtons.mA = "#{OMWEngine:InventorySelect}"; mControllerButtons.mB = "#{Interface:Close}"; mControllerButtons.mX.clear(); - mControllerButtons.mR2 = "#{sCompanionShare}"; + mControllerButtons.mR2 = "#{Interface:Share}"; break; case MWGui::GM_Container: mControllerButtons.mA = "#{OMWEngine:InventorySelect}"; mControllerButtons.mB = "#{Interface:Close}"; mControllerButtons.mX = "#{Interface:TakeAll}"; - mControllerButtons.mR2 = "#{sContainer}"; + mControllerButtons.mR2 = "#{Interface:Container}"; break; case MWGui::GM_Barter: - mControllerButtons.mA = "#{sSell}"; + mControllerButtons.mA = "#{Interface:Sell}"; mControllerButtons.mB = "#{Interface:Cancel}"; mControllerButtons.mX = "#{Interface:Offer}"; - mControllerButtons.mR2 = "#{sBarter}"; + mControllerButtons.mR2 = "#{Interface:Barter}"; break; case MWGui::GM_Inventory: default: - mControllerButtons.mA = "#{sEquip}"; + mControllerButtons.mA = "#{Interface:Equip}"; mControllerButtons.mB = "#{Interface:Back}"; - mControllerButtons.mX = "#{sDrop}"; + mControllerButtons.mX = "#{Interface:Drop}"; mControllerButtons.mR2.clear(); break; } diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index 85aadb2079..49e98f816d 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -836,9 +836,10 @@ namespace MWGui if (Settings::gui().mControllerMenus) { mControllerButtons.mB = "#{Interface:Back}"; - mControllerButtons.mX = global ? "#{sLocal}" : "#{sWorld}"; - mControllerButtons.mY = "#{sCenter}"; - mControllerButtons.mDpad = Settings::map().mAllowZooming ? "" : "#{sMove}"; + mControllerButtons.mX = global ? "#{Interface:Local}" : "#{Interface:World}"; + mControllerButtons.mY = "#{Interface:Center}"; + if (!Settings::map().mAllowZooming) + mControllerButtons.mDpad = "#{Interface:Move}"; } } @@ -1233,7 +1234,7 @@ namespace MWGui mLocalMap->setVisible(!global); mButton->setCaptionWithReplacing(global ? "#{sLocal}" : "#{sWorld}"); - mControllerButtons.mX = global ? "#{sLocal}" : "#{sWorld}"; + mControllerButtons.mX = global ? "#{Interface:Local}" : "#{Interface:World}"; MWBase::Environment::get().getWindowManager()->updateControllerButtonsOverlay(); } @@ -1537,7 +1538,10 @@ namespace MWGui ControllerButtons* EditNoteDialog::getControllerButtons() { - mControllerButtons.mX = getDeleteButtonShown() ? "#{sDelete}" : ""; + if (getDeleteButtonShown()) + mControllerButtons.mX = "#{Interface:Delete}"; + else + mControllerButtons.mX.clear(); return &mControllerButtons; } diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index a6f0683cb4..32754d9b2a 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -111,9 +111,9 @@ namespace MWGui mControllerButtons.mLStick = "#{Interface:Mouse}"; mControllerButtons.mA = "#{Interface:Select}"; mControllerButtons.mB = "#{Interface:Back}"; - mControllerButtons.mY = "#{sSex}"; - mControllerButtons.mL1 = "#{sHair}"; - mControllerButtons.mR1 = "#{sFace}"; + mControllerButtons.mY = "#{Interface:Sex}"; + mControllerButtons.mL1 = "#{Interface:Hair}"; + mControllerButtons.mR1 = "#{Interface:Face}"; } updateRaces(); diff --git a/apps/openmw/mwinput/controllermanager.cpp b/apps/openmw/mwinput/controllermanager.cpp index b88c17edda..c1f73ae7c9 100644 --- a/apps/openmw/mwinput/controllermanager.cpp +++ b/apps/openmw/mwinput/controllermanager.cpp @@ -215,6 +215,12 @@ namespace MWInput void ControllerManager::axisMoved(int deviceID, const SDL_ControllerAxisEvent& arg) { + if (mBindingsManager->isDetectingBindingState()) + { + mBindingsManager->controllerAxisMoved(deviceID, arg); + return; + } + if (!Settings::input().mEnableController || MWBase::Environment::get().getInputManager()->controlsDisabled()) return; diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 23c11f81fd..28416a10ba 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -42,6 +42,20 @@ namespace MWLua { + namespace + { + struct BoolScopeGuard + { + bool& mValue; + BoolScopeGuard(bool& value) + : mValue(value) + { + mValue = true; + } + + ~BoolScopeGuard() { mValue = false; } + }; + } static LuaUtil::LuaStateSettings createLuaStateSettings() { @@ -264,31 +278,33 @@ namespace MWLua // can teleport the player to the starting location before the first frame is rendered. mGlobalScripts.newGameStarted(); } + BoolScopeGuard updateGuard(mRunningSynchronizedUpdates); - // We apply input events in `synchronizedUpdate` rather than in `update` in order to reduce input latency. - mProcessingInputEvents = true; + MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); PlayerScripts* playerScripts = mPlayer.isEmpty() ? nullptr : dynamic_cast(mPlayer.getRefData().getLuaScripts()); - MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); - - for (const auto& event : mMenuInputEvents) - mMenuScripts.processInputEvent(event); - mMenuInputEvents.clear(); - if (playerScripts && !windowManager->containsMode(MWGui::GM_MainMenu)) + // We apply input events in `synchronizedUpdate` rather than in `update` in order to reduce input latency. { - for (const auto& event : mInputEvents) - playerScripts->processInputEvent(event); + BoolScopeGuard processingGuard(mProcessingInputEvents); + + for (const auto& event : mMenuInputEvents) + mMenuScripts.processInputEvent(event); + mMenuInputEvents.clear(); + if (playerScripts && !windowManager->containsMode(MWGui::GM_MainMenu)) + { + for (const auto& event : mInputEvents) + playerScripts->processInputEvent(event); + } + mInputEvents.clear(); + mLuaEvents.callMenuEventHandlers(); + float frameDuration = MWBase::Environment::get().getWorld()->getTimeManager()->isPaused() + ? 0.f + : MWBase::Environment::get().getFrameDuration(); + mInputActions.update(frameDuration); + mMenuScripts.onFrame(frameDuration); + if (playerScripts) + playerScripts->onFrame(frameDuration); } - mInputEvents.clear(); - mLuaEvents.callMenuEventHandlers(); - float frameDuration = MWBase::Environment::get().getWorld()->getTimeManager()->isPaused() - ? 0.f - : MWBase::Environment::get().getFrameDuration(); - mInputActions.update(frameDuration); - mMenuScripts.onFrame(frameDuration); - if (playerScripts) - playerScripts->onFrame(frameDuration); - mProcessingInputEvents = false; for (const auto& [message, mode] : mUIMessages) windowManager->messageBox(message, mode); @@ -316,7 +332,7 @@ namespace MWLua void LuaManager::applyDelayedActions() { - mApplyingDelayedActions = true; + BoolScopeGuard applyingGuard(mApplyingDelayedActions); for (DelayedAction& action : mActionQueue) action.apply(); mActionQueue.clear(); @@ -324,7 +340,6 @@ namespace MWLua if (mTeleportPlayerAction) mTeleportPlayerAction->apply(); mTeleportPlayerAction.reset(); - mApplyingDelayedActions = false; } void LuaManager::clear() diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 42b18d236f..80c3163c80 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -125,7 +125,7 @@ namespace MWLua // Some changes to the game world can not be done from the scripting thread (because it runs in parallel with // OSG Cull), so we need to queue it and apply from the main thread. - void addAction(std::function action, std::string_view name = ""); + void addAction(std::function action, std::string_view name = {}); void addTeleportPlayerAction(std::function action); // Saving @@ -174,6 +174,8 @@ namespace MWLua void sendLocalEvent( const MWWorld::Ptr& target, const std::string& name, const std::optional& data = std::nullopt); + bool isSynchronizedUpdateRunning() const { return mRunningSynchronizedUpdates; } + private: void initConfiguration(); LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, @@ -187,6 +189,7 @@ namespace MWLua bool mApplyingDelayedActions = false; bool mNewGameStarted = false; bool mReloadAllScriptsRequested = false; + bool mRunningSynchronizedUpdates = false; LuaUtil::ScriptsConfiguration mConfiguration; LuaUtil::LuaState mLua; LuaUi::ResourceManager mUiResourceManager; diff --git a/apps/openmw/mwlua/menuscripts.cpp b/apps/openmw/mwlua/menuscripts.cpp index 6387a3dce1..c9134201d3 100644 --- a/apps/openmw/mwlua/menuscripts.cpp +++ b/apps/openmw/mwlua/menuscripts.cpp @@ -8,6 +8,7 @@ #include "../mwstate/character.hpp" #include "context.hpp" +#include "luamanagerimp.hpp" namespace MWLua { @@ -72,7 +73,9 @@ namespace MWLua return sol::nullopt; }; - api["saveGame"] = [](std::string_view description, sol::optional slotName) { + api["saveGame"] = [context](std::string_view description, sol::optional slotName) { + if (!context.mLuaManager->isSynchronizedUpdateRunning()) + throw std::runtime_error("menu.saveGame can only be used during engine or event handler processing"); MWBase::StateManager* manager = MWBase::Environment::get().getStateManager(); const MWState::Character* character = manager->getCurrentCharacter(); const MWState::Slot* slot = nullptr; diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 2930c2dfbc..b282999e11 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -851,7 +851,7 @@ namespace MWMechanics else if (!godmode) { damageSkill(target, effect, effect.mMagnitude); - if (!caster.isEmpty()) + if (!caster.isEmpty() && caster.getClass().isNpc()) fortifySkill(caster, effect, effect.mMagnitude); } break; @@ -1303,7 +1303,7 @@ namespace MWMechanics { const auto caster = world->searchPtrViaActorId(spellParams.getCasterActorId()); restoreSkill(target, effect, effect.mMagnitude); - if (!caster.isEmpty()) + if (!caster.isEmpty() && caster.getClass().isNpc()) fortifySkill(caster, effect, -effect.mMagnitude); } break; diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 3fb559c568..549bacb887 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -659,15 +659,13 @@ void MWState::StateManager::loadGame(const Character* character, const std::file { const char* release; // Report the last version still capable of reading this save - if (e.getFormatVersion() <= ESM::OpenMW0_48SaveGameFormatVersion) + if (e.getFormatVersion() < ESM::OpenMW0_49MinSaveGameFormatVersion) release = "OpenMW 0.48.0"; - else if (e.getFormatVersion() <= ESM::OpenMW0_49SaveGameFormatVersion) - release = "OpenMW 0.49.0"; else { // Insert additional else if statements above to cover future releases - static_assert(ESM::MinSupportedSaveGameFormatVersion <= ESM::OpenMW0_50SaveGameFormatVersion); - release = "OpenMW 0.50.0"; + static_assert(ESM::MinSupportedSaveGameFormatVersion <= ESM::OpenMW0_49MinSaveGameFormatVersion); + release = "OpenMW 0.51.0"; } auto l10n = MWBase::Environment::get().getL10nManager()->getContext("OMWEngine"); std::string error = l10n->formatMessage("LoadingRequiresOldVersionError", { "version" }, { release }); diff --git a/components/esm3/formatversion.hpp b/components/esm3/formatversion.hpp index c205f2fbb7..f25cdca4bf 100644 --- a/components/esm3/formatversion.hpp +++ b/components/esm3/formatversion.hpp @@ -31,9 +31,7 @@ namespace ESM inline constexpr FormatVersion CurrentSaveGameFormatVersion = 34; inline constexpr FormatVersion MinSupportedSaveGameFormatVersion = 5; - inline constexpr FormatVersion OpenMW0_48SaveGameFormatVersion = 21; - inline constexpr FormatVersion OpenMW0_49SaveGameFormatVersion = 34; - inline constexpr FormatVersion OpenMW0_50SaveGameFormatVersion = CurrentSaveGameFormatVersion; + inline constexpr FormatVersion OpenMW0_49MinSaveGameFormatVersion = 5; } #endif diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index 538ed48929..663aca8842 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -47,7 +47,7 @@ namespace LuaUtil } } - void ScriptsContainer::printError(int scriptId, std::string_view msg, const std::exception& e) + void ScriptsContainer::printError(int scriptId, std::string_view msg, const std::exception& e) const { Log(Debug::Error) << mNamePrefix << "[" << scriptPath(scriptId) << "] " << msg << ": " << e.what(); } @@ -408,7 +408,7 @@ namespace LuaUtil void ScriptsContainer::save(ESM::LuaScripts& data) { - if (UnloadedData* unloadedData = std::get_if(&mData)) + if (const UnloadedData* unloadedData = std::get_if(&mData)) { data.mScripts = unloadedData->mScripts; return; diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 8eaaf2955f..275c300ac9 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -271,7 +271,7 @@ namespace LuaUtil // Returns script by id (throws an exception if doesn't exist) Script& getScript(int scriptId); - void printError(int scriptId, std::string_view msg, const std::exception& e); + void printError(int scriptId, std::string_view msg, const std::exception& e) const; const VFS::Path::Normalized& scriptPath(int scriptId) const { diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index a2722c1b8f..687d0730b3 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -444,15 +444,6 @@ namespace Resource Resource::NifFileManager* nifFileManager, Resource::BgsmFileManager* bgsmFileManager, double expiryDelay) : ResourceManager(vfs, expiryDelay) , mShaderManager(new Shader::ShaderManager) - , mForceShaders(false) - , mClampLighting(true) - , mAutoUseNormalMaps(false) - , mAutoUseSpecularMaps(false) - , mApplyLightingToEnvMaps(false) - , mLightingMethod(SceneUtil::LightingMethod::FFP) - , mConvertAlphaTestToAlphaToCoverage(false) - , mAdjustCoverageForAlphaTest(false) - , mSupportsNormalsRT(false) , mSharedStateManager(new SharedStateManager) , mImageManager(imageManager) , mNifFileManager(nifFileManager) @@ -460,8 +451,8 @@ namespace Resource , mMinFilter(osg::Texture::LINEAR_MIPMAP_LINEAR) , mMagFilter(osg::Texture::LINEAR) , mMaxAnisotropy(1.f) - , mUnRefImageDataAfterApply(false) , mParticleSystemMask(~0u) + , mLightingMethod(SceneUtil::LightingMethod::FFP) { } @@ -986,8 +977,7 @@ namespace Resource osg::ref_ptr SceneManager::cloneErrorMarker() { - if (!mErrorMarker) - mErrorMarker = loadErrorMarker(); + std::call_once(mErrorMarkerFlag, [this] { mErrorMarker = loadErrorMarker(); }); return static_cast(mErrorMarker->clone(osg::CopyOp::DEEP_COPY_ALL)); } diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index d0769b2b02..aabfc2ee6d 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -237,42 +237,43 @@ namespace Resource osg::ref_ptr loadErrorMarker(); osg::ref_ptr cloneErrorMarker(); + mutable std::mutex mSharedStateMutex; + std::unique_ptr mShaderManager; - bool mForceShaders; - bool mClampLighting; - bool mAutoUseNormalMaps; std::string mNormalMapPattern; std::string mNormalHeightMapPattern; - bool mAutoUseSpecularMaps; std::string mSpecularMapPattern; - bool mApplyLightingToEnvMaps; - SceneUtil::LightingMethod mLightingMethod; - SceneUtil::LightManager::SupportedMethods mSupportedLightingMethods; - bool mConvertAlphaTestToAlphaToCoverage; - bool mAdjustCoverageForAlphaTest; - bool mSupportsNormalsRT; std::array, 2> mOpaqueDepthTex; - bool mWeatherParticleOcclusion = false; osg::ref_ptr mSharedStateManager; - mutable std::mutex mSharedStateMutex; Resource::ImageManager* mImageManager; Resource::NifFileManager* mNifFileManager; Resource::BgsmFileManager* mBgsmFileManager; + osg::ref_ptr mIncrementalCompileOperation; + mutable osg::ref_ptr mErrorMarker; + mutable std::once_flag mErrorMarkerFlag; osg::Texture::FilterMode mMinFilter; osg::Texture::FilterMode mMagFilter; float mMaxAnisotropy; - bool mUnRefImageDataAfterApply; - - osg::ref_ptr mIncrementalCompileOperation; unsigned int mParticleSystemMask; - mutable osg::ref_ptr mErrorMarker; + SceneUtil::LightingMethod mLightingMethod; + SceneUtil::LightManager::SupportedMethods mSupportedLightingMethods; + bool mForceShaders = false; + bool mClampLighting = true; + bool mAutoUseNormalMaps = false; + bool mAutoUseSpecularMaps = false; + bool mApplyLightingToEnvMaps = false; + bool mConvertAlphaTestToAlphaToCoverage = false; + bool mAdjustCoverageForAlphaTest = false; + bool mSupportsNormalsRT = false; + bool mWeatherParticleOcclusion = false; + bool mUnRefImageDataAfterApply = false; - SceneManager(const SceneManager&); - void operator=(const SceneManager&); + SceneManager(const SceneManager&) = delete; + void operator=(const SceneManager&) = delete; }; } diff --git a/docs/source/reference/modding/paths.rst b/docs/source/reference/modding/paths.rst index 2fdedaa90e..d5765b8ce5 100644 --- a/docs/source/reference/modding/paths.rst +++ b/docs/source/reference/modding/paths.rst @@ -70,6 +70,31 @@ Screenshots .. note:: Flatpak sets ``$XDG_DATA_HOME`` to ``$HOME/.var/app/$FLATPAK_ID/data``, so screenshots will be at ``$HOME/.var/app/org.openmw.OpenMW/data/openmw/screenshots`` if you use the Flatpak. +Override directory (data-local) +------------------------------- + +This is the directory in which OpenMW-CS saves generated content files. +Additionally, this is always the last-loaded data directory in OpenMW, overriding any which came before it. +This can be useful, for instance, for placing automatically-generated plugins created by external tools or to be very certain that particular assets are always overridden regardless of load order. +You may define your own, custom ``data-local`` directory by using it as a key in ``openmw.cfg``, e.g. ``data-local=C:/Games/OpenMW/data``. + ++--------------+----------------------------------------------------------------------------------------------------+ +| OS | Location | ++==============+====================================================================================================+ +| Linux | ``$XDG_DATA_HOME/openmw/data`` or ``$HOME/.local/share/openmw/data`` | ++--------------+----------------------------------------------------------------------------------------------------+ +| Mac | ``$HOME/Library/Application\ Support/openmw/data`` | ++--------------+---------------+------------------------------------------------------------------------------------+ +| Windows | File Explorer | ``Documents\My Games\OpenMW\data`` | +| +---------------+------------------------------------------------------------------------------------+ +| | PowerShell | ``Join-Path ([environment]::GetFolderPath("mydocuments")) "My Games\OpenMW\data"`` | +| +---------------+------------------------------------------------------------------------------------+ +| | Example | ``C:\Users\Username\Documents\My Games\OpenMW\data`` | ++--------------+---------------+------------------------------------------------------------------------------------+ + +.. note:: + Flatpak sets ``$XDG_DATA_HOME`` to ``$HOME/.var/app/$FLATPAK_ID/data``, so data-local will be set to ``$HOME/.var/app/org.openmw.OpenMW/data/openmw/data`` if you use the Flatpak. + Custom configuration directories ================================ diff --git a/files/data-mw/l10n/Interface/gmst.yaml b/files/data-mw/l10n/Interface/gmst.yaml index 0ff365ae60..0ab886a83e 100644 --- a/files/data-mw/l10n/Interface/gmst.yaml +++ b/files/data-mw/l10n/Interface/gmst.yaml @@ -1,17 +1,26 @@ Ask: "sAsk" Back: "sBack" +Barter: "sBarter" Buy: "sBuy" Cancel: "sCancel" +Center: "sCenter" # This has a trailing space in Russian and French games Close: "sClose" +Container: "sContainer" # This has a trailing space in the Russian game Create: "sCreate" +Delete: "sDelete" DisposeOfCorpse: "sDisposeofCorpse" Done: "sDone" +Drop: "sDrop" # This has a trailing space in the Russian game +Equip: "sEquip" # This has a trailing space in the Russian game +Face: "sFace" Goodbye: "sGoodbye" +Hair: "sHair" Info: "sInfo" Inventory: "sInventory" Item: "sItem" +Local: "sLocal" MagicEffects: "sMagicEffects" -# NB: sMouse exists but it is not localized in the Russian game and should not be used to translate Mouse +# Mouse/Move: sMouse and sMove exist but they are not localised in the Russian game and should not be used Next: "sNext" No: "sNo" None: "sNone" @@ -25,6 +34,9 @@ Rest: "sRest" ScrollDown: "sScrolldown" ScrollUp: "sScrollup" Select: "sSelect" +Sell: "sSell" +Sex: "sSex" +Share: "sCompanionShare" Soul: "sSoulGem" Take: "sTake" TakeAll: "sTakeAll" @@ -32,4 +44,5 @@ Topics: "sTopics" Travel: "sTravel" UntilHealed: "sUntilHealed" Wait: "sWait" +World: "sWorld" Yes: "sYes" diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index 0e328bb3a5..540e176750 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -76,6 +76,7 @@ set(BUILTIN_DATA_FILES l10n/OMWCamera/fr.yaml l10n/OMWCamera/pl.yaml + l10n/OMWCombat/de.yaml l10n/OMWCombat/en.yaml l10n/OMWCombat/fr.yaml l10n/OMWCombat/ru.yaml diff --git a/files/data/l10n/Interface/de.yaml b/files/data/l10n/Interface/de.yaml index 98a34a1f0b..81244aeb8d 100644 --- a/files/data/l10n/Interface/de.yaml +++ b/files/data/l10n/Interface/de.yaml @@ -1,12 +1,17 @@ Ask: "Fragen" Back: "Zurück" +Barter: "Handeln" Buy: "Kaufen" Cancel: "Abbruch" +Center: "Zentrieren" Close: "Schließen" +Container: "Behälter" Copy: "Kopieren" Create: "Herstellen" +Delete: "Entfernen" DisposeOfCorpse: "Leiche beseitigen" Done: "Fertig" +Drop: "Ablegen" DurationDay: "{days} d " DurationHour: "{hours} h " DurationMinute: "{minutes} min " @@ -26,12 +31,17 @@ DurationYear: |- one{{years} Jahr } other{{years} Jahre } } +Equip: "Verwenden" +Face: "Gesicht" Goodbye: "Lebt wohl!" +Hair: "Haar" Info: "Info" Inventory: "Inventar" Item: "Gegenstand" +Local: "Lokal" MagicEffects: "Magischer Effekt" Mouse: "Maus" +Move: "Bewegen" Next: "Weiter" No: "Nein" # This one is a bit tricky since it can be translated to @@ -51,6 +61,9 @@ Rest: "Rasten" ScrollDown: "Nach unten scrollen" ScrollUp: "Nach oben scrollen" Select: "Auswählen" +Sell: "Verkaufen" +Sex: "Geschlecht" +Share: "Teilen" Soul: "Seele" Take: "Nehmen" TakeAll: "Alles nehmen" @@ -58,4 +71,5 @@ Topics: "Themen" Travel: "Reisen" UntilHealed: "Bis geheilt" Wait: "Warten" +World: "Welt" Yes: "Ja" diff --git a/files/data/l10n/Interface/en.yaml b/files/data/l10n/Interface/en.yaml index 10b0793eba..cd5ee9df0d 100644 --- a/files/data/l10n/Interface/en.yaml +++ b/files/data/l10n/Interface/en.yaml @@ -1,12 +1,17 @@ Ask: "Ask" Back: "Back" +Barter: "Barter" Buy: "Buy" Cancel: "Cancel" +Center: "Center" Close: "Close" +Container: "Container" Copy: "Copy" Create: "Create" +Delete: "Delete" DisposeOfCorpse: "Dispose of Corpse" Done: "Done" +Drop: "Drop" DurationDay: "{days} d " DurationHour: "{hours} h " DurationMinute: "{minutes} min " @@ -21,12 +26,17 @@ DurationYear: |- one{{years} yr } other{{years} yrs } } +Equip: "Equip" +Face: "Face" Goodbye: "Goodbye" +Hair: "Hair" Info: "Info" Inventory: "Inventory" Item: "Item" +Local: "Local" MagicEffects: "Magic Effects" Mouse: "Mouse" +Move: "Move" Next: "Next" No: "No" None: "None" @@ -42,6 +52,9 @@ Rest: "Rest" ScrollDown: "Scroll Down" ScrollUp: "Scroll Up" Select: "Select" +Sell: "Sell" +Sex: "Sex" +Share: "Share" Soul: "Soul" Take: "Take" TakeAll: "Take All" @@ -49,4 +62,5 @@ Topics: "Topics" Travel: "Travel" UntilHealed: "Until Healed" Wait: "Wait" +World: "World" Yes: "Yes" diff --git a/files/data/l10n/Interface/fr.yaml b/files/data/l10n/Interface/fr.yaml index 7de0a23a27..d6f17150a3 100644 --- a/files/data/l10n/Interface/fr.yaml +++ b/files/data/l10n/Interface/fr.yaml @@ -1,12 +1,17 @@ Ask: "Demander" Back: "En arrière" +Barter: "Marchander" Buy: "Acheter" Cancel: "Annuler" +Center: "Centrer" Close: "Fermer" +Container: "Contenant" Copy: "Copier" Create: "Créer" DisposeOfCorpse: "Supprimer cadavre" +Delete: "Effacer" Done: "Fait" +Drop: "Lâcher" DurationDay: |- {days, plural, one{{days} jour } @@ -21,12 +26,17 @@ DurationYear: |- one{{years} an } other{{years} ans } } +Equip: "S'équiper" +Face: "Face" Goodbye: "Au revoir" +Hair: "Cheveux" Info: "Info" Inventory: "Inventaire" Item: "Objet" +Local: "Local" MagicEffects: "Effets magiques" Mouse: "Souris" +Move: "Déplacement" Next: "Suivant" No: "Non" None: "Aucun" @@ -42,6 +52,9 @@ Rest: "Repos" ScrollDown: "Défilement bas" ScrollUp: "Défilement haut" Select: "Sélectionner" +Sell: "Vendre" +Sex: "Sexe" +Share: "Répartir" Soul: "Ame" Take: "Prendre" TakeAll: "Tout prendre" @@ -49,4 +62,5 @@ Topics: "Sujets" Travel: "Voyager" UntilHealed: "Récup. totale" Wait: "Attendre" +World: "Monde" Yes: "Oui" diff --git a/files/data/l10n/Interface/pl.yaml b/files/data/l10n/Interface/pl.yaml index 07f7480146..2968c482dc 100644 --- a/files/data/l10n/Interface/pl.yaml +++ b/files/data/l10n/Interface/pl.yaml @@ -1,12 +1,17 @@ Ask: "Zapytaj" Back: "Wstecz" +Barter: "Handel" Buy: "Kup" Cancel: "Anuluj" +Center: "Centruj" Close: "Zamknij" +Container: "Pojemnik" Copy: "Kopiuj" Create: "Utwórz" +Delete: "Usuń" DisposeOfCorpse: "Usuń zwłoki" Done: "Koniec" +Drop: "Upuść" DurationDay: "{days} d. " DurationHour: "{hours} godz. " DurationMinute: "{minutes} min " @@ -19,12 +24,17 @@ DurationYear: |- few{{years} lata } many{{years} lat } } +Equip: "Załóż" +Face: "Twarz" Goodbye: "Do widzenia" +Hair: "Włosy" Info: "Info" Inventory: "Ekwipunek" Item: "Przedmiot" +Local: "Okolica" MagicEffects: "Magiczne efekty" Mouse: "Mysz" +Move: "Przenieś" Next: "Nast." No: "Nie" None: "Brak" @@ -40,6 +50,9 @@ Rest: "Odpocznij" ScrollDown: "Przewiń w dół" ScrollUp: "Przewiń w górę" Select: "Wybierz" +Sell: "Sprzedaj" +Sex: "Płeć" +Share: "Podział" Soul: "Dusza" Take: "Weź" TakeAll: "Weź wszystko" @@ -47,4 +60,5 @@ Topics: "Tematy" Travel: "Podróż" UntilHealed: "Do wyzdr." Wait: "Czekaj" +World: "Świat" Yes: "Tak" diff --git a/files/data/l10n/Interface/ru.yaml b/files/data/l10n/Interface/ru.yaml index 7281635aac..eb98026298 100644 --- a/files/data/l10n/Interface/ru.yaml +++ b/files/data/l10n/Interface/ru.yaml @@ -1,12 +1,17 @@ Ask: "Спросить" Back: "Назад" +Barter: "Торговать" Buy: "Купить" Cancel: "Отмена" +Center: "Центр" Close: "Закрыть" +Container: "Контейнер" Copy: "Скопировать" Create: "Создать" +Delete: "Удалить" DisposeOfCorpse: "Убрать тело" Done: "Готово" +Drop: "Бросить" DurationDay: "{days} д " DurationHour: "{hours} ч " DurationMinute: "{minutes} мин " @@ -18,12 +23,17 @@ DurationYear: |- few{{years} г } other{{years} л } } +Equip: "Надеть" +Face: "Лицо" Goodbye: "Прощание" +Hair: "Прическа" Info: "Инфо" Inventory: "Инвентарь" Item: "Предмет" +Local: "Местность" MagicEffects: "Маг. эффекты" Mouse: "Мышь" +Move: "Переместить" Next: "След" No: "Нет" None: "Нет" @@ -39,6 +49,9 @@ Rest: "Отдых" ScrollDown: "Прокрутить вниз" ScrollUp: "Прокрутить вверх" Select: "Выбрать" +Sell: "Продать" +Sex: "Пол" +Share: "Доля" Soul: "Душа" Take: "Взять" TakeAll: "Взять все" @@ -46,4 +59,5 @@ Topics: "Темы" Travel: "Путешествие" UntilHealed: "Выздороветь" Wait: "Ждать" +World: "Мир" Yes: "Да" diff --git a/files/data/l10n/Interface/sv.yaml b/files/data/l10n/Interface/sv.yaml index f33d9a7947..6871a27cf2 100644 --- a/files/data/l10n/Interface/sv.yaml +++ b/files/data/l10n/Interface/sv.yaml @@ -1,12 +1,17 @@ Ask: "Fråga" Back: "Bakåt" +Barter: "Handla" Buy: "Köp" Cancel: "Avbryt" +Center: "Centrera" Close: "Stäng" +Container: "Behållare" Copy: "Kopiera" Create: "Skapa" +Delete: "Radera" DisposeOfCorpse: "Undanröj liket" Done: "Klar" +Drop: "Släpp" DurationDay: "{days} d " DurationHour: "{hours} tim " DurationMinute: "{minutes} min " @@ -21,12 +26,17 @@ DurationYear: |- one{{years} år } other{{years} år } } +Equip: "Utrusta" +Face: "Ansikte" Goodbye: "Adjö" +Hair: "Hår" Info: "Info" Item: "Föremål" Inventory: "Inventariet" +Local: "Lokal" MagicEffects: "Magiska effekter" Mouse: "Mus" +Move: "Flytta" Next: "Nästa" No: "Nej" None: "Inget" @@ -42,6 +52,9 @@ Rest: "Vila" ScrollDown: "Scrolla ner" ScrollUp: "Scrolla upp" Select: "Välj" +Sell: "Sälj" +Sex: "Kön" +Share: "Dela" Soul: "Själ" Take: "Ta" TakeAll: "Ta allt" @@ -49,4 +62,5 @@ Topics: "Ämnen" Travel: "Res" UntilHealed: "Tills återställd" Wait: "Vänta" +World: "Värld" Yes: "Ja" diff --git a/files/data/l10n/OMWCombat/de.yaml b/files/data/l10n/OMWCombat/de.yaml new file mode 100644 index 0000000000..7a51d5928d --- /dev/null +++ b/files/data/l10n/OMWCombat/de.yaml @@ -0,0 +1,16 @@ +Combat: "OpenMW: Kampf" +combatSettingsPageDescription: "OpenMW-Kampfeinstellungen" + +combatSettings: "Kampf" + +unarmedCreatureAttacksDamageArmor: "Angriffe unbewaffneter Kreaturen beschädigen Rüstung" +unarmedCreatureAttacksDamageArmorDescription: | + Auch Angriffe unbewaffneter Kreaturen beschädigen Rüstung. + +redistributeShieldHitsWhenNotWearingShield: "Schildtreffer bei fehlendem Schild umverteilen" +redistributeShieldHitsWhenNotWearingShieldDescription: | + Entspricht "Shield hit location fix" aus dem Morrowind Code Patch. Wenn kein Schild getragen wird, werden Treffer auf den Schild‑Slot auf den linken Schulterpanzer oder den Kürass umverteilt. + +spawnBloodEffectsOnPlayer: "Bluteffekte für Spieler anzeigen" +spawnBloodEffectsOnPlayerDescription: | + Wenn aktiviert, werden beim Spieler bei Treffern im Kampf Bluteffekte angezeigt, genau wie bei anderen Charakteren. diff --git a/files/data/openmw_aux/util.lua b/files/data/openmw_aux/util.lua index ca4fe7ed31..3c4462deec 100644 --- a/files/data/openmw_aux/util.lua +++ b/files/data/openmw_aux/util.lua @@ -110,5 +110,38 @@ function aux_util.mapFilterSort(array, scoreFn) return sortedValues, sortedScores end +--- +-- Iterates over an array of event handlers, calling each in turn until one returns false. +-- @function [parent=#util] callEventHandlers +-- @param #table handlers An optional array of handlers to invoke +-- @param #any ... Arguments to pass to each event handler +-- @return boolean True if no further handlers should be called +function aux_util.callEventHandlers(handlers, ...) + if handlers then + for i = #handlers, 1, -1 do + if handlers[i](...) == false then + return true + end + end + end + return false +end + +--- +-- Iterates over an array of event handler arrays, passing each to `aux_util.callEventHandlers` until the event is handled. +-- @function [parent=#util] callMultipleEventHandlers +-- @param #table handlers An array of event handler arrays +-- @param #any ... Arguments to pass to each event handler +-- @return boolean True if no further handlers should be called +function aux_util.callMultipleEventHandlers(handlers, ...) + for i = 1, #handlers do + local stop = aux_util.callEventHandlers(handlers[i], ...) + if stop then + return true + end + end + return false +end + return aux_util diff --git a/files/data/scripts/omw/activationhandlers.lua b/files/data/scripts/omw/activationhandlers.lua index 3850b207eb..b879d56706 100644 --- a/files/data/scripts/omw/activationhandlers.lua +++ b/files/data/scripts/omw/activationhandlers.lua @@ -2,6 +2,7 @@ local async = require('openmw.async') local core = require('openmw.core') local types = require('openmw.types') local world = require('openmw.world') +local auxUtil = require('openmw_aux.util') local EnableObject = async:registerTimerCallback('EnableObject', function(obj) obj.enabled = true end) @@ -38,21 +39,9 @@ local function onActivate(obj, actor) if obj.parentContainer then return end - local handlers = handlersPerObject[obj.id] - if handlers then - for i = #handlers, 1, -1 do - if handlers[i](obj, actor) == false then - return -- skip other handlers - end - end - end - handlers = handlersPerType[obj.type] - if handlers then - for i = #handlers, 1, -1 do - if handlers[i](obj, actor) == false then - return -- skip other handlers - end - end + local handled = auxUtil.callMultipleEventHandlers({ handlersPerObject[obj.id], handlersPerType[obj.type] }, obj, actor) + if handled then + return end types.Actor.activeEffects(actor):remove('invisibility') world._runStandardActivationAction(obj, actor) diff --git a/files/data/scripts/omw/combat/local.lua b/files/data/scripts/omw/combat/local.lua index 7c688d6569..2cc9e7728f 100644 --- a/files/data/scripts/omw/combat/local.lua +++ b/files/data/scripts/omw/combat/local.lua @@ -6,6 +6,7 @@ local self = require('openmw.self') local storage = require('openmw.storage') local types = require('openmw.types') local util = require('openmw.util') +local auxUtil = require('openmw_aux.util') local Actor = types.Actor local Weapon = types.Weapon local Player = types.Player @@ -270,10 +271,8 @@ local function spawnBloodEffect(position) end local function onHit(data) - for i = #onHitHandlers, 1, -1 do - if onHitHandlers[i](data) == false then - return -- skip other handlers - end + if auxUtil.callEventHandlers(onHitHandlers, data) then + return end if data.successful and not godMode() then I.Combat.applyArmor(data) diff --git a/files/data/scripts/omw/mechanics/animationcontroller.lua b/files/data/scripts/omw/mechanics/animationcontroller.lua index 9edc7565ca..b7a8e74ca3 100644 --- a/files/data/scripts/omw/mechanics/animationcontroller.lua +++ b/files/data/scripts/omw/mechanics/animationcontroller.lua @@ -1,13 +1,10 @@ local anim = require('openmw.animation') local self = require('openmw.self') +local auxUtil = require('openmw_aux.util') local playBlendedHandlers = {} -local function onPlayBlendedAnimation(groupname, options) - for i = #playBlendedHandlers, 1, -1 do - if playBlendedHandlers[i](groupname, options) == false then - return - end - end +local function onPlayBlendedAnimation(groupname, options) + auxUtil.callEventHandlers(playBlendedHandlers, groupname, options) end local function playBlendedAnimation(groupname, options) @@ -20,22 +17,7 @@ end local textKeyHandlers = {} local function onAnimationTextKey(groupname, key) - local handlers = textKeyHandlers[groupname] - if handlers then - for i = #handlers, 1, -1 do - if handlers[i](groupname, key) == false then - return - end - end - end - handlers = textKeyHandlers[''] - if handlers then - for i = #handlers, 1, -1 do - if handlers[i](groupname, key) == false then - return - end - end - end + auxUtil.callMultipleEventHandlers({ textKeyHandlers[groupname], textKeyHandlers[''] }, groupname, key) end local initialized = false diff --git a/files/data/scripts/omw/skillhandlers.lua b/files/data/scripts/omw/skillhandlers.lua index 9b58d81174..c5e3293fa6 100644 --- a/files/data/scripts/omw/skillhandlers.lua +++ b/files/data/scripts/omw/skillhandlers.lua @@ -2,6 +2,7 @@ local self = require('openmw.self') local I = require('openmw.interfaces') local types = require('openmw.types') local core = require('openmw.core') +local auxUtil = require('openmw_aux.util') local NPC = require('openmw.types').NPC local Skill = core.stats.Skill @@ -104,11 +105,7 @@ local function skillUsed(skillid, options) end end - for i = #skillUsedHandlers, 1, -1 do - if skillUsedHandlers[i](skillid, options) == false then - return - end - end + auxUtil.callEventHandlers(skillUsedHandlers, skillid, options) end local function skillLevelUp(skillid, source) @@ -144,11 +141,7 @@ local function skillLevelUp(skillid, source) options.levelUpSpecializationIncreaseValue = core.getGMST('iLevelupSpecialization') end - for i = #skillLevelUpHandlers, 1, -1 do - if skillLevelUpHandlers[i](skillid, source, options) == false then - return - end - end + auxUtil.callEventHandlers(skillLevelUpHandlers, skillid, source, options) end return { diff --git a/files/data/scripts/omw/usehandlers.lua b/files/data/scripts/omw/usehandlers.lua index cf976994be..c94ab12a40 100644 --- a/files/data/scripts/omw/usehandlers.lua +++ b/files/data/scripts/omw/usehandlers.lua @@ -1,26 +1,15 @@ local types = require('openmw.types') local world = require('openmw.world') +local auxUtil = require('openmw_aux.util') local handlersPerObject = {} local handlersPerType = {} local function useItem(obj, actor, force) local options = { force = force or false } - local handlers = handlersPerObject[obj.id] - if handlers then - for i = #handlers, 1, -1 do - if handlers[i](obj, actor, options) == false then - return -- skip other handlers - end - end - end - handlers = handlersPerType[obj.type] - if handlers then - for i = #handlers, 1, -1 do - if handlers[i](obj, actor, options) == false then - return -- skip other handlers - end - end + local handled = auxUtil.callMultipleEventHandlers({ handlersPerObject[obj.id], handlersPerType[obj.type] }, obj, actor, options) + if handled then + return end world._runStandardUseAction(obj, actor, options.force) end diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index add678a37f..c2b3d78ea9 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -1224,6 +1224,16 @@ -- @usage -- Start a new quest, add it to the player's quest list but don't add any journal entries -- types.Player.quests(player)["ms_fargothring"].stage = 0 +--- +-- Adds a topic to the list of ones known by the player, so that it can be used in dialogue with actors who can talk about that topic. +-- @function [parent=#PLAYER] addTopic +-- @param openmw.core#GameObject player +-- @param string topicId +-- @usage -- Add topic to the list of known ones, in a player script +-- self.type.addTopic(self, "Some Work") +-- @usage -- Give all players in the current world a specific topic, in a global script +-- for _, player in ipairs(world.players) do player.type.addTopic(player, "Some Unrelated Work") end + --- -- Returns @{#PlayerJournal}, which contains the read-only access to journal text data accumulated by the player. -- Not the same as @{openmw_core#Dialogue.journal} which holds raw game records: with placeholders for dynamic variables and no player-specific info.