From 5d4fc960624e4ea831dd3f0b2599d77542dc93df Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sat, 30 Aug 2025 14:21:31 +0200 Subject: [PATCH 01/16] Bump us up to 0.51 --- CHANGELOG.md | 4 ++++ CMakeLists.txt | 2 +- README.md | 2 +- apps/openmw/mwstate/statemanagerimp.cpp | 8 +++----- components/esm3/formatversion.hpp | 4 +--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c3990295e..24f3a027bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.51.0 +------ + + 0.50.0 ------ diff --git a/CMakeLists.txt b/CMakeLists.txt index 834668e92a..55711e2c80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,7 +80,7 @@ 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_POSTPROCESSING_API_REVISION 3) 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/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 34aa3eaa46..d6cdf99bff 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 From ff40de89aaa3d86f3575802c20b4bb066c69a530 Mon Sep 17 00:00:00 2001 From: Kuyondo Date: Sun, 31 Aug 2025 18:14:49 +0800 Subject: [PATCH 02/16] less restrictive ammo usage --- apps/openmw/mwclass/weapon.cpp | 18 ++++++++++++++++-- apps/openmw/mwgui/inventorywindow.cpp | 6 +++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwclass/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index 863083ee94..80ed5862ea 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 && isAmmo) || (activeWeapUsesAmmo && isAmmo && sameAmmoType))) + 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 93fae2ea96..dc11f1a849 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; From d78480120d986e7f8f0e5c6393e7b6c195d76201 Mon Sep 17 00:00:00 2001 From: Kuyondo Date: Wed, 3 Sep 2025 16:30:36 +0800 Subject: [PATCH 03/16] readable if statement --- apps/openmw/mwclass/weapon.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwclass/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index 80ed5862ea..197c3ff1b3 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -285,7 +285,7 @@ namespace MWClass bool activeWeapUsesAmmo = activeWeapon->mWeaponClass == ESM::WeaponType::Class::Ranged; bool sameAmmoType = activeWeapon->mAmmoType == type; // special case for ammo equipping - if (!((!activeWeapUsesAmmo && isAmmo) || (activeWeapUsesAmmo && isAmmo && sameAmmoType))) + if ((activeWeapUsesAmmo && !sameAmmoType) || !isAmmo) return { 0, "#{sCantEquipWeapWarning}" }; } } From 0df0ad9c1c0c6ee34fb5cbd55eb092e1d55c695c Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Wed, 3 Sep 2025 22:17:22 +0200 Subject: [PATCH 04/16] Deduplicate event handling code --- files/data/openmw_aux/util.lua | 33 +++++++++++++++++++ files/data/scripts/omw/activationhandlers.lua | 19 +++-------- files/data/scripts/omw/combat/local.lua | 7 ++-- .../omw/mechanics/animationcontroller.lua | 26 +++------------ files/data/scripts/omw/skillhandlers.lua | 13 ++------ files/data/scripts/omw/usehandlers.lua | 19 +++-------- 6 files changed, 51 insertions(+), 66 deletions(-) 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 From 4168b6d02e4ab7f4afdfd308711048807f146955 Mon Sep 17 00:00:00 2001 From: Kuyondo Date: Thu, 11 Sep 2025 22:08:58 +0800 Subject: [PATCH 05/16] less restrictive probes and lockpicks equipping too --- apps/openmw/mwclass/lockpick.cpp | 1 + apps/openmw/mwclass/probe.cpp | 1 + 2 files changed, 2 insertions(+) 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}" }; From 0d260b3c8aeec2701d71348fdc3019062ee770b8 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 14 Sep 2025 00:44:05 +0300 Subject: [PATCH 06/16] Add German translations for OMWCombat --- files/data/CMakeLists.txt | 1 + files/data/l10n/OMWCombat/de.yaml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 files/data/l10n/OMWCombat/de.yaml 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/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. From baf575e594547e944356262d77bcd784fa41774b Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 14 Sep 2025 01:41:11 +0300 Subject: [PATCH 07/16] Add Sex into OpenMW ...as well as the other lines required to expose the race menu gamepad actions to l10n and use them --- apps/openmw/mwgui/race.cpp | 6 +++--- files/data-mw/l10n/Interface/gmst.yaml | 3 +++ files/data/l10n/Interface/de.yaml | 3 +++ files/data/l10n/Interface/en.yaml | 3 +++ files/data/l10n/Interface/fr.yaml | 3 +++ files/data/l10n/Interface/pl.yaml | 3 +++ files/data/l10n/Interface/ru.yaml | 3 +++ files/data/l10n/Interface/sv.yaml | 3 +++ 8 files changed, 24 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index fe01a60894..ddcb843bf7 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/files/data-mw/l10n/Interface/gmst.yaml b/files/data-mw/l10n/Interface/gmst.yaml index 0ff365ae60..cb3f337bcc 100644 --- a/files/data-mw/l10n/Interface/gmst.yaml +++ b/files/data-mw/l10n/Interface/gmst.yaml @@ -6,7 +6,9 @@ Close: "sClose" Create: "sCreate" DisposeOfCorpse: "sDisposeofCorpse" Done: "sDone" +Face: "sFace" Goodbye: "sGoodbye" +Hair: "sHair" Info: "sInfo" Inventory: "sInventory" Item: "sItem" @@ -25,6 +27,7 @@ Rest: "sRest" ScrollDown: "sScrolldown" ScrollUp: "sScrollup" Select: "sSelect" +Sex: "sSex" Soul: "sSoulGem" Take: "sTake" TakeAll: "sTakeAll" diff --git a/files/data/l10n/Interface/de.yaml b/files/data/l10n/Interface/de.yaml index 98a34a1f0b..3922e5414d 100644 --- a/files/data/l10n/Interface/de.yaml +++ b/files/data/l10n/Interface/de.yaml @@ -26,7 +26,9 @@ DurationYear: |- one{{years} Jahr } other{{years} Jahre } } +Face: "Gesicht" Goodbye: "Lebt wohl!" +Hair: "Haar" Info: "Info" Inventory: "Inventar" Item: "Gegenstand" @@ -51,6 +53,7 @@ Rest: "Rasten" ScrollDown: "Nach unten scrollen" ScrollUp: "Nach oben scrollen" Select: "Auswählen" +Sex: "Geschlecht" Soul: "Seele" Take: "Nehmen" TakeAll: "Alles nehmen" diff --git a/files/data/l10n/Interface/en.yaml b/files/data/l10n/Interface/en.yaml index 10b0793eba..71a0fcaea2 100644 --- a/files/data/l10n/Interface/en.yaml +++ b/files/data/l10n/Interface/en.yaml @@ -21,7 +21,9 @@ DurationYear: |- one{{years} yr } other{{years} yrs } } +Face: "Face" Goodbye: "Goodbye" +Hair: "Hair" Info: "Info" Inventory: "Inventory" Item: "Item" @@ -42,6 +44,7 @@ Rest: "Rest" ScrollDown: "Scroll Down" ScrollUp: "Scroll Up" Select: "Select" +Sex: "Sex" Soul: "Soul" Take: "Take" TakeAll: "Take All" diff --git a/files/data/l10n/Interface/fr.yaml b/files/data/l10n/Interface/fr.yaml index 7de0a23a27..24e474fd56 100644 --- a/files/data/l10n/Interface/fr.yaml +++ b/files/data/l10n/Interface/fr.yaml @@ -21,7 +21,9 @@ DurationYear: |- one{{years} an } other{{years} ans } } +Face: "Face" Goodbye: "Au revoir" +Hair: "Cheveux" Info: "Info" Inventory: "Inventaire" Item: "Objet" @@ -42,6 +44,7 @@ Rest: "Repos" ScrollDown: "Défilement bas" ScrollUp: "Défilement haut" Select: "Sélectionner" +Sex: "Sexe" Soul: "Ame" Take: "Prendre" TakeAll: "Tout prendre" diff --git a/files/data/l10n/Interface/pl.yaml b/files/data/l10n/Interface/pl.yaml index 07f7480146..41b3600577 100644 --- a/files/data/l10n/Interface/pl.yaml +++ b/files/data/l10n/Interface/pl.yaml @@ -19,7 +19,9 @@ DurationYear: |- few{{years} lata } many{{years} lat } } +Face: "Twarz" Goodbye: "Do widzenia" +Hair: "Włosy" Info: "Info" Inventory: "Ekwipunek" Item: "Przedmiot" @@ -40,6 +42,7 @@ Rest: "Odpocznij" ScrollDown: "Przewiń w dół" ScrollUp: "Przewiń w górę" Select: "Wybierz" +Sex: "Płeć" Soul: "Dusza" Take: "Weź" TakeAll: "Weź wszystko" diff --git a/files/data/l10n/Interface/ru.yaml b/files/data/l10n/Interface/ru.yaml index 7281635aac..80f5574403 100644 --- a/files/data/l10n/Interface/ru.yaml +++ b/files/data/l10n/Interface/ru.yaml @@ -18,7 +18,9 @@ DurationYear: |- few{{years} г } other{{years} л } } +Face: "Лицо" Goodbye: "Прощание" +Hair: "Прическа" Info: "Инфо" Inventory: "Инвентарь" Item: "Предмет" @@ -39,6 +41,7 @@ Rest: "Отдых" ScrollDown: "Прокрутить вниз" ScrollUp: "Прокрутить вверх" Select: "Выбрать" +Sex: "Пол" Soul: "Душа" Take: "Взять" TakeAll: "Взять все" diff --git a/files/data/l10n/Interface/sv.yaml b/files/data/l10n/Interface/sv.yaml index f33d9a7947..ff0d9a988e 100644 --- a/files/data/l10n/Interface/sv.yaml +++ b/files/data/l10n/Interface/sv.yaml @@ -21,7 +21,9 @@ DurationYear: |- one{{years} år } other{{years} år } } +Face: "Ansikte" Goodbye: "Adjö" +Hair: "Hår" Info: "Info" Item: "Föremål" Inventory: "Inventariet" @@ -42,6 +44,7 @@ Rest: "Vila" ScrollDown: "Scrolla ner" ScrollUp: "Scrolla upp" Select: "Välj" +Sex: "Kön" Soul: "Själ" Take: "Ta" TakeAll: "Ta allt" From b0055ec8e71e50b8848d6755cc349b2e6aea5292 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 14 Sep 2025 02:13:39 +0300 Subject: [PATCH 08/16] Use l10n for map gamepad actions --- apps/openmw/mwgui/mapwindow.cpp | 14 +++++++++----- files/data-mw/l10n/Interface/gmst.yaml | 6 +++++- files/data/l10n/Interface/de.yaml | 5 +++++ files/data/l10n/Interface/en.yaml | 5 +++++ files/data/l10n/Interface/fr.yaml | 5 +++++ files/data/l10n/Interface/pl.yaml | 5 +++++ files/data/l10n/Interface/ru.yaml | 5 +++++ files/data/l10n/Interface/sv.yaml | 5 +++++ 8 files changed, 44 insertions(+), 6 deletions(-) diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index f80871d3ef..c6dc54c886 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -835,9 +835,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}"; } } @@ -1228,7 +1229,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(); } @@ -1532,7 +1533,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/files/data-mw/l10n/Interface/gmst.yaml b/files/data-mw/l10n/Interface/gmst.yaml index cb3f337bcc..e409d2e61d 100644 --- a/files/data-mw/l10n/Interface/gmst.yaml +++ b/files/data-mw/l10n/Interface/gmst.yaml @@ -2,8 +2,10 @@ Ask: "sAsk" Back: "sBack" Buy: "sBuy" Cancel: "sCancel" +Center: "sCenter" # NB: in Russian and French localisations this has a trailing space Close: "sClose" Create: "sCreate" +Delete: "sDelete" DisposeOfCorpse: "sDisposeofCorpse" Done: "sDone" Face: "sFace" @@ -12,8 +14,9 @@ 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 +# NB: sMouse and sMove exist but they are not localised in the Russian game and should not be used Next: "sNext" No: "sNo" None: "sNone" @@ -35,4 +38,5 @@ Topics: "sTopics" Travel: "sTravel" UntilHealed: "sUntilHealed" Wait: "sWait" +World: "sWorld" Yes: "sYes" diff --git a/files/data/l10n/Interface/de.yaml b/files/data/l10n/Interface/de.yaml index 3922e5414d..cd1797edf0 100644 --- a/files/data/l10n/Interface/de.yaml +++ b/files/data/l10n/Interface/de.yaml @@ -2,9 +2,11 @@ Ask: "Fragen" Back: "Zurück" Buy: "Kaufen" Cancel: "Abbruch" +Center: "Zentrieren" Close: "Schließen" Copy: "Kopieren" Create: "Herstellen" +Delete: "Entfernen" DisposeOfCorpse: "Leiche beseitigen" Done: "Fertig" DurationDay: "{days} d " @@ -32,8 +34,10 @@ 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 @@ -61,4 +65,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 71a0fcaea2..0930546fd5 100644 --- a/files/data/l10n/Interface/en.yaml +++ b/files/data/l10n/Interface/en.yaml @@ -2,9 +2,11 @@ Ask: "Ask" Back: "Back" Buy: "Buy" Cancel: "Cancel" +Center: "Center" Close: "Close" Copy: "Copy" Create: "Create" +Delete: "Delete" DisposeOfCorpse: "Dispose of Corpse" Done: "Done" DurationDay: "{days} d " @@ -27,8 +29,10 @@ Hair: "Hair" Info: "Info" Inventory: "Inventory" Item: "Item" +Local: "Local" MagicEffects: "Magic Effects" Mouse: "Mouse" +Move: "Move" Next: "Next" No: "No" None: "None" @@ -52,4 +56,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 24e474fd56..df3f7965e5 100644 --- a/files/data/l10n/Interface/fr.yaml +++ b/files/data/l10n/Interface/fr.yaml @@ -2,10 +2,12 @@ Ask: "Demander" Back: "En arrière" Buy: "Acheter" Cancel: "Annuler" +Center: "Centrer" Close: "Fermer" Copy: "Copier" Create: "Créer" DisposeOfCorpse: "Supprimer cadavre" +Delete: "Effacer" Done: "Fait" DurationDay: |- {days, plural, @@ -27,8 +29,10 @@ Hair: "Cheveux" Info: "Info" Inventory: "Inventaire" Item: "Objet" +Local: "Local" MagicEffects: "Effets magiques" Mouse: "Souris" +Move: "Déplacement" Next: "Suivant" No: "Non" None: "Aucun" @@ -52,4 +56,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 41b3600577..cc8220db23 100644 --- a/files/data/l10n/Interface/pl.yaml +++ b/files/data/l10n/Interface/pl.yaml @@ -2,9 +2,11 @@ Ask: "Zapytaj" Back: "Wstecz" Buy: "Kup" Cancel: "Anuluj" +Center: "Centruj" Close: "Zamknij" Copy: "Kopiuj" Create: "Utwórz" +Delete: "Usuń" DisposeOfCorpse: "Usuń zwłoki" Done: "Koniec" DurationDay: "{days} d. " @@ -25,8 +27,10 @@ Hair: "Włosy" Info: "Info" Inventory: "Ekwipunek" Item: "Przedmiot" +Local: "Okolica" MagicEffects: "Magiczne efekty" Mouse: "Mysz" +Move: "Przenieś" Next: "Nast." No: "Nie" None: "Brak" @@ -50,4 +54,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 80f5574403..de1981dcf4 100644 --- a/files/data/l10n/Interface/ru.yaml +++ b/files/data/l10n/Interface/ru.yaml @@ -2,9 +2,11 @@ Ask: "Спросить" Back: "Назад" Buy: "Купить" Cancel: "Отмена" +Center: "Центр" Close: "Закрыть" Copy: "Скопировать" Create: "Создать" +Delete: "Удалить" DisposeOfCorpse: "Убрать тело" Done: "Готово" DurationDay: "{days} д " @@ -24,8 +26,10 @@ Hair: "Прическа" Info: "Инфо" Inventory: "Инвентарь" Item: "Предмет" +Local: "Местность" MagicEffects: "Маг. эффекты" Mouse: "Мышь" +Move: "Переместить" Next: "След" No: "Нет" None: "Нет" @@ -49,4 +53,5 @@ Topics: "Темы" Travel: "Путешествие" UntilHealed: "Выздороветь" Wait: "Ждать" +World: "Мир" Yes: "Да" diff --git a/files/data/l10n/Interface/sv.yaml b/files/data/l10n/Interface/sv.yaml index ff0d9a988e..ed22ff2dcf 100644 --- a/files/data/l10n/Interface/sv.yaml +++ b/files/data/l10n/Interface/sv.yaml @@ -2,9 +2,11 @@ Ask: "Fråga" Back: "Bakåt" Buy: "Köp" Cancel: "Avbryt" +Center: "Centrera" Close: "Stäng" Copy: "Kopiera" Create: "Skapa" +Delete: "Radera" DisposeOfCorpse: "Undanröj liket" Done: "Klar" DurationDay: "{days} d " @@ -27,8 +29,10 @@ 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" @@ -52,4 +56,5 @@ Topics: "Ämnen" Travel: "Res" UntilHealed: "Tills återställd" Wait: "Vänta" +World: "Värld" Yes: "Ja" From 1182004937c037b6a1c38584b66f8d9d09f0dc28 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 14 Sep 2025 03:55:16 +0300 Subject: [PATCH 09/16] Use l10n for inventory gamepad actions --- apps/openmw/mwgui/inventorywindow.cpp | 12 ++++++------ files/data-mw/l10n/Interface/gmst.yaml | 10 ++++++++-- files/data/l10n/Interface/de.yaml | 6 ++++++ files/data/l10n/Interface/en.yaml | 6 ++++++ files/data/l10n/Interface/fr.yaml | 6 ++++++ files/data/l10n/Interface/pl.yaml | 6 ++++++ files/data/l10n/Interface/ru.yaml | 6 ++++++ files/data/l10n/Interface/sv.yaml | 6 ++++++ 8 files changed, 50 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index 64d45f1447..44be79ab91 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -968,25 +968,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/files/data-mw/l10n/Interface/gmst.yaml b/files/data-mw/l10n/Interface/gmst.yaml index e409d2e61d..0ab886a83e 100644 --- a/files/data-mw/l10n/Interface/gmst.yaml +++ b/files/data-mw/l10n/Interface/gmst.yaml @@ -1,13 +1,17 @@ Ask: "sAsk" Back: "sBack" +Barter: "sBarter" Buy: "sBuy" Cancel: "sCancel" -Center: "sCenter" # NB: in Russian and French localisations this has a trailing space +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" @@ -16,7 +20,7 @@ Inventory: "sInventory" Item: "sItem" Local: "sLocal" MagicEffects: "sMagicEffects" -# NB: sMouse and sMove exist but they are not localised in the Russian game and should not be used +# 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" @@ -30,7 +34,9 @@ Rest: "sRest" ScrollDown: "sScrolldown" ScrollUp: "sScrollup" Select: "sSelect" +Sell: "sSell" Sex: "sSex" +Share: "sCompanionShare" Soul: "sSoulGem" Take: "sTake" TakeAll: "sTakeAll" diff --git a/files/data/l10n/Interface/de.yaml b/files/data/l10n/Interface/de.yaml index cd1797edf0..81244aeb8d 100644 --- a/files/data/l10n/Interface/de.yaml +++ b/files/data/l10n/Interface/de.yaml @@ -1,14 +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 " @@ -28,6 +31,7 @@ DurationYear: |- one{{years} Jahr } other{{years} Jahre } } +Equip: "Verwenden" Face: "Gesicht" Goodbye: "Lebt wohl!" Hair: "Haar" @@ -57,7 +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" diff --git a/files/data/l10n/Interface/en.yaml b/files/data/l10n/Interface/en.yaml index 0930546fd5..cd5ee9df0d 100644 --- a/files/data/l10n/Interface/en.yaml +++ b/files/data/l10n/Interface/en.yaml @@ -1,14 +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 " @@ -23,6 +26,7 @@ DurationYear: |- one{{years} yr } other{{years} yrs } } +Equip: "Equip" Face: "Face" Goodbye: "Goodbye" Hair: "Hair" @@ -48,7 +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" diff --git a/files/data/l10n/Interface/fr.yaml b/files/data/l10n/Interface/fr.yaml index df3f7965e5..d6f17150a3 100644 --- a/files/data/l10n/Interface/fr.yaml +++ b/files/data/l10n/Interface/fr.yaml @@ -1,14 +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 } @@ -23,6 +26,7 @@ DurationYear: |- one{{years} an } other{{years} ans } } +Equip: "S'équiper" Face: "Face" Goodbye: "Au revoir" Hair: "Cheveux" @@ -48,7 +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" diff --git a/files/data/l10n/Interface/pl.yaml b/files/data/l10n/Interface/pl.yaml index cc8220db23..2968c482dc 100644 --- a/files/data/l10n/Interface/pl.yaml +++ b/files/data/l10n/Interface/pl.yaml @@ -1,14 +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 " @@ -21,6 +24,7 @@ DurationYear: |- few{{years} lata } many{{years} lat } } +Equip: "Załóż" Face: "Twarz" Goodbye: "Do widzenia" Hair: "Włosy" @@ -46,7 +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" diff --git a/files/data/l10n/Interface/ru.yaml b/files/data/l10n/Interface/ru.yaml index de1981dcf4..eb98026298 100644 --- a/files/data/l10n/Interface/ru.yaml +++ b/files/data/l10n/Interface/ru.yaml @@ -1,14 +1,17 @@ Ask: "Спросить" Back: "Назад" +Barter: "Торговать" Buy: "Купить" Cancel: "Отмена" Center: "Центр" Close: "Закрыть" +Container: "Контейнер" Copy: "Скопировать" Create: "Создать" Delete: "Удалить" DisposeOfCorpse: "Убрать тело" Done: "Готово" +Drop: "Бросить" DurationDay: "{days} д " DurationHour: "{hours} ч " DurationMinute: "{minutes} мин " @@ -20,6 +23,7 @@ DurationYear: |- few{{years} г } other{{years} л } } +Equip: "Надеть" Face: "Лицо" Goodbye: "Прощание" Hair: "Прическа" @@ -45,7 +49,9 @@ Rest: "Отдых" ScrollDown: "Прокрутить вниз" ScrollUp: "Прокрутить вверх" Select: "Выбрать" +Sell: "Продать" Sex: "Пол" +Share: "Доля" Soul: "Душа" Take: "Взять" TakeAll: "Взять все" diff --git a/files/data/l10n/Interface/sv.yaml b/files/data/l10n/Interface/sv.yaml index ed22ff2dcf..6871a27cf2 100644 --- a/files/data/l10n/Interface/sv.yaml +++ b/files/data/l10n/Interface/sv.yaml @@ -1,14 +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 " @@ -23,6 +26,7 @@ DurationYear: |- one{{years} år } other{{years} år } } +Equip: "Utrusta" Face: "Ansikte" Goodbye: "Adjö" Hair: "Hår" @@ -48,7 +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" From 9a4db2d65f02c7969a76d0dc89d05d24937712b5 Mon Sep 17 00:00:00 2001 From: Dave Corley Date: Sun, 14 Sep 2025 14:38:05 -0500 Subject: [PATCH 10/16] CLEANUP: Document addTopic --- files/lua_api/openmw/types.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) 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. From ff79c2d8268c33732aa15de0adea2457ba2e3c6b Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sat, 20 Sep 2025 10:39:43 +0000 Subject: [PATCH 11/16] Turn menu.saveGame into a delayed action --- CMakeLists.txt | 2 +- apps/openmw/mwlua/luamanagerimp.cpp | 59 ++++++++++++++++++----------- apps/openmw/mwlua/luamanagerimp.hpp | 5 ++- apps/openmw/mwlua/menuscripts.cpp | 5 ++- components/lua/scriptscontainer.cpp | 4 +- components/lua/scriptscontainer.hpp | 2 +- 6 files changed, 49 insertions(+), 28 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 834668e92a..63ed9edfe1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 50) 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/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 0b760ccdf0..c0146234f5 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(); + double frameDuration = MWBase::Environment::get().getWorld()->getTimeManager()->isPaused() + ? 0.0 + : MWBase::Environment::get().getFrameDuration(); + mInputActions.update(frameDuration); + mMenuScripts.onFrame(frameDuration); + if (playerScripts) + playerScripts->onFrame(frameDuration); } - mInputEvents.clear(); - mLuaEvents.callMenuEventHandlers(); - double frameDuration = MWBase::Environment::get().getWorld()->getTimeManager()->isPaused() - ? 0.0 - : 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/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index f5b5605e37..36bdbca8b1 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 { From d2958a17faa3346203127b8dd4ce32b6aacaf6cf Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 22 Sep 2025 22:13:03 +0200 Subject: [PATCH 12/16] Make Absorb Skill safe for creatures --- CHANGELOG.md | 1 + apps/openmw/mwmechanics/spelleffects.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 830ab93094..78a65d7557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,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/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index c2a7924c77..85fa14b09b 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -850,7 +850,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; @@ -1302,7 +1302,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; From 000c5d2a084148c9e25d49d7e07e397f154cba10 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 25 Sep 2025 20:47:10 +0200 Subject: [PATCH 13/16] Ensure error marker assignment is thread safe --- apps/components_tests/CMakeLists.txt | 1 + .../resource/testresourcesystem.cpp | 32 ++++++++++++++++ components/resource/scenemanager.cpp | 14 +------ components/resource/scenemanager.hpp | 37 ++++++++++--------- 4 files changed, 54 insertions(+), 30 deletions(-) create mode 100644 apps/components_tests/resource/testresourcesystem.cpp 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/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index 84ba08c0b3..ffa6abe58e 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -446,15 +446,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) @@ -462,8 +453,8 @@ namespace Resource , mMinFilter(osg::Texture::LINEAR_MIPMAP_LINEAR) , mMagFilter(osg::Texture::LINEAR) , mMaxAnisotropy(1) - , mUnRefImageDataAfterApply(false) , mParticleSystemMask(~0u) + , mLightingMethod(SceneUtil::LightingMethod::FFP) { } @@ -988,8 +979,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 370383975d..df37a6165a 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; int 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; }; } From ae0886ae361acf9370a593a61cb18837871e3d9a Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Mon, 29 Sep 2025 02:19:06 +0300 Subject: [PATCH 14/16] Make sure gamepad triggers can be bound (#8721) --- apps/openmw/mwinput/controllermanager.cpp | 6 ++++++ 1 file changed, 6 insertions(+) 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; From 0eb1a79b0004c87171bbc3eda29af9a57ce29fab Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Thu, 2 Oct 2025 08:28:30 +0300 Subject: [PATCH 15/16] Update encumbrance unconditionally (#8729) The flag is only set when items are physically added/removed, which doesn't happen during barter --- apps/openmw/mwgui/inventorywindow.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index 44be79ab91..6c9a9869bb 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -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(); From fd7e6b6bcb23d0dff633e19b3d4efc6c59e322d4 Mon Sep 17 00:00:00 2001 From: Dave Corley Date: Thu, 2 Oct 2025 21:28:26 +0000 Subject: [PATCH 16/16] DOC: Document data-local --- docs/source/reference/modding/paths.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) 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 ================================