From 3ef2084f80146534b1b3920797985276c6575ab5 Mon Sep 17 00:00:00 2001 From: Bob Tuttle Date: Mon, 22 Jul 2024 20:19:21 +0000 Subject: [PATCH 001/154] Update install-game-files.rst --- docs/source/manuals/installation/install-game-files.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 8a7a594691..fe236168d5 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -150,7 +150,7 @@ If you are running macOS, you can also download Morrowind through Steam: } #. Launch the Steam client and let it download. You can then find ``Morrowind.esm`` at - ``~/Library/Application Support/Steam/steamapps/common/The Elder Scrolls III - Morrowind/Data Files/`` + ``~/Library/Application Support/Steam/steamapps/common/The Elder Scrolls III - Morrowind/Data Files/``. The ~/Library folder is hidden by default. To get to it from the Installation Wizard popup, you will need to go to Users/your YOUR_USERNAME_HERE/ and do ``CMD+SHFT+PERIOD`` to reveal the ~/Library folder. Linux ^^^^^ From be1ce81be744c1df03c7b00da0e2b239012a2aeb Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 6 Jan 2025 23:05:32 +0100 Subject: [PATCH 002/154] Write debug recast mesh before generating navmesh --- .../detournavigator/asyncnavmeshupdater.cpp | 60 +++++++++++-------- .../detournavigator/asyncnavmeshupdater.hpp | 2 - 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index 24d7cc0d32..71c8bbc2d3 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -74,6 +75,36 @@ namespace DetourNavigator { return job.mGeneratedNavMeshData != nullptr; } + + std::string makeRevision(const Version& version) + { + return Misc::StringUtils::format(".%zu.%zu", version.mGeneration, version.mRevision); + } + + void writeDebugRecastMesh( + const Settings& settings, const TilePosition& tilePosition, const RecastMesh& recastMesh) + { + if (!settings.mEnableWriteRecastMeshToFile) + return; + std::string revision; + if (settings.mEnableRecastMeshFileNameRevision) + revision = makeRevision(recastMesh.getVersion()); + writeToFile(recastMesh, + Misc::StringUtils::format( + "%s%d.%d.", settings.mRecastMeshPathPrefix, tilePosition.x(), tilePosition.y()), + revision, settings.mRecast); + } + + void writeDebugNavMesh( + const Settings& settings, const GuardedNavMeshCacheItem& navMeshCacheItem, const Version& version) + { + if (!settings.mEnableWriteNavMeshToFile) + return; + std::string revision; + if (settings.mEnableNavMeshFileNameRevision) + revision = makeRevision(version); + writeToFile(navMeshCacheItem.lockConst()->getImpl(), settings.mNavMeshPathPrefix, revision); + } } std::ostream& operator<<(std::ostream& stream, JobStatus value) @@ -512,6 +543,8 @@ namespace DetourNavigator return JobStatus::Done; } + writeDebugRecastMesh(mSettings, job.mChangedTile, *recastMesh); + NavMeshTilesCache::Value cachedNavMeshData = mNavMeshTilesCache.get(job.mAgentBounds, job.mChangedTile, *recastMesh); std::unique_ptr preparedNavMeshData; @@ -633,7 +666,7 @@ namespace DetourNavigator mPresentTiles.insert(std::make_tuple(job.mAgentBounds, job.mChangedTile)); } - writeDebugFiles(job, &recastMesh); + writeDebugNavMesh(mSettings, navMeshCacheItem, navMeshVersion); return isSuccess(status) ? JobStatus::Done : JobStatus::Fail; } @@ -688,31 +721,6 @@ namespace DetourNavigator return job; } - void AsyncNavMeshUpdater::writeDebugFiles(const Job& job, const RecastMesh* recastMesh) const - { - std::string revision; - std::string recastMeshRevision; - std::string navMeshRevision; - if ((mSettings.get().mEnableWriteNavMeshToFile || mSettings.get().mEnableWriteRecastMeshToFile) - && (mSettings.get().mEnableRecastMeshFileNameRevision || mSettings.get().mEnableNavMeshFileNameRevision)) - { - revision = "." - + std::to_string((std::chrono::steady_clock::now() - std::chrono::steady_clock::time_point()).count()); - if (mSettings.get().mEnableRecastMeshFileNameRevision) - recastMeshRevision = revision; - if (mSettings.get().mEnableNavMeshFileNameRevision) - navMeshRevision = std::move(revision); - } - if (recastMesh && mSettings.get().mEnableWriteRecastMeshToFile) - writeToFile(*recastMesh, - mSettings.get().mRecastMeshPathPrefix + std::to_string(job.mChangedTile.x()) + "_" - + std::to_string(job.mChangedTile.y()) + "_", - recastMeshRevision, mSettings.get().mRecast); - if (mSettings.get().mEnableWriteNavMeshToFile) - if (const auto shared = job.mNavMeshCacheItem.lock()) - writeToFile(shared->lockConst()->getImpl(), mSettings.get().mNavMeshPathPrefix, navMeshRevision); - } - bool AsyncNavMeshUpdater::lockTile( std::size_t jobId, const AgentBounds& agentBounds, const TilePosition& changedTile) { diff --git a/components/detournavigator/asyncnavmeshupdater.hpp b/components/detournavigator/asyncnavmeshupdater.hpp index a4c446afc9..7877ff8082 100644 --- a/components/detournavigator/asyncnavmeshupdater.hpp +++ b/components/detournavigator/asyncnavmeshupdater.hpp @@ -248,8 +248,6 @@ namespace DetourNavigator void postThreadJob(JobIt job, std::deque& queue); - void writeDebugFiles(const Job& job, const RecastMesh* recastMesh) const; - bool lockTile(std::size_t jobId, const AgentBounds& agentBounds, const TilePosition& changedTile); void unlockTile(std::size_t jobId, const AgentBounds& agentBounds, const TilePosition& changedTile); From 6464d9913467c9667929de203c8d4b5bdc37f701 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 6 Jan 2025 23:05:58 +0100 Subject: [PATCH 003/154] Throw system error on open file failure --- components/detournavigator/debug.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/detournavigator/debug.cpp b/components/detournavigator/debug.cpp index 5ce1464bdd..3a3430c9d3 100644 --- a/components/detournavigator/debug.cpp +++ b/components/detournavigator/debug.cpp @@ -1,5 +1,4 @@ #include "debug.hpp" -#include "exceptions.hpp" #include "recastmesh.hpp" #include "settings.hpp" #include "settingsutils.hpp" @@ -17,7 +16,7 @@ #include #include #include -#include +#include namespace DetourNavigator { @@ -224,7 +223,8 @@ namespace DetourNavigator const auto path = pathPrefix + "recastmesh" + revision + ".obj"; std::ofstream file(std::filesystem::path(path), std::ios::out); if (!file.is_open()) - throw NavigatorException("Open file failed: " + path); + throw std::system_error( + errno, std::generic_category(), "Failed to open file to write recast mesh: " + path); file.exceptions(std::ios::failbit | std::ios::badbit); file.precision(std::numeric_limits::max_exponent10); std::vector vertices = recastMesh.getMesh().getVertices(); @@ -271,7 +271,7 @@ namespace DetourNavigator const auto path = pathPrefix + "all_tiles_navmesh" + revision + ".bin"; std::ofstream file(std::filesystem::path(path), std::ios::out | std::ios::binary); if (!file.is_open()) - throw NavigatorException("Open file failed: " + path); + throw std::system_error(errno, std::generic_category(), "Failed to open file to write navmesh: " + path); file.exceptions(std::ios::failbit | std::ios::badbit); NavMeshSetHeader header; From d4f4b3c3045a88512a1bef7637ea3e0629b9e8b1 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Wed, 8 Jan 2025 07:34:15 +0300 Subject: [PATCH 004/154] Fix default audio device switch for PulseAudio backend (#7731) --- apps/openmw/mwsound/openal_output.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openal_output.cpp index 3c3d6fb26e..aa68997d0e 100644 --- a/apps/openmw/mwsound/openal_output.cpp +++ b/apps/openmw/mwsound/openal_output.cpp @@ -417,19 +417,15 @@ namespace MWSound { { const std::lock_guard openLock(mOutput.mReopenMutex); - auto defaultName = getDeviceName(nullptr); + std::basic_string_view defaultName = getDeviceName(nullptr); if (mCurrentName != defaultName) { Log(Debug::Info) << "Default audio device changed"; - ALCboolean reopened - = alcReopenDeviceSOFT(mOutput.mDevice, nullptr, mOutput.mContextAttributes.data()); + ALCboolean reopened = alcReopenDeviceSOFT( + mOutput.mDevice, defaultName.data(), mOutput.mContextAttributes.data()); if (reopened == AL_FALSE) - { - mCurrentName = defaultName; Log(Debug::Warning) << "Failed to switch to new audio device"; - } - else - mCurrentName = getDeviceName(mOutput.mDevice); + mCurrentName = defaultName; } } mCondVar.wait_for(lock, std::chrono::seconds(2)); From 29af981345a92594ff3975ea0ee583738df93f34 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Thu, 9 Jan 2025 15:21:14 +0000 Subject: [PATCH 005/154] Don't give commas special meaning when matching comments to openmw.cfg values Previously, comments would be associated with the openmw.cfg line that followed them, but only up to the first comma. This meant that if you had fallback=thing,otherthing and fallback=thing,thirdthing, comments above the thirdthing line would be moved above the otherthing line, even though both lines would be kept when the file was written out. This seemed to be an attempt at a feature when cc9cii first implemented the comment preservation system, but it only seems to cause confusion. --- components/config/gamesettings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index 3210716a5c..bf8cdef933 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -325,7 +325,7 @@ bool Config::GameSettings::writeFileWithComments(QFile& file) // +----------------------------------------------------------+ // // - QRegularExpression settingRegex("^([^=]+)\\s*=\\s*([^,]+)(.*)$"); + QRegularExpression settingRegex("^([^=]+)\\s*=\\s*(.+?)\\s*$"); std::vector comments; auto commentStart = fileCopy.end(); std::map> commentsMap; From dd44b2668c02de1f65044edd042a8c3bdab83365 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Thu, 9 Jan 2025 03:31:09 +0300 Subject: [PATCH 006/154] Be prepared if someone feels like breaking getDeviceName --- apps/openmw/mwsound/openal_output.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openal_output.cpp index aa68997d0e..f829ea458a 100644 --- a/apps/openmw/mwsound/openal_output.cpp +++ b/apps/openmw/mwsound/openal_output.cpp @@ -420,12 +420,12 @@ namespace MWSound std::basic_string_view defaultName = getDeviceName(nullptr); if (mCurrentName != defaultName) { - Log(Debug::Info) << "Default audio device changed"; + mCurrentName = defaultName; + Log(Debug::Info) << "Default audio device changed to \"" << mCurrentName << "\""; ALCboolean reopened = alcReopenDeviceSOFT( - mOutput.mDevice, defaultName.data(), mOutput.mContextAttributes.data()); + mOutput.mDevice, mCurrentName.data(), mOutput.mContextAttributes.data()); if (reopened == AL_FALSE) Log(Debug::Warning) << "Failed to switch to new audio device"; - mCurrentName = defaultName; } } mCondVar.wait_for(lock, std::chrono::seconds(2)); From e1208b64e71b8150b0359086a73c1a11ca3e6578 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Thu, 9 Jan 2025 17:16:06 +0000 Subject: [PATCH 007/154] Update comments --- components/config/gamesettings.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index bf8cdef933..a5e71326b2 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -274,9 +274,8 @@ bool Config::GameSettings::isOrderedLine(const QString& line) // - Always ignore a line beginning with '#' or empty lines; added above a config // entry. // -// - If a line in file exists with matching key and first part of value (before ',', -// '\n', etc) also matches, then replace the line with that of mUserSettings. -// - else remove line +// - If a line in file exists with matching key and value, then replace the line with that of mUserSettings. +// - else if only the key matches, remove comment // // - If there is no corresponding line in file, add at the end // @@ -395,8 +394,7 @@ bool Config::GameSettings::writeFileWithComments(QFile& file) // look for a key in the line if (!match.hasMatch() || settingRegex.captureCount() < 2) { - // no key or first part of value found in line, replace with a null string which - // will be removed later + // no key or no value found in line, replace with a null string which will be removed later *iter = QString(); comments.clear(); commentStart = fileCopy.end(); From 383876a516b74595a5836eb013a723ad30dd4333 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sat, 11 Jan 2025 15:03:17 +0300 Subject: [PATCH 008/154] Handle weird post-processing chains gracefully (#8295) --- CHANGELOG.md | 1 + apps/openmw/mwrender/postprocessor.cpp | 10 +++++++--- apps/openmw/mwrender/postprocessor.hpp | 4 +++- components/fx/technique.cpp | 8 ++++++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fab893258f..fdbc118c2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -226,6 +226,7 @@ Bug #8231: AGOP doesn't like NiCollisionSwitch Bug #8237: Non-bipedal creatures should *not* use spellcast equip/unequip animations Bug #8252: Plugin dependencies are not required to be loaded + Bug #8295: Post-processing chain is case-sensitive Feature #1415: Infinite fall failsafe Feature #2566: Handle NAM9 records for manual cell references Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp index 8fdc0a45ca..4847da9cee 100644 --- a/apps/openmw/mwrender/postprocessor.cpp +++ b/apps/openmw/mwrender/postprocessor.cpp @@ -762,14 +762,18 @@ namespace MWRender if (Misc::StringUtils::ciEqual(technique->getName(), name)) return technique; + std::string realName = name; + auto fileIter = mTechniqueFileMap.find(name); + if (fileIter != mTechniqueFileMap.end()) + realName = fileIter->first; + auto technique = std::make_shared(*mVFS, *mRendering.getResourceSystem()->getImageManager(), - name, renderWidth(), renderHeight(), mUBO, mNormalsSupported); + std::move(realName), renderWidth(), renderHeight(), mUBO, mNormalsSupported); technique->compile(); if (technique->getStatus() != fx::Technique::Status::File_Not_exists) - technique->setLastModificationTime( - std::filesystem::last_write_time(mTechniqueFileMap[technique->getName()])); + technique->setLastModificationTime(std::filesystem::last_write_time(fileIter->second)); if (loadNextFrame) { diff --git a/apps/openmw/mwrender/postprocessor.hpp b/apps/openmw/mwrender/postprocessor.hpp index 2630467f95..af6eeae62b 100644 --- a/apps/openmw/mwrender/postprocessor.hpp +++ b/apps/openmw/mwrender/postprocessor.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "pingpongcanvas.hpp" #include "transparentpass.hpp" @@ -229,7 +230,8 @@ namespace MWRender TechniqueList mQueuedTemplates; TechniqueList mInternalTechniques; - std::unordered_map mTechniqueFileMap; + std::unordered_map + mTechniqueFileMap; RenderingManager& mRendering; osgViewer::Viewer* mViewer; diff --git a/components/fx/technique.cpp b/components/fx/technique.cpp index c99d30a1de..f56e0d498e 100644 --- a/components/fx/technique.cpp +++ b/components/fx/technique.cpp @@ -109,6 +109,14 @@ namespace fx { clear(); + if (std::ranges::count(mFilePath.value(), '/') > 1) + { + Log(Debug::Error) << "Could not load technique, invalid location '" << mFilePath << "'"; + + mStatus = Status::File_Not_exists; + return false; + } + if (!mVFS.exists(mFilePath)) { Log(Debug::Error) << "Could not load technique, file does not exist '" << mFilePath << "'"; From e908f28cb79bc5965fd79e1f710c0dab3ad9ffcf Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Sat, 11 Jan 2025 17:27:12 +0000 Subject: [PATCH 009/154] c h a n g e l o g --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fab893258f..738b17586c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -310,6 +310,7 @@ Feature #8109: Expose commitCrime to Lua API Feature #8130: Launcher: Add the ability to open a selected data directory in the file browser Feature #8145: Starter spell flag is not exposed + Feature #8287: Launcher: Special handling for comma in openmw.cfg entries is unintuitive and should be removed Task #5859: User openmw-cs.cfg has comment talking about settings.cfg Task #5896: Do not use deprecated MyGUI properties Task #6085: Replace boost::filesystem with std::filesystem From a4bc99db7a5e18d640596891d069078d92bb000f Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Sat, 11 Jan 2025 19:36:24 +0000 Subject: [PATCH 010/154] Install tools left out on Windows --- apps/bsatool/CMakeLists.txt | 4 ++++ apps/esmtool/CMakeLists.txt | 4 ++++ apps/niftest/CMakeLists.txt | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/apps/bsatool/CMakeLists.txt b/apps/bsatool/CMakeLists.txt index b2ad8f16b2..39065f1410 100644 --- a/apps/bsatool/CMakeLists.txt +++ b/apps/bsatool/CMakeLists.txt @@ -18,6 +18,10 @@ if (BUILD_WITH_CODE_COVERAGE) target_link_libraries(bsatool gcov) endif() +if (WIN32) + install(TARGETS bsatool RUNTIME DESTINATION ".") +endif() + if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(bsatool PRIVATE diff --git a/apps/esmtool/CMakeLists.txt b/apps/esmtool/CMakeLists.txt index 6dd592a4fe..963f4f5508 100644 --- a/apps/esmtool/CMakeLists.txt +++ b/apps/esmtool/CMakeLists.txt @@ -25,6 +25,10 @@ if (BUILD_WITH_CODE_COVERAGE) target_link_libraries(esmtool gcov) endif() +if (WIN32) + install(TARGETS esmtool RUNTIME DESTINATION ".") +endif() + if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(esmtool PRIVATE diff --git a/apps/niftest/CMakeLists.txt b/apps/niftest/CMakeLists.txt index cf37162f6e..da2cfac785 100644 --- a/apps/niftest/CMakeLists.txt +++ b/apps/niftest/CMakeLists.txt @@ -17,6 +17,10 @@ if (BUILD_WITH_CODE_COVERAGE) target_link_libraries(niftest gcov) endif() +if (WIN32) + install(TARGETS niftest RUNTIME DESTINATION ".") +endif() + if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(niftest PRIVATE ) endif() From ffedd62ea1aeaf200eee9c8374e746098e32fec3 Mon Sep 17 00:00:00 2001 From: Mehdi Yousfi-Monod Date: Sun, 12 Jan 2025 18:00:19 +0100 Subject: [PATCH 011/154] Lua: Allow creating arrows and bolt records (#8300) --- CMakeLists.txt | 2 +- apps/openmw/mwlua/types/weapon.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bcb27f7649..170a56aade 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 49) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 69) +set(OPENMW_LUA_API_REVISION 70) set(OPENMW_POSTPROCESSING_API_REVISION 2) set(OPENMW_VERSION_COMMITHASH "") diff --git a/apps/openmw/mwlua/types/weapon.cpp b/apps/openmw/mwlua/types/weapon.cpp index d5c52c8c4f..7fbfd5fb41 100644 --- a/apps/openmw/mwlua/types/weapon.cpp +++ b/apps/openmw/mwlua/types/weapon.cpp @@ -63,7 +63,7 @@ namespace if (rec["type"] != sol::nil) { int weaponType = rec["type"].get(); - if (weaponType >= 0 && weaponType <= ESM::Weapon::MarksmanThrown) + if (weaponType >= 0 && weaponType <= ESM::Weapon::Last) weapon.mData.mType = weaponType; else throw std::runtime_error("Invalid Weapon Type provided: " + std::to_string(weaponType)); From bc3c3bbc9c2c023e75e17092eef59a47307c74e8 Mon Sep 17 00:00:00 2001 From: Dave Corley Date: Sun, 12 Jan 2025 09:20:31 -0700 Subject: [PATCH 012/154] FIX: tooltips lose some of the relevant information if not stored as a QString --- apps/opencs/view/render/instancemode.cpp | 38 ++++++++++++------------ apps/opencs/view/render/instancemode.hpp | 1 + 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/apps/opencs/view/render/instancemode.cpp b/apps/opencs/view/render/instancemode.cpp index 7a59222eff..fb085f075a 100644 --- a/apps/opencs/view/render/instancemode.cpp +++ b/apps/opencs/view/render/instancemode.cpp @@ -60,24 +60,6 @@ #include "pagedworldspacewidget.hpp" #include "worldspacewidget.hpp" -namespace -{ - constexpr std::string_view sInstanceModeTooltip = R"( - Instance editing -
  • Use {scene-select-primary} and {scene-select-secondary} to select and unselect instances
  • -
  • Use {scene-edit-primary} to manipulate instances
  • -
  • Use {scene-select-tertiary} to select a reference object and then {scene-edit-secondary} to snap - selection relative to the reference object
  • -
  • Use {scene-submode-move}, {scene-submode-rotate}, {scene-submode-scale} to change to move, rotate, and - scale modes respectively
  • -
  • Use {scene-axis-x}, {scene-axis-y}, and {scene-axis-z} to lock changes to X, Y, and Z axes - respectively
  • -
  • Use {scene-delete} to delete currently selected objects
  • -
  • Use {scene-duplicate} to duplicate instances
  • -
  • Use {scene-instance-drop} to drop instances
-)"; -} - int CSVRender::InstanceMode::getSubModeFromId(const std::string& id) const { return id == "move" ? 0 : (id == "rotate" ? 1 : 2); @@ -312,10 +294,28 @@ void CSVRender::InstanceMode::setDragAxis(const char axis) mDragAxis = newDragAxis; } +QString CSVRender::InstanceMode::getTooltip() +{ + return QString( + "Instance editing" + "
  • Use {scene-select-primary} and {scene-select-secondary} to select and unselect instances
  • " + "
  • Use {scene-edit-primary} to manipulate instances
  • " + "
  • Use {scene-select-tertiary} to select a reference object and then {scene-edit-secondary} to snap " + "selection relative to the reference object
  • " + "
  • Use {scene-submode-move}, {scene-submode-rotate}, {scene-submode-scale} to change to move, " + "rotate, and " + "scale modes respectively
  • " + "
  • Use {scene-axis-x}, {scene-axis-y}, and {scene-axis-z} to lock changes to X, Y, and Z axes " + "respectively
  • " + "
  • Use {scene-delete} to delete currently selected objects
  • " + "
  • Use {scene-duplicate} to duplicate instances
  • " + "
  • Use {scene-instance-drop} to drop instances
"); +} + CSVRender::InstanceMode::InstanceMode( WorldspaceWidget* worldspaceWidget, osg::ref_ptr parentNode, QWidget* parent) : EditMode(worldspaceWidget, Misc::ScalableIcon::load(":scenetoolbar/editing-instance"), - Mask_Reference | Mask_Terrain, sInstanceModeTooltip.data(), parent) + Mask_Reference | Mask_Terrain, getTooltip(), parent) , mSubMode(nullptr) , mSubModeId("move") , mSelectionMode(nullptr) diff --git a/apps/opencs/view/render/instancemode.hpp b/apps/opencs/view/render/instancemode.hpp index 193423efd5..f4dd9d99ea 100644 --- a/apps/opencs/view/render/instancemode.hpp +++ b/apps/opencs/view/render/instancemode.hpp @@ -53,6 +53,7 @@ namespace CSVRender std::vector mObjectsAtDragStart; CSMWorld::IdTable* mSelectionGroups; + QString getTooltip(); int getSubModeFromId(const std::string& id) const; osg::Vec3 quatToEuler(const osg::Quat& quat) const; From d3fe31803f40042ac5998d87dbfa5e53d918432d Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 12 Jan 2025 17:41:48 +0300 Subject: [PATCH 013/154] Editor: Prevent crash on smoothing undefined cell borders (#8299) --- CHANGELOG.md | 1 + apps/opencs/view/render/terrainshapemode.cpp | 95 +++++++++++--------- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 511b2d5dbd..68fbad9555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -227,6 +227,7 @@ Bug #8237: Non-bipedal creatures should *not* use spellcast equip/unequip animations Bug #8252: Plugin dependencies are not required to be loaded Bug #8295: Post-processing chain is case-sensitive + Bug #8299: Crash while smoothing landscape Feature #1415: Infinite fall failsafe Feature #2566: Handle NAM9 records for manual cell references Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking diff --git a/apps/opencs/view/render/terrainshapemode.cpp b/apps/opencs/view/render/terrainshapemode.cpp index b9dc301efa..bc1c6c6365 100644 --- a/apps/opencs/view/render/terrainshapemode.cpp +++ b/apps/opencs/view/render/terrainshapemode.cpp @@ -761,18 +761,17 @@ void CSVRender::TerrainShapeMode::smoothHeight( // this = this Cell // left = x - 1, up = y - 1, right = x + 1, down = y + 1 // Altered = transient edit (in current edited) - float thisAlteredHeight = 0.0f; - if (paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) != nullptr) - thisAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY); float thisHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX]; - float leftHeight = 0.0f; - float leftAlteredHeight = 0.0f; - float upAlteredHeight = 0.0f; - float rightHeight = 0.0f; - float rightAlteredHeight = 0.0f; - float downHeight = 0.0f; - float downAlteredHeight = 0.0f; - float upHeight = 0.0f; + float* thisAlteredHeightPtr = paged->getCellAlteredHeight(cellCoords, inCellX, inCellY); + float thisAlteredHeight = thisAlteredHeightPtr != nullptr ? *thisAlteredHeightPtr : 0.f; + float leftHeight = thisHeight; + float leftAlteredHeight = thisAlteredHeight; + float rightHeight = thisHeight; + float rightAlteredHeight = thisAlteredHeight; + float downHeight = thisHeight; + float downAlteredHeight = thisAlteredHeight; + float upHeight = thisHeight; + float upAlteredHeight = thisAlteredHeight; if (allowLandShapeEditing(cellId)) { @@ -780,70 +779,80 @@ void CSVRender::TerrainShapeMode::smoothHeight( if (inCellX == 0) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY()); - const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer - = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) - .value(); - leftHeight = landLeftShapePointer[inCellY * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE - 2)]; - if (paged->getCellAlteredHeight(cellCoords.move(-1, 0), inCellX, ESM::Land::LAND_SIZE - 2)) - leftAlteredHeight - = *paged->getCellAlteredHeight(cellCoords.move(-1, 0), ESM::Land::LAND_SIZE - 2, inCellY); + if (isLandLoaded(cellId)) + { + const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); + leftHeight = landLeftShapePointer[inCellY * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE - 2)]; + float* alteredHeightPtr + = paged->getCellAlteredHeight(cellCoords.move(-1, 0), ESM::Land::LAND_SIZE - 2, inCellY); + leftAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; + } } if (inCellY == 0) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() - 1); - const CSMWorld::LandHeightsColumn::DataType landUpShapePointer - = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) - .value(); - upHeight = landUpShapePointer[(ESM::Land::LAND_SIZE - 2) * ESM::Land::LAND_SIZE + inCellX]; - if (paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2)) - upAlteredHeight - = *paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2); + if (isLandLoaded(cellId)) + { + const CSMWorld::LandHeightsColumn::DataType landUpShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); + upHeight = landUpShapePointer[(ESM::Land::LAND_SIZE - 2) * ESM::Land::LAND_SIZE + inCellX]; + float* alteredHeightPtr + = paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2); + upAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; + } } if (inCellX > 0) { leftHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX - 1]; - leftAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX - 1, inCellY); + float* alteredHeightPtr = paged->getCellAlteredHeight(cellCoords, inCellX - 1, inCellY); + leftAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; } if (inCellY > 0) { upHeight = landShapePointer[(inCellY - 1) * ESM::Land::LAND_SIZE + inCellX]; - upAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY - 1); + float* alteredHeightPtr = paged->getCellAlteredHeight(cellCoords, inCellX, inCellY - 1); + upAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; } if (inCellX == ESM::Land::LAND_SIZE - 1) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY()); - const CSMWorld::LandHeightsColumn::DataType landRightShapePointer - = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) - .value(); - rightHeight = landRightShapePointer[inCellY * ESM::Land::LAND_SIZE + 1]; - if (paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY)) + if (isLandLoaded(cellId)) { - rightAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY); + const CSMWorld::LandHeightsColumn::DataType landRightShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); + rightHeight = landRightShapePointer[inCellY * ESM::Land::LAND_SIZE + 1]; + float* alteredHeightPtr = paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY); + rightAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; } } if (inCellY == ESM::Land::LAND_SIZE - 1) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); - const CSMWorld::LandHeightsColumn::DataType landDownShapePointer - = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) - .value(); - downHeight = landDownShapePointer[1 * ESM::Land::LAND_SIZE + inCellX]; - if (paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1)) + if (isLandLoaded(cellId)) { - downAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1); + const CSMWorld::LandHeightsColumn::DataType landDownShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); + downHeight = landDownShapePointer[1 * ESM::Land::LAND_SIZE + inCellX]; + float* alteredHeightPtr = paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1); + downAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; } } if (inCellX < ESM::Land::LAND_SIZE - 1) { rightHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX + 1]; - if (paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY)) - rightAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY); + float* alteredHeightPtr = paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY); + rightAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; } if (inCellY < ESM::Land::LAND_SIZE - 1) { downHeight = landShapePointer[(inCellY + 1) * ESM::Land::LAND_SIZE + inCellX]; - if (paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1)) - downAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1); + float* alteredHeightPtr = paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1); + downAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; } float averageHeight = (upHeight + downHeight + rightHeight + leftHeight + upAlteredHeight From 806635b96c98fa9ad0d1d07f25ecc6f9b211afb7 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Fri, 10 Jan 2025 00:46:58 +0000 Subject: [PATCH 014/154] Don't unnecessarily overwrite openmw.cfg We don't need to risk reformatting the user's potentially-handwritten file if it parses to the same thing as we're about to write. --- components/config/gamesettings.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index a5e71326b2..f318cec4a4 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -301,6 +301,25 @@ bool Config::GameSettings::writeFileWithComments(QFile& file) if (fileCopy.empty()) return writeFile(stream); + QMultiMap existingSettings; + QString context = QFileInfo(file).absoluteDir().path(); + if (readFile(stream, existingSettings, context)) + { + // don't use QMultiMap operator== as mUserSettings may have blank context fields + // don't use one std::equal with custom predicate as (until Qt 6.4) there was no key-value iterator + if (std::equal(existingSettings.keyBegin(), existingSettings.keyEnd(), mUserSettings.keyBegin(), + mUserSettings.keyEnd()) + && std::equal(existingSettings.cbegin(), existingSettings.cend(), mUserSettings.cbegin(), + [](const SettingValue& l, const SettingValue& r) { + return l.originalRepresentation == r.originalRepresentation; + })) + { + // The existing file already contains what we need, don't risk scrambling comments and formatting + return true; + } + } + stream.seek(0); + // start // | // | +----------------------------------------------------------+ From 085c3b03a9d445bfcd65d23b943361b7c00aacd8 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Sat, 11 Jan 2025 17:24:35 +0000 Subject: [PATCH 015/154] c h a n g e l o g --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 511b2d5dbd..7e38f7381b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -311,6 +311,7 @@ Feature #8109: Expose commitCrime to Lua API Feature #8130: Launcher: Add the ability to open a selected data directory in the file browser Feature #8145: Starter spell flag is not exposed + Feature #8286: Launcher: Preserve semantically identical openmw.cfg Feature #8287: Launcher: Special handling for comma in openmw.cfg entries is unintuitive and should be removed Task #5859: User openmw-cs.cfg has comment talking about settings.cfg Task #5896: Do not use deprecated MyGUI properties From 9fc62be2c69c7e7d50843b806c27d9dff5317dcb Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Tue, 14 Jan 2025 19:39:24 +0300 Subject: [PATCH 016/154] Track ESM4 file loading progress --- apps/openmw/mwworld/esmloader.cpp | 2 +- apps/openmw/mwworld/esmstore.cpp | 11 +++++++++-- apps/openmw/mwworld/esmstore.hpp | 2 +- components/esm4/reader.hpp | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwworld/esmloader.cpp b/apps/openmw/mwworld/esmloader.cpp index ebdd40303a..833f152e13 100644 --- a/apps/openmw/mwworld/esmloader.cpp +++ b/apps/openmw/mwworld/esmloader.cpp @@ -70,7 +70,7 @@ namespace MWWorld mEncoder != nullptr ? &mEncoder->getStatelessEncoder() : nullptr); reader.setModIndex(index); reader.updateModIndices(mNameToIndex); - mStore.loadESM4(reader); + mStore.loadESM4(reader, listener); break; } } diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 15a687f4d7..ea183b6b53 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -468,9 +468,16 @@ namespace MWWorld } } - void ESMStore::loadESM4(ESM4::Reader& reader) + void ESMStore::loadESM4(ESM4::Reader& reader, Loading::Listener* listener) { - auto visitorRec = [this](ESM4::Reader& reader) { return ESMStoreImp::readRecord(reader, *this); }; + if (listener != nullptr) + listener->setProgressRange(::EsmLoader::fileProgress); + auto visitorRec = [this, listener](ESM4::Reader& reader) { + bool result = ESMStoreImp::readRecord(reader, *this); + if (listener != nullptr) + listener->setProgress(::EsmLoader::fileProgress * reader.getFileOffset() / reader.getFileSize()); + return result; + }; ESM4::ReaderUtils::readAll(reader, visitorRec, [](ESM4::Reader&) {}); } diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index d8cfd1dcdf..6c71ae0052 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -219,7 +219,7 @@ namespace MWWorld void validateDynamic(); void load(ESM::ESMReader& esm, Loading::Listener* listener, ESM::Dialogue*& dialogue); - void loadESM4(ESM4::Reader& esm); + void loadESM4(ESM4::Reader& esm, Loading::Listener* listener); template const Store& get() const diff --git a/components/esm4/reader.hpp b/components/esm4/reader.hpp index 914fa4a647..e8be484355 100644 --- a/components/esm4/reader.hpp +++ b/components/esm4/reader.hpp @@ -217,7 +217,7 @@ namespace ESM4 // Methods added for updating loading progress bars inline std::size_t getFileSize() const { return mFileSize; } - inline std::size_t getFileOffset() const { return mStream->tellg(); } + inline std::size_t getFileOffset() const { return mSavedStream ? mSavedStream->tellg() : mStream->tellg(); } // Methods added for saving/restoring context ReaderContext getContext(); // WARN: must be called immediately after reading the record header From 2a62dd728f293e1be21aee212658287165bade01 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Thu, 16 Jan 2025 01:02:16 +0300 Subject: [PATCH 017/154] Check if the victim is within weapon reach upon hit (#8280) --- apps/openmw/mwclass/creature.cpp | 8 ++++---- apps/openmw/mwclass/npc.cpp | 9 ++++----- apps/openmw/mwmechanics/combat.cpp | 24 ++++++++++++++++++++++-- apps/openmw/mwmechanics/combat.hpp | 4 ++++ 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 007338095f..9224f6f0d8 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -236,11 +236,8 @@ namespace MWClass } MWBase::World* world = MWBase::Environment::get().getWorld(); - const MWWorld::Store& store = world->getStore().get(); - float dist = store.find("fCombatDistance")->mValue.getFloat(); - if (!weapon.isEmpty()) - dist *= weapon.get()->mBase->mData.mReach; + const float dist = MWMechanics::getMeleeWeaponReach(ptr, weapon); const std::pair result = MWMechanics::getHitContact(ptr, dist); if (result.first.isEmpty()) // Didn't hit anything return true; @@ -281,6 +278,9 @@ namespace MWClass if (otherstats.isDead()) // Can't hit dead actors return; + if (!MWMechanics::isInMeleeReach(ptr, victim, MWMechanics::getMeleeWeaponReach(ptr, weapon))) + return; + if (!success) { victim.getClass().onHit( diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 22c953d27d..0b61436d11 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -572,12 +572,8 @@ namespace MWClass weapon = *weaponslot; MWBase::World* world = MWBase::Environment::get().getWorld(); - const MWWorld::Store& store = world->getStore().get(); - const float fCombatDistance = store.find("fCombatDistance")->mValue.getFloat(); - float dist = fCombatDistance - * (!weapon.isEmpty() ? weapon.get()->mBase->mData.mReach - : store.find("fHandToHandReach")->mValue.getFloat()); + const float dist = MWMechanics::getMeleeWeaponReach(ptr, weapon); const std::pair result = MWMechanics::getHitContact(ptr, dist); if (result.first.isEmpty()) // Didn't hit anything return true; @@ -615,6 +611,9 @@ namespace MWClass if (otherstats.isDead()) // Can't hit dead actors return; + if (!MWMechanics::isInMeleeReach(ptr, victim, MWMechanics::getMeleeWeaponReach(ptr, weapon))) + return; + if (ptr == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->setEnemy(victim); diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 5d283214a3..e7c7342284 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -578,6 +578,24 @@ namespace MWMechanics return dist; } + float getMeleeWeaponReach(const MWWorld::Ptr& actor, const MWWorld::Ptr& weapon) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::Store& store = world->getStore().get(); + const float fCombatDistance = store.find("fCombatDistance")->mValue.getFloat(); + if (!weapon.isEmpty()) + return fCombatDistance * weapon.get()->mBase->mData.mReach; + if (actor.getClass().isNpc()) + return fCombatDistance * store.find("fHandToHandReach")->mValue.getFloat(); + return fCombatDistance; + } + + bool isInMeleeReach(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const float reach) + { + const float heightDiff = actor.getRefData().getPosition().pos[2] - target.getRefData().getPosition().pos[2]; + return std::abs(heightDiff) < reach && getDistanceToBounds(actor, target) < reach; + } + std::pair getHitContact(const MWWorld::Ptr& actor, float reach) { // Lasciate ogne speranza, voi ch'entrate @@ -614,11 +632,13 @@ namespace MWMechanics { if (actor == target || target.getClass().getCreatureStats(target).isDead()) continue; + const float dist = getDistanceToBounds(actor, target); - const osg::Vec3f targetPos(target.getRefData().getPosition().asVec3()); - if (dist >= reach || dist >= minDist || std::abs(targetPos.z() - actorPos.z()) >= reach) + if (dist >= minDist || !isInMeleeReach(actor, target, reach)) continue; + const osg::Vec3f targetPos(target.getRefData().getPosition().asVec3()); + // Horizontal angle checks. osg::Vec2f actorToTargetXY{ targetPos.x() - actorPos.x(), targetPos.y() - actorPos.y() }; actorToTargetXY.normalize(); diff --git a/apps/openmw/mwmechanics/combat.hpp b/apps/openmw/mwmechanics/combat.hpp index 92033c7e77..fa3660a016 100644 --- a/apps/openmw/mwmechanics/combat.hpp +++ b/apps/openmw/mwmechanics/combat.hpp @@ -64,6 +64,10 @@ namespace MWMechanics // Cursed distance calculation used for combat proximity and hit checks in Morrowind float getDistanceToBounds(const MWWorld::Ptr& actor, const MWWorld::Ptr& target); + float getMeleeWeaponReach(const MWWorld::Ptr& actor, const MWWorld::Ptr& weapon); + + bool isInMeleeReach(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const float reach); + // Similarly cursed hit target selection std::pair getHitContact(const MWWorld::Ptr& actor, float reach); From af9a9a6d642ed62d9e3cadf46e323c39bb174995 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 20 Jan 2025 16:49:22 +0100 Subject: [PATCH 018/154] Remove superfluous includes from animblendrules.cpp --- components/sceneutil/animblendrules.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/components/sceneutil/animblendrules.cpp b/components/sceneutil/animblendrules.cpp index c812feb07e..c285988f40 100644 --- a/components/sceneutil/animblendrules.cpp +++ b/components/sceneutil/animblendrules.cpp @@ -1,17 +1,13 @@ #include "animblendrules.hpp" #include -#include +#include #include #include #include #include -#include -#include -#include -#include #include #include From e90c4187ff035475eaddc55333d92daae4698eac Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Mon, 20 Jan 2025 22:50:38 +0000 Subject: [PATCH 019/154] Pin awscli to 2.22.35 Because of https://github.com/aws/aws-cli/issues/9214, 2.23.0 and later won't work with our non-Amazon-hosted S3 buckets. --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f1da3eb43c..7499ab2287 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -540,7 +540,7 @@ macOS14_Xcode15_arm64: script: - apt-get update - apt-get install -y curl gcab unzip - - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscli-exe-linux-x86_64.zip + - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64-2.22.35.zip" -o awscli-exe-linux-x86_64.zip - unzip -d awscli-exe-linux-x86_64 awscli-exe-linux-x86_64.zip - pushd awscli-exe-linux-x86_64 - ./aws/install From 2d0f45ea41c2df2210bd06f06b93b8b89cf8468f Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Tue, 21 Jan 2025 17:15:10 +0000 Subject: [PATCH 020/154] Log awscli version 2.23.0 had breaking changes, so we need to know if we're using it, and be able to diagnose anything else caused by breaking changes in the future now they're a possibility. --- .github/workflows/windows.yml | 4 +++- .gitlab-ci.yml | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 82386e5292..b1c7c41b15 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -202,7 +202,9 @@ jobs: AWS_DEFAULT_REGION: eu-west-3 if: ${{ env.AWS_ACCESS_KEY_ID != '' && env.AWS_SECRET_ACCESS_KEY != '' && inputs.package }} working-directory: ${{ github.workspace }}/SymStore - run: aws --endpoint-url https://rgw.ctrl-c.liu.se s3 sync --size-only --exclude * --include *.ex_ --include *.dl_ --include *.pd_ . s3://openmw-sym + run: | + aws --version + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 sync --size-only --exclude * --include *.ex_ --include *.dl_ --include *.pd_ . s3://openmw-sym - name: Add install directory to PATH shell: bash diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7499ab2287..4a8f0f96b9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -510,6 +510,7 @@ Ubuntu_GCC_integration_tests_asan: - CI/before_script.osx.sh - cd build; make -j $(sysctl -n hw.logicalcpu) package - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME##*/}.dmg"; done + - aws --version - | if [[ -n "${AWS_ACCESS_KEY_ID}" ]]; then artifactDirectory="${CI_PROJECT_NAMESPACE//[\"<>|$'\t'\/\\?*]/_}/${CI_COMMIT_REF_NAME//[\"<>|$'\t'\/\\?*]/_}/${CI_COMMIT_SHORT_SHA//[\"<>|$'\t'\/\\?*]/_}-${CI_JOB_ID//[\"<>|$'\t'\/\\?*]/_}/" @@ -621,6 +622,7 @@ macOS14_Xcode15_arm64: - echo "CI_COMMIT_REF_NAME ${CI_COMMIT_REF_NAME}`nCI_JOB_ID ${CI_JOB_ID}`nCI_COMMIT_SHA ${CI_COMMIT_SHA}" | Out-File -Encoding UTF8 CI-ID.txt - $artifactDirectory = "$(Make-SafeFileName("${CI_PROJECT_NAMESPACE}"))/$(Make-SafeFileName("${CI_COMMIT_REF_NAME}"))/$(Make-SafeFileName("${CI_COMMIT_SHORT_SHA}-${CI_JOB_ID}"))/" - Get-ChildItem -Recurse *.ilk | Remove-Item + - aws --version - | if (Get-ChildItem -Recurse *.pdb) { 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt @@ -767,6 +769,7 @@ macOS14_Xcode15_arm64: - echo "CI_COMMIT_REF_NAME ${CI_COMMIT_REF_NAME}`nCI_JOB_ID ${CI_JOB_ID}`nCI_COMMIT_SHA ${CI_COMMIT_SHA}" | Out-File -Encoding UTF8 CI-ID.txt - $artifactDirectory = "$(Make-SafeFileName("${CI_PROJECT_NAMESPACE}"))/$(Make-SafeFileName("${CI_COMMIT_REF_NAME}"))/$(Make-SafeFileName("${CI_COMMIT_SHORT_SHA}-${CI_JOB_ID}"))/" - Get-ChildItem -Recurse *.ilk | Remove-Item + - aws --version - | if (Get-ChildItem -Recurse *.pdb) { 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt From 57ffc11fbaef391fe92ed532bc9c3b86d2d30dc9 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Tue, 21 Jan 2025 18:47:54 +0000 Subject: [PATCH 021/154] Try installing specific version of awscli on MacOS --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4a8f0f96b9..45d8d9ed30 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -503,6 +503,7 @@ Ubuntu_GCC_integration_tests_asan: - ccache/ script: - CI/before_install.osx.sh + - brew install awscli@2.22.35 - export CCACHE_BASEDIR="$(pwd)" - export CCACHE_DIR="$(pwd)/ccache" - mkdir -pv "${CCACHE_DIR}" From 9ae12baee1c8e568831023dd74144c8c52203572 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Tue, 21 Jan 2025 20:44:08 +0000 Subject: [PATCH 022/154] Pin it on Windows, too --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 45d8d9ed30..63c650945c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -589,7 +589,7 @@ macOS14_Xcode15_arm64: - choco install vswhere -y - choco install ninja -y - choco install python -y - - choco install awscli -y + - choco install awscli -y --version=2.22.35 - refreshenv - | function Make-SafeFileName { @@ -741,7 +741,7 @@ macOS14_Xcode15_arm64: - choco install 7zip -y - choco install vswhere -y - choco install python -y - - choco install awscli -y + - choco install awscli -y --version=2.22.35 - refreshenv - | function Make-SafeFileName { From 2762be9f85e5b12a18d53536538defc9d2376e21 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Sun, 26 Jan 2025 14:57:34 +0100 Subject: [PATCH 023/154] opaque depth texture must account for multiview --- files/shaders/compatibility/bs/default.frag | 4 +--- files/shaders/compatibility/bs/nolighting.frag | 3 +-- files/shaders/compatibility/objects.frag | 6 ++---- files/shaders/lib/core/fragment.glsl | 7 +++++++ files/shaders/lib/core/fragment.h.glsl | 2 ++ files/shaders/lib/core/fragment_multiview.glsl | 8 ++++++++ 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/files/shaders/compatibility/bs/default.frag b/files/shaders/compatibility/bs/default.frag index 5068c01d80..3d2ad07d1d 100644 --- a/files/shaders/compatibility/bs/default.frag +++ b/files/shaders/compatibility/bs/default.frag @@ -26,8 +26,6 @@ uniform sampler2D normalMap; varying vec2 normalMapUV; #endif -uniform sampler2D opaqueDepthTex; - varying float euclideanDepth; varying float linearDepth; @@ -59,7 +57,7 @@ void main() #if defined(DISTORTION) && DISTORTION vec2 screenCoords = gl_FragCoord.xy / (screenRes * @distorionRTRatio); gl_FragData[0].a *= getDiffuseColor().a; - gl_FragData[0] = applyDistortion(gl_FragData[0], distortionStrength, gl_FragCoord.z, texture2D(opaqueDepthTex, screenCoords).x); + gl_FragData[0] = applyDistortion(gl_FragData[0], distortionStrength, gl_FragCoord.z, sampleOpaqueDepthTex(screenCoords).x); return; #endif diff --git a/files/shaders/compatibility/bs/nolighting.frag b/files/shaders/compatibility/bs/nolighting.frag index c9e3ca4e13..da267d7858 100644 --- a/files/shaders/compatibility/bs/nolighting.frag +++ b/files/shaders/compatibility/bs/nolighting.frag @@ -35,7 +35,6 @@ uniform float alphaRef; #if @softParticles #include "lib/particle/soft.glsl" -uniform sampler2D opaqueDepthTex; uniform float particleSize; uniform bool particleFade; uniform float softFalloffDepth; @@ -70,7 +69,7 @@ void main() viewNormal, near, far, - texture2D(opaqueDepthTex, screenCoords).x, + sampleOpaqueDepthTex(screenCoords).x, particleSize, particleFade, softFalloffDepth diff --git a/files/shaders/compatibility/objects.frag b/files/shaders/compatibility/objects.frag index 8bd9664b41..999079a31d 100644 --- a/files/shaders/compatibility/objects.frag +++ b/files/shaders/compatibility/objects.frag @@ -113,8 +113,6 @@ uniform sampler2D orthoDepthMap; varying vec3 orthoDepthMapCoord; #endif -uniform sampler2D opaqueDepthTex; - void main() { #if @particleOcclusion @@ -143,7 +141,7 @@ vec2 screenCoords = gl_FragCoord.xy / screenRes; #if defined(DISTORTION) && DISTORTION gl_FragData[0].a *= getDiffuseColor().a; - gl_FragData[0] = applyDistortion(gl_FragData[0], distortionStrength, gl_FragCoord.z, texture2D(opaqueDepthTex, screenCoords / @distorionRTRatio).x); + gl_FragData[0] = applyDistortion(gl_FragData[0], distortionStrength, gl_FragCoord.z, sampleOpaqueDepthTex(screenCoords / @distorionRTRatio).x); return; #endif @@ -258,7 +256,7 @@ vec2 screenCoords = gl_FragCoord.xy / screenRes; viewNormal, near, far, - texture2D(opaqueDepthTex, screenCoords).x, + sampleOpaqueDepthTex(screenCoords).x, particleSize, particleFade, softFalloffDepth diff --git a/files/shaders/lib/core/fragment.glsl b/files/shaders/lib/core/fragment.glsl index 9b983148cb..d58e17feaf 100644 --- a/files/shaders/lib/core/fragment.glsl +++ b/files/shaders/lib/core/fragment.glsl @@ -40,3 +40,10 @@ vec3 sampleSkyColor(vec2 uv) return texture2D(sky, uv).xyz; } #endif + +uniform sampler2D opaqueDepthTex; + +vec4 sampleOpaqueDepthTex(vec2 uv) +{ + return texture2D(opaqueDepthTex, uv); +} diff --git a/files/shaders/lib/core/fragment.h.glsl b/files/shaders/lib/core/fragment.h.glsl index b8c3f9a32b..9b7ef768e6 100644 --- a/files/shaders/lib/core/fragment.h.glsl +++ b/files/shaders/lib/core/fragment.h.glsl @@ -17,4 +17,6 @@ vec4 samplerLastShader(vec2 uv); vec3 sampleSkyColor(vec2 uv); #endif +vec4 sampleOpaqueDepthTex(vec2 uv); + #endif // OPENMW_FRAGMENT_H_GLSL diff --git a/files/shaders/lib/core/fragment_multiview.glsl b/files/shaders/lib/core/fragment_multiview.glsl index 2880087104..767a3e04bd 100644 --- a/files/shaders/lib/core/fragment_multiview.glsl +++ b/files/shaders/lib/core/fragment_multiview.glsl @@ -2,6 +2,7 @@ #extension GL_OVR_multiview : require #extension GL_OVR_multiview2 : require +#extension GL_EXT_texture_array : require #include "lib/core/fragment.h.glsl" @@ -44,3 +45,10 @@ vec3 sampleSkyColor(vec2 uv) return texture(sky, vec3((uv), gl_ViewID_OVR)).xyz; } #endif + +uniform sampler2DArray opaqueDepthTex; + +vec4 sampleOpaqueDepthTex(vec2 uv) +{ + return texture2DArray(opaqueDepthTex, vec3((uv), gl_ViewID_OVR)); +} From 6071de9d1d647a43182716df22e579558b7bbc00 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Tue, 1 Oct 2024 19:18:11 +0200 Subject: [PATCH 024/154] Set a dummy texture for sky blending, when multiview is enabled. --- apps/openmw/mwrender/characterpreview.cpp | 19 ++++++++++++++++++- apps/openmw/mwrender/localmap.cpp | 18 +++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 123eadfdec..b9ebfd0c52 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -258,12 +259,28 @@ namespace MWRender // TODO: Clean up this mess of loose uniforms that shaders depend on. // turn off sky blending + int skyTextureSlot = mResourceSystem->getSceneManager()->getShaderManager().reserveGlobalTextureUnits(Shader::ShaderManager::Slot::SkyTexture); stateset->addUniform(new osg::Uniform("far", 10000000.0f)); stateset->addUniform(new osg::Uniform("skyBlendingStart", 8000000.0f)); - stateset->addUniform(new osg::Uniform("sky", 0)); + stateset->addUniform(new osg::Uniform("sky", skyTextureSlot)); stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{ 1, 1 })); + if (Stereo::getMultiview()) + { + // The above set the sky texture unit to 0. Normally this is fine since texture unit 0 is the sampler2d diffuseMap, which will be set. + // However, in multiview the sky texture is a sampler2darray, and so needs to be set separately with a dummy texture + osg::Texture2DArray* textureArray = new osg::Texture2DArray; + textureArray->setTextureSize(1, 1, 2); + textureArray->setName("fakeSkyTexture"); + textureArray->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + textureArray->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + textureArray->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + textureArray->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + textureArray->setWrap(osg::Texture::WRAP_R, osg::Texture::CLAMP_TO_EDGE); + stateset->setTextureAttributeAndModes(skyTextureSlot, textureArray, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED); + } stateset->addUniform(new osg::Uniform("emissiveMult", 1.f)); + // Opaque stuff must have 1 as its fragment alpha as the FBO is translucent, so having blending off isn't enough osg::ref_ptr noBlendAlphaEnv = new osg::TexEnvCombine(); diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 968903f6a3..dcc2e3d887 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -734,10 +735,25 @@ namespace MWRender stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); // turn of sky blending + int skyTextureSlot = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getShaderManager().reserveGlobalTextureUnits(Shader::ShaderManager::Slot::SkyTexture); stateset->addUniform(new osg::Uniform("far", 10000000.0f)); stateset->addUniform(new osg::Uniform("skyBlendingStart", 8000000.0f)); - stateset->addUniform(new osg::Uniform("sky", 0)); + stateset->addUniform(new osg::Uniform("sky", skyTextureSlot)); stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{ 1, 1 })); + if (Stereo::getMultiview()) + { + // The above set the sky texture unit to 0. Normally this is fine since texture unit 0 is the sampler2d diffuseMap, which will be set. + // However, in multiview the sky texture is a sampler2darray, and so needs to be set separately with a dummy texture + osg::Texture2DArray* textureArray = new osg::Texture2DArray; + textureArray->setTextureSize(1, 1, 2); + textureArray->setName("fakeSkyTexture"); + textureArray->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + textureArray->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + textureArray->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + textureArray->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + textureArray->setWrap(osg::Texture::WRAP_R, osg::Texture::CLAMP_TO_EDGE); + stateset->setTextureAttributeAndModes(skyTextureSlot, textureArray, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED); + } osg::ref_ptr lightmodel = new osg::LightModel; lightmodel->setAmbientIntensity(osg::Vec4(0.3f, 0.3f, 0.3f, 1.f)); From a2f5e1c07545e21825770044c86ba5bf37b11d01 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Sun, 26 Jan 2025 16:23:56 +0100 Subject: [PATCH 025/154] Fix multiview use in techniques --- components/fx/pass.cpp | 21 +++++++++++++++---- components/fx/technique.cpp | 2 +- files/data/shaders/internal_distortion.omwfx | 4 ++-- .../compatibility/multiview_resolve.frag | 2 +- .../shaders/lib/core/fragment_multiview.glsl | 2 +- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/components/fx/pass.cpp b/components/fx/pass.cpp index cf50d20fe2..c55bee76e3 100644 --- a/components/fx/pass.cpp +++ b/components/fx/pass.cpp @@ -81,6 +81,7 @@ namespace fx #define omw_Position @position #define omw_Texture1D @texture1D #define omw_Texture2D @texture2D +#define omw_Texture2DArray @texture2DArray #define omw_Texture3D @texture3D #define omw_Vertex @vertex #define omw_FragColor @fragColor @@ -154,7 +155,7 @@ mat4 omw_InvProjectionMatrix() float omw_GetDepth(vec2 uv) { #if OMW_MULTIVIEW - float depth = omw_Texture2D(omw_SamplerDepth, vec3(uv, gl_ViewID_OVR)).r; + float depth = omw_Texture2DArray(omw_SamplerDepth, vec3(uv, gl_ViewID_OVR)).r; #else float depth = omw_Texture2D(omw_SamplerDepth, uv).r; #endif @@ -165,10 +166,19 @@ mat4 omw_InvProjectionMatrix() #endif } + vec4 omw_GetDistortion(vec2 uv) + { +#if OMW_MULTIVIEW + return omw_Texture2DArray(omw_SamplerDistortion, vec3(uv, gl_ViewID_OVR)); +#else + return omw_Texture2D(omw_SamplerDistortion, uv); +#endif + } + vec4 omw_GetLastShader(vec2 uv) { #if OMW_MULTIVIEW - return omw_Texture2D(omw_SamplerLastShader, vec3(uv, gl_ViewID_OVR)); + return omw_Texture2DArray(omw_SamplerLastShader, vec3(uv, gl_ViewID_OVR)); #else return omw_Texture2D(omw_SamplerLastShader, uv); #endif @@ -177,7 +187,7 @@ mat4 omw_InvProjectionMatrix() vec4 omw_GetLastPass(vec2 uv) { #if OMW_MULTIVIEW - return omw_Texture2D(omw_SamplerLastPass, vec3(uv, gl_ViewID_OVR)); + return omw_Texture2DArray(omw_SamplerLastPass, vec3(uv, gl_ViewID_OVR)); #else return omw_Texture2D(omw_SamplerLastPass, uv); #endif @@ -186,7 +196,7 @@ mat4 omw_InvProjectionMatrix() vec3 omw_GetNormals(vec2 uv) { #if OMW_MULTIVIEW - return omw_Texture2D(omw_SamplerNormals, vec3(uv, gl_ViewID_OVR)).rgb * 2.0 - 1.0; + return omw_Texture2DArray(omw_SamplerNormals, vec3(uv, gl_ViewID_OVR)).rgb * 2.0 - 1.0; #else return omw_Texture2D(omw_SamplerNormals, uv).rgb * 2.0 - 1.0; #endif @@ -275,6 +285,9 @@ float omw_EstimateFogCoverageFromUV(vec2 uv) { "@hdr", technique.getHDR() ? "1" : "0" }, { "@in", mLegacyGLSL ? "varying" : "in" }, { "@out", mLegacyGLSL ? "varying" : "out" }, { "@position", "gl_Position" }, { "@texture1D", mLegacyGLSL ? "texture1D" : "texture" }, + // Note, @texture2DArray must be defined before @texture2D since @texture2D is a perfect prefix of + // texture2DArray + { "@texture2DArray", mLegacyGLSL ? "texture2DArray" : "texture" }, { "@texture2D", mLegacyGLSL ? "texture2D" : "texture" }, { "@texture3D", mLegacyGLSL ? "texture3D" : "texture" }, { "@vertex", mLegacyGLSL ? "gl_Vertex" : "_omw_Vertex" }, diff --git a/components/fx/technique.cpp b/components/fx/technique.cpp index f56e0d498e..5963e274ec 100644 --- a/components/fx/technique.cpp +++ b/components/fx/technique.cpp @@ -85,7 +85,7 @@ namespace fx mDescription = {}; mVersion = {}; mGLSLExtensions.clear(); - mGLSLVersion = mUBO ? 330 : 120; + mGLSLVersion = (mUBO || Stereo::getMultiview()) ? 330 : 120; mGLSLProfile.clear(); mDynamic = false; } diff --git a/files/data/shaders/internal_distortion.omwfx b/files/data/shaders/internal_distortion.omwfx index b641bb6711..b8e62ff229 100644 --- a/files/data/shaders/internal_distortion.omwfx +++ b/files/data/shaders/internal_distortion.omwfx @@ -6,11 +6,11 @@ fragment main { { const float multiplier = 0.14; - vec2 offset = omw_Texture2D(omw_SamplerDistortion, omw_TexCoord).rg; + vec2 offset = omw_GetDistortion(omw_TexCoord).rg; offset *= multiplier; offset = clamp(offset, vec2(-1.0), vec2(1.0)); - float occlusionFactor = omw_Texture2D(omw_SamplerDistortion, omw_TexCoord+offset).b; + float occlusionFactor = omw_GetDistortion(omw_TexCoord+offset).b; omw_FragColor = mix(omw_GetLastShader(omw_TexCoord + offset), omw_GetLastShader(omw_TexCoord), occlusionFactor); } diff --git a/files/shaders/compatibility/multiview_resolve.frag b/files/shaders/compatibility/multiview_resolve.frag index a77a3b35d1..9aeb54314b 100644 --- a/files/shaders/compatibility/multiview_resolve.frag +++ b/files/shaders/compatibility/multiview_resolve.frag @@ -1,4 +1,4 @@ -#version 120 +#version 330 #extension GL_EXT_texture_array : require varying vec2 uv; diff --git a/files/shaders/lib/core/fragment_multiview.glsl b/files/shaders/lib/core/fragment_multiview.glsl index 767a3e04bd..e29c5afbc9 100644 --- a/files/shaders/lib/core/fragment_multiview.glsl +++ b/files/shaders/lib/core/fragment_multiview.glsl @@ -50,5 +50,5 @@ uniform sampler2DArray opaqueDepthTex; vec4 sampleOpaqueDepthTex(vec2 uv) { - return texture2DArray(opaqueDepthTex, vec3((uv), gl_ViewID_OVR)); + return texture(opaqueDepthTex, vec3((uv), gl_ViewID_OVR)); } From ea51c55d007c9d070950a9b4d610b9148d580051 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Sat, 18 Jan 2025 16:11:47 +0100 Subject: [PATCH 026/154] Restore valid per view shadow settings. --- components/sceneutil/mwshadowtechnique.cpp | 128 +++++++++++++-------- components/sceneutil/mwshadowtechnique.hpp | 10 +- 2 files changed, 89 insertions(+), 49 deletions(-) diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp index 9fd435f40d..929ffadcbf 100644 --- a/components/sceneutil/mwshadowtechnique.cpp +++ b/components/sceneutil/mwshadowtechnique.cpp @@ -986,9 +986,12 @@ void SceneUtil::MWShadowTechnique::copyShadowMap(osgUtil::CullVisitor& cv, ViewD lhs_sd->_camera = rhs_sd->_camera; lhs_sd->_textureUnit = rhs_sd->_textureUnit; lhs_sd->_texture = rhs_sd->_texture; + lhs_sd->_sm_i = rhs_sd->_sm_i; sdl.push_back(lhs_sd); } + copyShadowStateSettings(cv, lhs); + if (lhs->_numValidShadows > 0) { prepareStateSetForRenderingShadow(*lhs, cv.getTraversalNumber()); @@ -1000,6 +1003,15 @@ void SceneUtil::MWShadowTechnique::setCustomFrustumCallback(CustomFrustumCallbac _customFrustumCallback = cfc; } +void SceneUtil::MWShadowTechnique::copyShadowStateSettings(osgUtil::CullVisitor& cv, ViewDependentData* vdd) +{ + for (const auto& sd : vdd->getShadowDataList()) + { + //assignTexGenSettings(&cv, sd->_camera, sd->_textureUnit, sd->_texgen); + assignValidRegionSettings(cv, sd->_camera, sd->_sm_i, vdd->_uniforms[cv.getTraversalNumber()%2]); + assignShadowStateSettings(cv, sd->_camera, sd->_sm_i, vdd->_uniforms[cv.getTraversalNumber()%2]); + } +} void MWShadowTechnique::update(osg::NodeVisitor& nv) { @@ -1053,6 +1065,8 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) _shadowedScene->osg::Group::traverse(cv); return; } + + Uniforms& vddUniforms = vdd->_uniforms[cv.getTraversalNumber() % 2]; ShadowSettings* settings = getShadowedScene()->getShadowSettings(); @@ -1498,31 +1512,10 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) cv.popStateSet(); + if (!orthographicViewFrustum && settings->getShadowMapProjectionHint()==ShadowSettings::PERSPECTIVE_SHADOW_MAP) { - { - osg::Matrix validRegionMatrix = cv.getCurrentCamera()->getInverseViewMatrix() * camera->getViewMatrix() * camera->getProjectionMatrix(); - - std::string validRegionUniformName = "validRegionMatrix" + std::to_string(sm_i); - osg::ref_ptr validRegionUniform; - - for (const auto & uniform : _uniforms[cv.getTraversalNumber() % 2]) - { - if (uniform->getName() == validRegionUniformName) - { - validRegionUniform = uniform; - break; - } - } - - if (!validRegionUniform) - { - validRegionUniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, validRegionUniformName); - _uniforms[cv.getTraversalNumber() % 2].push_back(validRegionUniform); - } - - validRegionUniform->set(validRegionMatrix); - } + assignValidRegionSettings(cv, camera, sm_i, vddUniforms); if (settings->getMultipleShadowMapHint() == ShadowSettings::CASCADED) adjustPerspectiveShadowMapCameraSettings(vdsmCallback->getRenderStage(), frustum, pl, camera.get(), cascaseNear, cascadeFar); @@ -1537,31 +1530,7 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) // 4.4 compute main scene graph TexGen + uniform settings + setup state // { - osg::Matrix shadowSpaceMatrix = cv.getCurrentCamera()->getInverseViewMatrix() * - camera->getViewMatrix() * - camera->getProjectionMatrix() * - osg::Matrix::translate(1.0,1.0,1.0) * - osg::Matrix::scale(0.5,0.5,0.5); - - std::string shadowSpaceUniformName = "shadowSpaceMatrix" + std::to_string(sm_i); - osg::ref_ptr shadowSpaceUniform; - - for (const auto & uniform : _uniforms[cv.getTraversalNumber() % 2]) - { - if (uniform->getName() == shadowSpaceUniformName) - { - shadowSpaceUniform = uniform; - break; - } - } - - if (!shadowSpaceUniform) - { - shadowSpaceUniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, shadowSpaceUniformName); - _uniforms[cv.getTraversalNumber() % 2].push_back(shadowSpaceUniform); - } - - shadowSpaceUniform->set(shadowSpaceMatrix); + assignShadowStateSettings(cv, camera, sm_i, vddUniforms); } // mark the light as one that has active shadows and requires shaders @@ -1569,6 +1538,7 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) // pass on shadow data to ShadowDataList sd->_textureUnit = textureUnit; + sd->_sm_i = sm_i; sdl.push_back(sd); @@ -3080,6 +3050,62 @@ bool MWShadowTechnique::adjustPerspectiveShadowMapCameraSettings(osgUtil::Render return true; } +void MWShadowTechnique::assignShadowStateSettings(osgUtil::CullVisitor& cv, osg::Camera* camera, unsigned int sm_i, Uniforms& uniforms) +{ + osg::Matrix inverseViewMatrix = osg::Matrix::inverse(*cv.getModelViewMatrix()); + osg::Matrix shadowSpaceMatrix = inverseViewMatrix * + camera->getViewMatrix() * + camera->getProjectionMatrix() * + osg::Matrix::translate(1.0,1.0,1.0) * + osg::Matrix::scale(0.5,0.5,0.5); + + std::string shadowSpaceUniformName = "shadowSpaceMatrix" + std::to_string(sm_i); + osg::ref_ptr shadowSpaceUniform; + + for (const auto & uniform : uniforms) + { + if (uniform->getName() == shadowSpaceUniformName) + { + shadowSpaceUniform = uniform; + break; + } + } + + if (!shadowSpaceUniform) + { + shadowSpaceUniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, shadowSpaceUniformName); + uniforms.push_back(shadowSpaceUniform); + } + + shadowSpaceUniform->set(shadowSpaceMatrix); + return; +} + +void SceneUtil::MWShadowTechnique::assignValidRegionSettings(osgUtil::CullVisitor & cv, osg::Camera* camera, unsigned int sm_i, Uniforms & uniforms) +{ + osg::Matrix validRegionMatrix = osg::Matrix::inverse(*cv.getModelViewMatrix()) * camera->getViewMatrix() * camera->getProjectionMatrix(); + + std::string validRegionUniformName = "validRegionMatrix" + std::to_string(sm_i); + osg::ref_ptr validRegionUniform; + + for (const auto & uniform : uniforms) + { + if (uniform->getName() == validRegionUniformName) + { + validRegionUniform = uniform; + break; + } + } + + if (!validRegionUniform) + { + validRegionUniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, validRegionUniformName); + uniforms.push_back(validRegionUniform); + } + + validRegionUniform->set(validRegionMatrix); +} + void MWShadowTechnique::cullShadowReceivingScene(osgUtil::CullVisitor* cv) const { OSG_INFO<<"cullShadowReceivingScene()"<addUniform(uniform); } + for(const auto& uniform : vdd._uniforms[traversalNumber % 2]) + { + OSG_INFO<<"addUniform("<getName()<<")"<addUniform(uniform); + } + if (_program.valid()) { stateset->setAttribute(_program.get()); diff --git a/components/sceneutil/mwshadowtechnique.hpp b/components/sceneutil/mwshadowtechnique.hpp index 8b39818e2e..4b9de9364a 100644 --- a/components/sceneutil/mwshadowtechnique.hpp +++ b/components/sceneutil/mwshadowtechnique.hpp @@ -157,6 +157,7 @@ namespace SceneUtil { * the resulting shadow map may be invalid. */ virtual void operator()(osgUtil::CullVisitor& cv, osg::BoundingBoxd& customClipSpace, osgUtil::CullVisitor*& sharedFrustumHint) = 0; }; + typedef std::vector< osg::ref_ptr > Uniforms; // forward declare class ViewDependentData; @@ -192,6 +193,7 @@ namespace SceneUtil { ViewDependentData* _viewDependentData; unsigned int _textureUnit; + unsigned int _sm_i; osg::ref_ptr _texture; osg::ref_ptr _camera; }; @@ -228,6 +230,7 @@ namespace SceneUtil { LightDataList _lightDataList; ShadowDataList _shadowDataList; + std::array _uniforms; unsigned int _numValidShadows; }; @@ -240,6 +243,8 @@ namespace SceneUtil { void setCustomFrustumCallback(CustomFrustumCallback* cfc); + void copyShadowStateSettings(osgUtil::CullVisitor& cv, ViewDependentData* vdd); + virtual void createShaders(); virtual bool selectActiveLights(osgUtil::CullVisitor* cv, ViewDependentData* vdd) const; @@ -251,6 +256,10 @@ namespace SceneUtil { virtual bool cropShadowCameraToMainFrustum(Frustum& frustum, osg::Camera* camera, double viewNear, double viewFar, std::vector& planeList); virtual bool adjustPerspectiveShadowMapCameraSettings(osgUtil::RenderStage* renderStage, Frustum& frustum, LightData& positionedLight, osg::Camera* camera, double viewNear, double viewFar); + + virtual void assignShadowStateSettings(osgUtil::CullVisitor& cv, osg::Camera* camera, unsigned int sm_i, Uniforms& uniforms); + + virtual void assignValidRegionSettings(osgUtil::CullVisitor& cv, osg::Camera* camera, unsigned int sm_i, Uniforms& uniforms); virtual void cullShadowReceivingScene(osgUtil::CullVisitor* cv) const; @@ -280,7 +289,6 @@ namespace SceneUtil { osg::ref_ptr _fallbackBaseTexture; osg::ref_ptr _fallbackShadowMapTexture; - typedef std::vector< osg::ref_ptr > Uniforms; std::array _uniforms; osg::ref_ptr _program; From a1df9afc9a6909a934fcaf4a02bf438499723490 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Sun, 26 Jan 2025 17:05:42 +0100 Subject: [PATCH 027/154] Formatting changes --- apps/openmw/mwrender/characterpreview.cpp | 4 +-- apps/openmw/mwrender/localmap.cpp | 38 +++++++++++++--------- components/sceneutil/mwshadowtechnique.cpp | 1 - 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index b9ebfd0c52..3c43227013 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -266,8 +266,7 @@ namespace MWRender stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{ 1, 1 })); if (Stereo::getMultiview()) { - // The above set the sky texture unit to 0. Normally this is fine since texture unit 0 is the sampler2d diffuseMap, which will be set. - // However, in multiview the sky texture is a sampler2darray, and so needs to be set separately with a dummy texture + // Multiview needs a texture2DArray dummy texture applied here osg::Texture2DArray* textureArray = new osg::Texture2DArray; textureArray->setTextureSize(1, 1, 2); textureArray->setName("fakeSkyTexture"); @@ -280,7 +279,6 @@ namespace MWRender } stateset->addUniform(new osg::Uniform("emissiveMult", 1.f)); - // Opaque stuff must have 1 as its fragment alpha as the FBO is translucent, so having blending off isn't enough osg::ref_ptr noBlendAlphaEnv = new osg::TexEnvCombine(); diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index dcc2e3d887..afa29b3f93 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -5,10 +5,10 @@ #include #include #include -#include #include #include #include +#include #include @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include #include #include @@ -735,25 +737,29 @@ namespace MWRender stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); // turn of sky blending - int skyTextureSlot = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getShaderManager().reserveGlobalTextureUnits(Shader::ShaderManager::Slot::SkyTexture); + int skyTextureSlot = MWBase::Environment::get() + .getResourceSystem() + ->getSceneManager() + ->getShaderManager() + .reserveGlobalTextureUnits(Shader::ShaderManager::Slot::SkyTexture); stateset->addUniform(new osg::Uniform("far", 10000000.0f)); stateset->addUniform(new osg::Uniform("skyBlendingStart", 8000000.0f)); stateset->addUniform(new osg::Uniform("sky", skyTextureSlot)); stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{ 1, 1 })); - if (Stereo::getMultiview()) - { - // The above set the sky texture unit to 0. Normally this is fine since texture unit 0 is the sampler2d diffuseMap, which will be set. - // However, in multiview the sky texture is a sampler2darray, and so needs to be set separately with a dummy texture - osg::Texture2DArray* textureArray = new osg::Texture2DArray; - textureArray->setTextureSize(1, 1, 2); - textureArray->setName("fakeSkyTexture"); - textureArray->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - textureArray->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - textureArray->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - textureArray->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - textureArray->setWrap(osg::Texture::WRAP_R, osg::Texture::CLAMP_TO_EDGE); - stateset->setTextureAttributeAndModes(skyTextureSlot, textureArray, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED); - } + if (Stereo::getMultiview()) + { + // Multiview needs a texture2DArray dummy texture applied here + osg::Texture2DArray* textureArray = new osg::Texture2DArray; + textureArray->setTextureSize(1, 1, 2); + textureArray->setName("fakeSkyTexture"); + textureArray->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + textureArray->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + textureArray->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + textureArray->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + textureArray->setWrap(osg::Texture::WRAP_R, osg::Texture::CLAMP_TO_EDGE); + stateset->setTextureAttributeAndModes(skyTextureSlot, textureArray, + osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED); + } osg::ref_ptr lightmodel = new osg::LightModel; lightmodel->setAmbientIntensity(osg::Vec4(0.3f, 0.3f, 0.3f, 1.f)); diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp index 929ffadcbf..4cd4fdd84a 100644 --- a/components/sceneutil/mwshadowtechnique.cpp +++ b/components/sceneutil/mwshadowtechnique.cpp @@ -1007,7 +1007,6 @@ void SceneUtil::MWShadowTechnique::copyShadowStateSettings(osgUtil::CullVisitor& { for (const auto& sd : vdd->getShadowDataList()) { - //assignTexGenSettings(&cv, sd->_camera, sd->_textureUnit, sd->_texgen); assignValidRegionSettings(cv, sd->_camera, sd->_sm_i, vdd->_uniforms[cv.getTraversalNumber()%2]); assignShadowStateSettings(cv, sd->_camera, sd->_sm_i, vdd->_uniforms[cv.getTraversalNumber()%2]); } From dec9ce4a5fd9d1e442fc99c5768f809e4fcad6e6 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Sun, 26 Jan 2025 17:41:43 +0100 Subject: [PATCH 028/154] sky dummy texture not actually needed when using sky texture slot. --- apps/openmw/mwrender/characterpreview.cpp | 13 ------------- apps/openmw/mwrender/localmap.cpp | 14 -------------- 2 files changed, 27 deletions(-) diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 3c43227013..3a5bbf45fa 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -264,19 +264,6 @@ namespace MWRender stateset->addUniform(new osg::Uniform("skyBlendingStart", 8000000.0f)); stateset->addUniform(new osg::Uniform("sky", skyTextureSlot)); stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{ 1, 1 })); - if (Stereo::getMultiview()) - { - // Multiview needs a texture2DArray dummy texture applied here - osg::Texture2DArray* textureArray = new osg::Texture2DArray; - textureArray->setTextureSize(1, 1, 2); - textureArray->setName("fakeSkyTexture"); - textureArray->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - textureArray->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - textureArray->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - textureArray->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - textureArray->setWrap(osg::Texture::WRAP_R, osg::Texture::CLAMP_TO_EDGE); - stateset->setTextureAttributeAndModes(skyTextureSlot, textureArray, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED); - } stateset->addUniform(new osg::Uniform("emissiveMult", 1.f)); diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index afa29b3f93..d31d08f04a 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -746,20 +746,6 @@ namespace MWRender stateset->addUniform(new osg::Uniform("skyBlendingStart", 8000000.0f)); stateset->addUniform(new osg::Uniform("sky", skyTextureSlot)); stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{ 1, 1 })); - if (Stereo::getMultiview()) - { - // Multiview needs a texture2DArray dummy texture applied here - osg::Texture2DArray* textureArray = new osg::Texture2DArray; - textureArray->setTextureSize(1, 1, 2); - textureArray->setName("fakeSkyTexture"); - textureArray->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - textureArray->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - textureArray->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - textureArray->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - textureArray->setWrap(osg::Texture::WRAP_R, osg::Texture::CLAMP_TO_EDGE); - stateset->setTextureAttributeAndModes(skyTextureSlot, textureArray, - osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED); - } osg::ref_ptr lightmodel = new osg::LightModel; lightmodel->setAmbientIntensity(osg::Vec4(0.3f, 0.3f, 0.3f, 1.f)); From 152dfacab27b226875a9624f5e36096ea2d72982 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Sun, 26 Jan 2025 19:20:46 +0100 Subject: [PATCH 029/154] multiview_resolve did not need to be version 330 --- files/shaders/compatibility/multiview_resolve.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/shaders/compatibility/multiview_resolve.frag b/files/shaders/compatibility/multiview_resolve.frag index 9aeb54314b..a77a3b35d1 100644 --- a/files/shaders/compatibility/multiview_resolve.frag +++ b/files/shaders/compatibility/multiview_resolve.frag @@ -1,4 +1,4 @@ -#version 330 +#version 120 #extension GL_EXT_texture_array : require varying vec2 uv; From efe72ea2d5555faf5a55ccb559d9ac563a4d0188 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Sun, 26 Jan 2025 19:23:12 +0100 Subject: [PATCH 030/154] Clang format --- apps/openmw/mwrender/characterpreview.cpp | 4 ++-- apps/openmw/mwrender/localmap.cpp | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 3a5bbf45fa..a6e9b848b4 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -3,7 +3,6 @@ #include #include -#include #include #include #include @@ -259,7 +258,8 @@ namespace MWRender // TODO: Clean up this mess of loose uniforms that shaders depend on. // turn off sky blending - int skyTextureSlot = mResourceSystem->getSceneManager()->getShaderManager().reserveGlobalTextureUnits(Shader::ShaderManager::Slot::SkyTexture); + int skyTextureSlot = mResourceSystem->getSceneManager()->getShaderManager().reserveGlobalTextureUnits( + Shader::ShaderManager::Slot::SkyTexture); stateset->addUniform(new osg::Uniform("far", 10000000.0f)); stateset->addUniform(new osg::Uniform("skyBlendingStart", 8000000.0f)); stateset->addUniform(new osg::Uniform("sky", skyTextureSlot)); diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index d31d08f04a..d51088d729 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include From b2c0d20d56319189c5ad8955a73aa7a7c425b93b Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Tue, 28 Jan 2025 20:05:08 +0100 Subject: [PATCH 031/154] explicitly include lib/core/fragment.h.glsl --- files/shaders/compatibility/bs/default.frag | 1 + files/shaders/compatibility/bs/nolighting.frag | 1 + files/shaders/compatibility/objects.frag | 1 + 3 files changed, 3 insertions(+) diff --git a/files/shaders/compatibility/bs/default.frag b/files/shaders/compatibility/bs/default.frag index 3d2ad07d1d..6f38c4df5e 100644 --- a/files/shaders/compatibility/bs/default.frag +++ b/files/shaders/compatibility/bs/default.frag @@ -40,6 +40,7 @@ uniform float specStrength; uniform bool useTreeAnim; uniform float distortionStrength; +#include "lib/core/fragment.h.glsl" #include "lib/light/lighting.glsl" #include "lib/material/alpha.glsl" #include "lib/util/distortion.glsl" diff --git a/files/shaders/compatibility/bs/nolighting.frag b/files/shaders/compatibility/bs/nolighting.frag index da267d7858..a46e99aa82 100644 --- a/files/shaders/compatibility/bs/nolighting.frag +++ b/files/shaders/compatibility/bs/nolighting.frag @@ -26,6 +26,7 @@ uniform float far; uniform float near; uniform float alphaRef; +#include "lib/core/fragment.h.glsl" #include "lib/material/alpha.glsl" #include "compatibility/vertexcolors.glsl" diff --git a/files/shaders/compatibility/objects.frag b/files/shaders/compatibility/objects.frag index 999079a31d..ad6d262f6d 100644 --- a/files/shaders/compatibility/objects.frag +++ b/files/shaders/compatibility/objects.frag @@ -89,6 +89,7 @@ varying vec4 passTangent; #define ADDITIVE_BLENDING #endif +#include "lib/core/fragment.h.glsl" #include "lib/light/lighting.glsl" #include "lib/material/parallax.glsl" #include "lib/material/alpha.glsl" From 517aa81938aaaca0882f8423fa525927aa18a6c3 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Wed, 29 Jan 2025 22:35:19 +0100 Subject: [PATCH 032/154] Change sky blending fix to remove changing the "sky" texture slot when disabling sky blending. --- apps/openmw/mwrender/characterpreview.cpp | 3 --- apps/openmw/mwrender/localmap.cpp | 6 ------ 2 files changed, 9 deletions(-) diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index a6e9b848b4..18604e78ca 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -258,11 +258,8 @@ namespace MWRender // TODO: Clean up this mess of loose uniforms that shaders depend on. // turn off sky blending - int skyTextureSlot = mResourceSystem->getSceneManager()->getShaderManager().reserveGlobalTextureUnits( - Shader::ShaderManager::Slot::SkyTexture); stateset->addUniform(new osg::Uniform("far", 10000000.0f)); stateset->addUniform(new osg::Uniform("skyBlendingStart", 8000000.0f)); - stateset->addUniform(new osg::Uniform("sky", skyTextureSlot)); stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{ 1, 1 })); stateset->addUniform(new osg::Uniform("emissiveMult", 1.f)); diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index d51088d729..6785ce29bf 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -736,14 +736,8 @@ namespace MWRender stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); // turn of sky blending - int skyTextureSlot = MWBase::Environment::get() - .getResourceSystem() - ->getSceneManager() - ->getShaderManager() - .reserveGlobalTextureUnits(Shader::ShaderManager::Slot::SkyTexture); stateset->addUniform(new osg::Uniform("far", 10000000.0f)); stateset->addUniform(new osg::Uniform("skyBlendingStart", 8000000.0f)); - stateset->addUniform(new osg::Uniform("sky", skyTextureSlot)); stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{ 1, 1 })); osg::ref_ptr lightmodel = new osg::LightModel; From 5b1aafb77ac2b2c78ba58b40d23d09d20010aa23 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Wed, 29 Jan 2025 23:01:34 +0100 Subject: [PATCH 033/154] Formatting mistakes --- components/sceneutil/mwshadowtechnique.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp index 4cd4fdd84a..9fa01385d5 100644 --- a/components/sceneutil/mwshadowtechnique.cpp +++ b/components/sceneutil/mwshadowtechnique.cpp @@ -1511,7 +1511,6 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) cv.popStateSet(); - if (!orthographicViewFrustum && settings->getShadowMapProjectionHint()==ShadowSettings::PERSPECTIVE_SHADOW_MAP) { assignValidRegionSettings(cv, camera, sm_i, vddUniforms); @@ -3077,7 +3076,6 @@ void MWShadowTechnique::assignShadowStateSettings(osgUtil::CullVisitor& cv, osg: } shadowSpaceUniform->set(shadowSpaceMatrix); - return; } void SceneUtil::MWShadowTechnique::assignValidRegionSettings(osgUtil::CullVisitor & cv, osg::Camera* camera, unsigned int sm_i, Uniforms & uniforms) From 4428c1db7de19d87c71fee1c2228bcd34e3cdb46 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Thu, 30 Jan 2025 20:53:56 +0100 Subject: [PATCH 034/154] Unused includes --- apps/openmw/mwrender/localmap.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 6785ce29bf..e582bb76ee 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -16,8 +16,6 @@ #include #include #include -#include -#include #include #include #include From 4c95e91a8d9581f6deb679e433ceea8593f8f5a0 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Fri, 31 Jan 2025 00:59:50 +0300 Subject: [PATCH 035/154] Replace awscli with s3cmd for macOS Homebrew doesn't let us downgrade, we have to use an alternative client (for now) --- .gitlab-ci.yml | 11 ++++++++--- CI/before_install.osx.sh | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 63c650945c..c910a0c4ed 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -503,7 +503,6 @@ Ubuntu_GCC_integration_tests_asan: - ccache/ script: - CI/before_install.osx.sh - - brew install awscli@2.22.35 - export CCACHE_BASEDIR="$(pwd)" - export CCACHE_DIR="$(pwd)/ccache" - mkdir -pv "${CCACHE_DIR}" @@ -511,12 +510,18 @@ Ubuntu_GCC_integration_tests_asan: - CI/before_script.osx.sh - cd build; make -j $(sysctl -n hw.logicalcpu) package - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME##*/}.dmg"; done - - aws --version - | if [[ -n "${AWS_ACCESS_KEY_ID}" ]]; then + echo "[default]" > ~/.s3cfg + echo "access_key = ${AWS_ACCESS_KEY_ID}" >> ~/.s3cfg + echo "secret_key = ${AWS_SECRET_ACCESS_KEY}" >> ~/.s3cfg + echo "host_base = rgw.ctrl-c.liu.se" >> ~/.s3cfg + echo "host_bucket = %(bucket)s.rgw.ctrl-c.liu.se" >> ~/.s3cfg + echo "use_https = True" >> ~/.s3cfg + artifactDirectory="${CI_PROJECT_NAMESPACE//[\"<>|$'\t'\/\\?*]/_}/${CI_COMMIT_REF_NAME//[\"<>|$'\t'\/\\?*]/_}/${CI_COMMIT_SHORT_SHA//[\"<>|$'\t'\/\\?*]/_}-${CI_JOB_ID//[\"<>|$'\t'\/\\?*]/_}/" for dmg in *.dmg; do - aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "${dmg}" s3://openmw-artifacts/${artifactDirectory} + s3cmd put "${dmg}" s3://openmw-artifacts/${artifactDirectory} done fi - ccache -s diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index 0120c55202..d73399c102 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -7,7 +7,7 @@ export HOMEBREW_AUTOREMOVE=1 brew tap --repair brew update --quiet -brew install curl xquartz gd fontconfig freetype harfbuzz brotli +brew install curl xquartz gd fontconfig freetype harfbuzz brotli s3cmd command -v ccache >/dev/null 2>&1 || brew install ccache command -v cmake >/dev/null 2>&1 || brew install cmake From e583e64380368a128d70faf6f871cc6d552a9453 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Fri, 31 Jan 2025 00:00:29 +0000 Subject: [PATCH 036/154] Downgrade preinstalled awscli to a version that works --- .github/workflows/windows.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b1c7c41b15..014542ed22 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -203,6 +203,7 @@ jobs: if: ${{ env.AWS_ACCESS_KEY_ID != '' && env.AWS_SECRET_ACCESS_KEY != '' && inputs.package }} working-directory: ${{ github.workspace }}/SymStore run: | + choco upgrade awscli -y --version=2.22.35 --allow-downgrade aws --version aws --endpoint-url https://rgw.ctrl-c.liu.se s3 sync --size-only --exclude * --include *.ex_ --include *.dl_ --include *.pd_ . s3://openmw-sym From a3531fe954bbb3327feeb5ede1e58b33cab660a2 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Fri, 31 Jan 2025 00:55:17 +0000 Subject: [PATCH 037/154] Direct downgrade failed, try uninstalling first --- .github/workflows/windows.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 014542ed22..d2a7d37990 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -203,7 +203,8 @@ jobs: if: ${{ env.AWS_ACCESS_KEY_ID != '' && env.AWS_SECRET_ACCESS_KEY != '' && inputs.package }} working-directory: ${{ github.workspace }}/SymStore run: | - choco upgrade awscli -y --version=2.22.35 --allow-downgrade + choco uninstall awscli -y + choco install awscli -y --version=2.22.35 aws --version aws --endpoint-url https://rgw.ctrl-c.liu.se s3 sync --size-only --exclude * --include *.ex_ --include *.dl_ --include *.pd_ . s3://openmw-sym From fe571c1a4de274c4cb5415b66b6f4f627700f35c Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Fri, 17 Jan 2025 13:45:36 +0300 Subject: [PATCH 038/154] Fix invisible rain when occlusion is enabled and sky blending isn't (#7273) --- apps/openmw/mwrender/sky.cpp | 45 ++++++++++++++++++------------------ apps/openmw/mwrender/sky.hpp | 3 ++- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 1cc8cc0d3e..b601c0dd61 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -262,15 +262,15 @@ namespace MWRender , mPrecipitationAlpha(0.f) , mDirtyParticlesEffect(false) { - osg::ref_ptr skyroot = new CameraRelativeTransform; - skyroot->setName("Sky Root"); + mSkyRootNode = new CameraRelativeTransform; + mSkyRootNode->setName("Sky Root"); // Assign empty program to specify we don't want shaders when we are rendering in FFP pipeline if (!mSceneManager->getForceShaders()) - skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), + mSkyRootNode->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED | osg::StateAttribute::ON); - mSceneManager->setUpNormalsRTForStateSet(skyroot->getOrCreateStateSet(), false); - SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*skyroot->getOrCreateStateSet()); - parentNode->addChild(skyroot); + mSceneManager->setUpNormalsRTForStateSet(mSkyRootNode->getOrCreateStateSet(), false); + SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*mSkyRootNode->getOrCreateStateSet()); + parentNode->addChild(mSkyRootNode); mEarlyRenderBinRoot = new osg::Group; // render before the world is rendered @@ -281,19 +281,18 @@ namespace MWRender if (enableSkyRTT) { mSkyRTT = new SkyRTT(Settings::fog().mSkyRttResolution, mEarlyRenderBinRoot); - skyroot->addChild(mSkyRTT); - mRootNode = new osg::Group; - skyroot->addChild(mRootNode); + mSkyRootNode->addChild(mSkyRTT); } - else - mRootNode = skyroot; - mRootNode->setNodeMask(Mask_Sky); - mRootNode->addChild(mEarlyRenderBinRoot); - mUnderwaterSwitch = new UnderwaterSwitchCallback(skyroot); + mSkyNode = new osg::Group; + mSkyNode->setNodeMask(Mask_Sky); + mSkyNode->addChild(mEarlyRenderBinRoot); + mSkyRootNode->addChild(mSkyNode); + + mUnderwaterSwitch = new UnderwaterSwitchCallback(mSkyRootNode); mPrecipitationOcclusion = Settings::shaders().mWeatherParticleOcclusion; - mPrecipitationOccluder = std::make_unique(skyroot, parentNode, rootNode, camera); + mPrecipitationOccluder = std::make_unique(mSkyRootNode, parentNode, rootNode, camera); } void SkyManager::create() @@ -464,7 +463,7 @@ namespace MWRender mRainParticleSystem->setUserValue("particleOcclusion", true); mSceneManager->recreateShaders(mRainNode); - mRootNode->addChild(mRainNode); + mSkyNode->addChild(mRainNode); if (mPrecipitationOcclusion) mPrecipitationOccluder->enable(); } @@ -474,7 +473,7 @@ namespace MWRender if (!mRainNode) return; - mRootNode->removeChild(mRainNode); + mSkyNode->removeChild(mRainNode); mRainNode = nullptr; mPlacer = nullptr; mCounter = nullptr; @@ -485,10 +484,10 @@ namespace MWRender SkyManager::~SkyManager() { - if (mRootNode) + if (mSkyRootNode) { - mRootNode->getParent(0)->removeChild(mRootNode); - mRootNode = nullptr; + mSkyRootNode->getParent(0)->removeChild(mSkyRootNode); + mSkyRootNode = nullptr; } } @@ -595,7 +594,7 @@ namespace MWRender const osg::Node::NodeMask mask = enabled ? Mask_Sky : 0u; mEarlyRenderBinRoot->setNodeMask(mask); - mRootNode->setNodeMask(mask); + mSkyNode->setNodeMask(mask); if (!enabled && mParticleNode && mParticleEffect) { @@ -691,7 +690,7 @@ namespace MWRender { if (mParticleNode) { - mRootNode->removeChild(mParticleNode); + mSkyNode->removeChild(mParticleNode); mParticleNode = nullptr; } if (mRainEffect.empty()) @@ -706,7 +705,7 @@ namespace MWRender mParticleNode = new osg::PositionAttitudeTransform; mParticleNode->addCullCallback(mUnderwaterSwitch); mParticleNode->setNodeMask(Mask_WeatherParticles); - mRootNode->addChild(mParticleNode); + mSkyNode->addChild(mParticleNode); } mParticleEffect = mSceneManager->getInstance(mCurrentParticleEffect, mParticleNode); diff --git a/apps/openmw/mwrender/sky.hpp b/apps/openmw/mwrender/sky.hpp index 6a32978c4e..6d33165c36 100644 --- a/apps/openmw/mwrender/sky.hpp +++ b/apps/openmw/mwrender/sky.hpp @@ -118,7 +118,8 @@ namespace MWRender osg::Camera* mCamera; - osg::ref_ptr mRootNode; + osg::ref_ptr mSkyRootNode; + osg::ref_ptr mSkyNode; osg::ref_ptr mEarlyRenderBinRoot; osg::ref_ptr mParticleNode; From cfa1ad0b33c4b32ae58872b0dff8b3207c786058 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Tue, 4 Feb 2025 20:22:14 +0300 Subject: [PATCH 039/154] Add a dummy serializer for billboards --- components/sceneutil/serialize.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/components/sceneutil/serialize.cpp b/components/sceneutil/serialize.cpp index 81053aa476..ac19b8f446 100644 --- a/components/sceneutil/serialize.cpp +++ b/components/sceneutil/serialize.cpp @@ -208,6 +208,7 @@ namespace SceneUtil "SceneUtil::TextKeyMapHolder", "Shader::AddedState", "Shader::RemovedAlphaFunc", + "NifOsg::BillboardCallback", "NifOsg::FlipController", "NifOsg::KeyframeController", "NifOsg::Emitter", From eaf9488ba03c4d33ca82d20e38632b8d2a56e3b7 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Tue, 4 Feb 2025 20:29:53 +0300 Subject: [PATCH 040/154] Silence SDL3 window/display events coming from SDL2-compat --- components/sdlutil/sdlinputwrapper.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/components/sdlutil/sdlinputwrapper.cpp b/components/sdlutil/sdlinputwrapper.cpp index 43de84bb70..41a4f13818 100644 --- a/components/sdlutil/sdlinputwrapper.cpp +++ b/components/sdlutil/sdlinputwrapper.cpp @@ -76,6 +76,18 @@ namespace SDLUtil while (SDL_PollEvent(&evt)) { +#if SDL_VERSION_ATLEAST(2, 30, 50) + // SDL2-compat may pass us SDL3 display and window events alongside the SDL2 events for funsies + // Until we are ready to move to SDL3, we'll want to prevent the noise + + // Silence 0x151 to 0x1FF range + if (evt.type > SDL_DISPLAYEVENT && evt.type < SDL_WINDOWEVENT) + continue; + + // Silence 0x202 to 0x2FF range + if (evt.type > SDL_SYSWMEVENT && evt.type < SDL_KEYDOWN) + continue; +#endif switch (evt.type) { case SDL_MOUSEMOTION: From c1960635d254db1c3f2cf6cefe9db8eac31830f0 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Fri, 7 Feb 2025 04:55:06 +0300 Subject: [PATCH 041/154] Optimize NIF boolean vector reading --- components/nif/nifstream.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/components/nif/nifstream.cpp b/components/nif/nifstream.cpp index f960e8d972..ef63bae937 100644 --- a/components/nif/nifstream.cpp +++ b/components/nif/nifstream.cpp @@ -228,13 +228,17 @@ namespace Nif { if (getVersion() < generateVersion(4, 1, 0, 0)) { - for (bool& value : std::span(dest, size)) - value = get() != 0; + std::vector buf(size); + read(buf.data(), size); + for (size_t i = 0; i < size; ++i) + dest[i] = buf[i] != 0; } else { - for (bool& value : std::span(dest, size)) - value = get() != 0; + std::vector buf(size); + read(buf.data(), size); + for (size_t i = 0; i < size; ++i) + dest[i] = buf[i] != 0; } } From 5626d925e39797017041b3d78406a8b4768c280f Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Mon, 10 Feb 2025 12:49:14 +0300 Subject: [PATCH 042/154] Avoid reference to temporary in levelled creatures bindings (#8347) --- apps/openmw/mwlua/types/levelledlist.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwlua/types/levelledlist.cpp b/apps/openmw/mwlua/types/levelledlist.cpp index fd848d9121..91c3a078a4 100644 --- a/apps/openmw/mwlua/types/levelledlist.cpp +++ b/apps/openmw/mwlua/types/levelledlist.cpp @@ -43,7 +43,7 @@ namespace MWLua [](const ESM::CreatureLevList& rec) -> std::string { return rec.mId.serializeText(); }); record["chanceNone"] = sol::readonly_property( [](const ESM::CreatureLevList& rec) -> float { return std::clamp(rec.mChanceNone / 100.f, 0.f, 1.f); }); - record["creatures"] = sol::readonly_property([&](const ESM::CreatureLevList& rec) -> sol::table { + record["creatures"] = sol::readonly_property([state](const ESM::CreatureLevList& rec) -> sol::table { sol::table res(state, sol::create); for (size_t i = 0; i < rec.mList.size(); ++i) res[LuaUtil::toLuaIndex(i)] = rec.mList[i]; From 86d56a0b1ae58a4404baeedd9380230bd384f002 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 10 Feb 2025 20:04:24 +0100 Subject: [PATCH 043/154] Clear queued scripts when clearing the Lua manager --- apps/openmw/mwlua/luamanagerimp.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 780ddaf9a9..aa33abda39 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -343,6 +343,7 @@ namespace MWLua mPlayerStorage.clearTemporaryAndRemoveCallbacks(); mInputActions.clear(); mInputTriggers.clear(); + mQueuedAutoStartedScripts.clear(); for (int i = 0; i < 5; ++i) lua_gc(mLua.unsafeState(), LUA_GCCOLLECT, 0); } From ad8f6e5eb6379d0754ac22eb30eb1b23c1b5354b Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Wed, 12 Feb 2025 22:07:30 +0100 Subject: [PATCH 044/154] Include Ptrs with a count of 0 in cell unloading --- apps/openmw/mwworld/cellstore.hpp | 21 +++++++++------------ apps/openmw/mwworld/scene.cpp | 2 +- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index 097053e2e0..2cc9b106a6 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -213,7 +213,7 @@ namespace MWWorld /// unintended behaviour. \attention This function also lists deleted (count 0) objects! /// \return Iteration completed? template - bool forEach(Visitor&& visitor) + bool forEach(Visitor&& visitor, bool includeDeleted = false) { if (mState != State_Loaded) return false; @@ -227,7 +227,7 @@ namespace MWWorld for (LiveCellRefBase* mergedRef : mMergedRefs) { - if (!isAccessible(mergedRef->mData, mergedRef->mRef)) + if (!includeDeleted && !isAccessible(mergedRef->mData, mergedRef->mRef)) continue; if (!visitor(MWWorld::Ptr(mergedRef, this))) @@ -242,7 +242,7 @@ namespace MWWorld /// unintended behaviour. \attention This function also lists deleted (count 0) objects! /// \return Iteration completed? template - bool forEachConst(Visitor&& visitor) const + bool forEachConst(Visitor&& visitor, bool includeDeleted = false) const { if (mState != State_Loaded) return false; @@ -252,7 +252,7 @@ namespace MWWorld for (const LiveCellRefBase* mergedRef : mMergedRefs) { - if (!isAccessible(mergedRef->mData, mergedRef->mRef)) + if (!includeDeleted && !isAccessible(mergedRef->mData, mergedRef->mRef)) continue; if (!visitor(MWWorld::ConstPtr(mergedRef, this))) @@ -267,7 +267,7 @@ namespace MWWorld /// unintended behaviour. \attention This function also lists deleted (count 0) objects! /// \return Iteration completed? template - bool forEachType(Visitor&& visitor) + bool forEachType(Visitor&& visitor, bool includeDeleted = false) { if (mState != State_Loaded) return false; @@ -279,16 +279,13 @@ namespace MWWorld mHasState = true; - CellRefList& list = get(); - - for (typename CellRefList::List::iterator it(list.mList.begin()); it != list.mList.end(); ++it) + for (LiveCellRefBase& base : get().mList) { - LiveCellRefBase* base = &*it; - if (mMovedToAnotherCell.find(base) != mMovedToAnotherCell.end()) + if (mMovedToAnotherCell.contains(&base)) continue; - if (!isAccessible(base->mData, base->mRef)) + if (!includeDeleted && !isAccessible(base.mData, base.mRef)) continue; - if (!visitor(MWWorld::Ptr(base, this))) + if (!visitor(MWWorld::Ptr(&base, this))) return false; } diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index c30c2b5af0..fb3aee958c 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -367,7 +367,7 @@ namespace MWWorld ListAndResetObjectsVisitor visitor; - cell->forEach(visitor); + cell->forEach(visitor, true); // Include objects being teleported by Lua for (const auto& ptr : visitor.mObjects) { if (const auto object = mPhysics->getObject(ptr)) From 602a429a68c3a070fc9e4b71879790095f57b32a Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 16 Feb 2025 17:46:52 +0300 Subject: [PATCH 045/154] Fix UB when pathgrid geometry is generated and all pathgrid edges are invalid --- components/sceneutil/pathgridutil.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/sceneutil/pathgridutil.cpp b/components/sceneutil/pathgridutil.cpp index b4de250e77..121ef068d8 100644 --- a/components/sceneutil/pathgridutil.cpp +++ b/components/sceneutil/pathgridutil.cpp @@ -126,9 +126,9 @@ namespace SceneUtil gridGeometry->setVertexArray(vertices); gridGeometry->setColorArray(colors, osg::Array::BIND_PER_VERTEX); - if (pointIndexCount) + if (!pointIndices->empty()) gridGeometry->addPrimitiveSet(pointIndices); - if (edgeIndexCount) + if (!lineIndices->empty()) gridGeometry->addPrimitiveSet(lineIndices); gridGeometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); } From d71e4ec9f0813b7087af10126468e58f95c123a2 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Tue, 18 Feb 2025 13:14:20 +0300 Subject: [PATCH 046/154] Editor: Fall back to the closest screen when necessary (#8354) --- apps/opencs/view/doc/view.cpp | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index 7ee8092df6..67297b3433 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -1164,23 +1164,31 @@ void CSVDoc::View::onRequestFocus(const std::string& id) QScreen* CSVDoc::View::getWidgetScreen(const QPoint& position) { QScreen* screen = QApplication::screenAt(position); - if (screen == nullptr) + if (screen) + return screen; + + const QList screens = QApplication::screens(); + if (screens.isEmpty()) + throw std::runtime_error("No screens available"); + + int closestDistance = std::numeric_limits::max(); + for (QScreen* candidate : screens) { - QPoint clampedPosition = position; + const QRect geometry = candidate->geometry(); + const int dx = position.x() - std::clamp(position.x(), geometry.left(), geometry.right()); + const int dy = position.y() - std::clamp(position.y(), geometry.top(), geometry.bottom()); + const int distance = dx * dx + dy * dy; - // If we failed to find the screen, - // clamp negative positions and try again - if (clampedPosition.x() <= 0) - clampedPosition.setX(0); - if (clampedPosition.y() <= 0) - clampedPosition.setY(0); - - screen = QApplication::screenAt(clampedPosition); + if (distance < closestDistance) + { + closestDistance = distance; + screen = candidate; + } } if (screen == nullptr) throw std::runtime_error( - Misc::StringUtils::format("Can not detect the screen for position [%d, %d]", position.x(), position.y())); + Misc::StringUtils::format("Cannot detect the screen for position [%d, %d]", position.x(), position.y())); return screen; } From 04689334c599a5459688721efb38b4ddb0185035 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Tue, 18 Feb 2025 22:28:54 +0300 Subject: [PATCH 047/154] Editor: Use the first/primary screen as last resort --- apps/opencs/view/doc/view.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index 67297b3433..afcab50ead 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -1187,8 +1187,7 @@ QScreen* CSVDoc::View::getWidgetScreen(const QPoint& position) } if (screen == nullptr) - throw std::runtime_error( - Misc::StringUtils::format("Cannot detect the screen for position [%d, %d]", position.x(), position.y())); + screen = screens.first(); return screen; } From cd53cbbea261c16bcd62c840c8edd1c099542570 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Fri, 21 Feb 2025 01:38:08 +0300 Subject: [PATCH 048/154] Don't disable automove when the player can't move (#8358) --- files/data/scripts/omw/input/playercontrols.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/data/scripts/omw/input/playercontrols.lua b/files/data/scripts/omw/input/playercontrols.lua index 32a6f404c1..244b952a1a 100644 --- a/files/data/scripts/omw/input/playercontrols.lua +++ b/files/data/scripts/omw/input/playercontrols.lua @@ -89,7 +89,7 @@ local function processMovement() local sideMovement = input.getRangeActionValue('MoveRight') - input.getRangeActionValue('MoveLeft') local run = input.getBooleanActionValue('Run') ~= settings:get('alwaysRun') - if movement ~= 0 or not Actor.canMove(self) then + if movement ~= 0 then autoMove = false elseif autoMove then movement = 1 From 835ad096574d562d060ab4a9411edbaf38c831c9 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Thu, 20 Feb 2025 02:54:41 +0300 Subject: [PATCH 049/154] Move #6097 from 0.49.0 to 0.48.0 changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 512315ade0..a360fc8fea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,6 @@ Bug #5977: Fatigueless NPCs' corpse underwater changes animation on game load Bug #6025: Subrecords cannot overlap records Bug #6027: Collisionshape becomes spiderweb-like when the mesh is too complex - Bug #6097: Level Progress Tooltip Sometimes Not Updated Bug #6146: Lua command `actor:setEquipment` doesn't trigger mwscripts when equipping or unequipping a scripted item Bug #6156: 1ft Charm or Sound magic effect vfx doesn't work properly Bug #6190: Unintuitive sun specularity time of day dependence @@ -392,6 +391,7 @@ Bug #6066: Addtopic "return" does not work from within script. No errors thrown Bug #6067: ESP loader fails for certain subrecord orders Bug #6087: Bound items added directly to the inventory disappear if their corresponding spell effect ends + Bug #6097: Level Progress Tooltip Sometimes Not Updated Bug #6101: Disarming trapped unlocked owned objects isn't considered a crime Bug #6107: Fatigue is incorrectly recalculated when fortify effect is applied or removed Bug #6109: Crash when playing a custom made menu_background file From dad22cb6727c536b42a65c29bbcbe100e6d54c0f Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sat, 22 Feb 2025 22:37:58 +0000 Subject: [PATCH 050/154] Apply jvoisin's suggestion to install-game-files.rst --- docs/source/manuals/installation/install-game-files.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index fe236168d5..42d4eb8d78 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -150,7 +150,7 @@ If you are running macOS, you can also download Morrowind through Steam: } #. Launch the Steam client and let it download. You can then find ``Morrowind.esm`` at - ``~/Library/Application Support/Steam/steamapps/common/The Elder Scrolls III - Morrowind/Data Files/``. The ~/Library folder is hidden by default. To get to it from the Installation Wizard popup, you will need to go to Users/your YOUR_USERNAME_HERE/ and do ``CMD+SHFT+PERIOD`` to reveal the ~/Library folder. + ``~/Library/Application Support/Steam/steamapps/common/The Elder Scrolls III - Morrowind/Data Files/``. The ~/Library folder is hidden by default. To get to it from the Installation Wizard popup, you will need to go to Users/YOUR_USERNAME_HERE/ and do ``CMD+SHIFT+PERIOD`` to reveal it. Linux ^^^^^ From 3e3dfac4e06969bee99deca1f625650d56bf507f Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 23 Feb 2025 10:48:13 +0100 Subject: [PATCH 051/154] Don't create a scrollbar that cannot be scrolled --- CHANGELOG.md | 1 + apps/openmw/mwgui/dialogue.cpp | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 512315ade0..3cf83459cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -228,6 +228,7 @@ Bug #8252: Plugin dependencies are not required to be loaded Bug #8295: Post-processing chain is case-sensitive Bug #8299: Crash while smoothing landscape + Bug #8364: Crash when clicking scrollbar without handle (divide by zero) Feature #1415: Infinite fall failsafe Feature #2566: Handle NAM9 records for manual cell references Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 6f154bb134..e14c400978 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -666,7 +666,8 @@ namespace MWGui else if (scrollbar) { mHistory->setSize(MyGUI::IntSize(mHistory->getWidth(), book->getSize().second)); - size_t range = book->getSize().second - viewHeight; + // Scroll range should be >= 2 to enable scrolling and prevent a crash + size_t range = std::max(book->getSize().second - viewHeight, size_t(2)); mScrollBar->setScrollRange(range); mScrollBar->setScrollPosition(range - 1); mScrollBar->setTrackSize( From f891a7c3b3e225737f0e735b176399d16b5e6380 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 24 Feb 2025 17:07:32 +0100 Subject: [PATCH 052/154] Turn ActorActiveEffects:remove into a delayed action --- CMakeLists.txt | 2 +- apps/openmw/mwlua/magicbindings.cpp | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 170a56aade..1805ea6fea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 49) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 70) +set(OPENMW_LUA_API_REVISION 71) set(OPENMW_POSTPROCESSING_API_REVISION 2) set(OPENMW_VERSION_COMMITHASH "") diff --git a/apps/openmw/mwlua/magicbindings.cpp b/apps/openmw/mwlua/magicbindings.cpp index 7259d03f4c..19dada74a7 100644 --- a/apps/openmw/mwlua/magicbindings.cpp +++ b/apps/openmw/mwlua/magicbindings.cpp @@ -1061,7 +1061,7 @@ namespace MWLua }; // types.Actor.activeEffects(o):removeEffect(id, ?arg) - activeEffectsT["remove"] = [getEffectKey](const ActorActiveEffects& effects, std::string_view idStr, + activeEffectsT["remove"] = [getEffectKey, context](const ActorActiveEffects& effects, std::string_view idStr, sol::optional argStr) { if (!effects.isActor()) return; @@ -1071,12 +1071,14 @@ namespace MWLua MWMechanics::EffectKey key = getEffectKey(idStr, argStr); - // Note that, although this is member method of ActorActiveEffects and we are removing an effect (not a - // spell), we still need to use the active spells store to purge this effect from active spells. - const auto& ptr = effects.mActor.ptr(); + context.mLuaManager->addAction([key, effects]() { + // Note that, although this is member method of ActorActiveEffects and we are removing an effect (not a + // spell), we still need to use the active spells store to purge this effect from active spells. + const auto& ptr = effects.mActor.ptr(); - auto& activeSpells = ptr.getClass().getCreatureStats(ptr).getActiveSpells(); - activeSpells.purgeEffect(ptr, key.mId, key.mArg); + auto& activeSpells = ptr.getClass().getCreatureStats(ptr).getActiveSpells(); + activeSpells.purgeEffect(ptr, key.mId, key.mArg); + }); }; // types.Actor.activeEffects(o):set(value, id, ?arg) From 3a98b945a881ca6fac57e16493581986b7c61c7d Mon Sep 17 00:00:00 2001 From: uramer Date: Sun, 23 Feb 2025 19:10:21 +0100 Subject: [PATCH 053/154] Keep MENU-registered input actions between games --- .../lua/test_inputactions.cpp | 4 +- apps/openmw/mwlua/inputbindings.cpp | 40 ++++++++++-------- apps/openmw/mwlua/luamanagerimp.cpp | 4 +- components/lua/inputactions.cpp | 42 +++++++++++++++++++ components/lua/inputactions.hpp | 20 ++------- 5 files changed, 72 insertions(+), 38 deletions(-) diff --git a/apps/components_tests/lua/test_inputactions.cpp b/apps/components_tests/lua/test_inputactions.cpp index cad17a5b99..b2a84f193c 100644 --- a/apps/components_tests/lua/test_inputactions.cpp +++ b/apps/components_tests/lua/test_inputactions.cpp @@ -38,10 +38,10 @@ namespace sol::state lua; LuaUtil::InputAction::Registry registry; LuaUtil::InputAction::Info a({ "a", LuaUtil::InputAction::Type::Boolean, "test", "a_name", "a_description", - sol::make_object(lua, false) }); + sol::make_object(lua, false), false }); registry.insert(a); LuaUtil::InputAction::Info b({ "b", LuaUtil::InputAction::Type::Boolean, "test", "b_name", "b_description", - sol::make_object(lua, false) }); + sol::make_object(lua, false), false }); registry.insert(b); LuaUtil::Callback bindA({ lua.load("return function() return true end")(), sol::table(lua, sol::create) }); LuaUtil::Callback bindBToA( diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp index b9affcdfca..27c6714bb4 100644 --- a/apps/openmw/mwlua/inputbindings.cpp +++ b/apps/openmw/mwlua/inputbindings.cpp @@ -139,16 +139,18 @@ namespace MWLua })); api["actions"] = std::ref(context.mLuaManager->inputActions()); - api["registerAction"] = [manager = context.mLuaManager](sol::table options) { - LuaUtil::InputAction::Info parsedOptions; - parsedOptions.mKey = options["key"].get(); - parsedOptions.mType = options["type"].get(); - parsedOptions.mL10n = options["l10n"].get(); - parsedOptions.mName = options["name"].get(); - parsedOptions.mDescription = options["description"].get(); - parsedOptions.mDefaultValue = options["defaultValue"].get(); - manager->inputActions().insert(std::move(parsedOptions)); - }; + api["registerAction"] + = [manager = context.mLuaManager, persistent = context.mType == Context::Menu](sol::table options) { + LuaUtil::InputAction::Info parsedOptions; + parsedOptions.mKey = options["key"].get(); + parsedOptions.mType = options["type"].get(); + parsedOptions.mL10n = options["l10n"].get(); + parsedOptions.mName = options["name"].get(); + parsedOptions.mDescription = options["description"].get(); + parsedOptions.mDefaultValue = options["defaultValue"].get(); + parsedOptions.mPersistent = persistent; + manager->inputActions().insert(std::move(parsedOptions)); + }; api["bindAction"] = [manager = context.mLuaManager]( std::string_view key, const sol::table& callback, sol::table dependencies) { std::vector parsedDependencies; @@ -178,14 +180,16 @@ namespace MWLua }; api["triggers"] = std::ref(context.mLuaManager->inputTriggers()); - api["registerTrigger"] = [manager = context.mLuaManager](sol::table options) { - LuaUtil::InputTrigger::Info parsedOptions; - parsedOptions.mKey = options["key"].get(); - parsedOptions.mL10n = options["l10n"].get(); - parsedOptions.mName = options["name"].get(); - parsedOptions.mDescription = options["description"].get(); - manager->inputTriggers().insert(std::move(parsedOptions)); - }; + api["registerTrigger"] + = [manager = context.mLuaManager, persistent = context.mType == Context::Menu](sol::table options) { + LuaUtil::InputTrigger::Info parsedOptions; + parsedOptions.mKey = options["key"].get(); + parsedOptions.mL10n = options["l10n"].get(); + parsedOptions.mName = options["name"].get(); + parsedOptions.mDescription = options["description"].get(); + parsedOptions.mPersistent = persistent; + manager->inputTriggers().insert(std::move(parsedOptions)); + }; api["registerTriggerHandler"] = [manager = context.mLuaManager](std::string_view key, const sol::table& callback) { manager->inputTriggers().registerHandler(key, LuaUtil::Callback::fromLua(callback)); diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index aa33abda39..5fa2d9867c 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -654,8 +654,8 @@ namespace MWLua MWBase::Environment::get().getL10nManager()->dropCache(); mUiResourceManager.clear(); mLua.dropScriptCache(); - mInputActions.clear(); - mInputTriggers.clear(); + mInputActions.clear(true); + mInputTriggers.clear(true); initConfiguration(); ESM::LuaScripts globalData; diff --git a/components/lua/inputactions.cpp b/components/lua/inputactions.cpp index 7c7551ba60..b0c7fe2e9f 100644 --- a/components/lua/inputactions.cpp +++ b/components/lua/inputactions.cpp @@ -239,6 +239,29 @@ namespace LuaUtil } }); } + + void Registry::clear(bool force) + { + std::vector infoToKeep; + if (!force) + { + for (const Info& info : mInfo) + if (info.mPersistent) + infoToKeep.push_back(info); + } + mKeys.clear(); + mIds.clear(); + mInfo.clear(); + mHandlers.clear(); + mBindings.clear(); + mValues.clear(); + mBindingTree.clear(); + if (!force) + { + for (const Info& i : infoToKeep) + insert(i); + } + } } namespace InputTrigger @@ -292,5 +315,24 @@ namespace LuaUtil }), handlers.end()); } + + void Registry::clear(bool force) + { + std::vector infoToKeep; + if (!force) + { + for (const Info& info : mInfo) + if (info.mPersistent) + infoToKeep.push_back(info); + } + mInfo.clear(); + mHandlers.clear(); + mIds.clear(); + if (!force) + { + for (const Info& i : infoToKeep) + insert(i); + } + } } } diff --git a/components/lua/inputactions.hpp b/components/lua/inputactions.hpp index d05bb71f2c..83de556f9f 100644 --- a/components/lua/inputactions.hpp +++ b/components/lua/inputactions.hpp @@ -29,6 +29,7 @@ namespace LuaUtil::InputAction std::string mName; std::string mDescription; sol::main_object mDefaultValue; + bool mPersistent; }; class MultiTree @@ -73,16 +74,7 @@ namespace LuaUtil::InputAction { mHandlers[safeIdByKey(key)].push_back(handler); } - void clear() - { - mKeys.clear(); - mIds.clear(); - mInfo.clear(); - mHandlers.clear(); - mBindings.clear(); - mValues.clear(); - mBindingTree.clear(); - } + void clear(bool force = false); private: using Id = MultiTree::Node; @@ -110,6 +102,7 @@ namespace LuaUtil::InputTrigger std::string mL10n; std::string mName; std::string mDescription; + bool mPersistent; }; class Registry @@ -130,12 +123,7 @@ namespace LuaUtil::InputTrigger void insert(const Info& info); void registerHandler(std::string_view key, const LuaUtil::Callback& callback); void activate(std::string_view key); - void clear() - { - mInfo.clear(); - mHandlers.clear(); - mIds.clear(); - } + void clear(bool force = false); private: using Id = size_t; From d400c0959ccb03755de272a831e4a7f5e86ac3ae Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 22:45:06 +0100 Subject: [PATCH 054/154] Hide main menu on new and loading game from menu scripts --- apps/openmw/mwstate/statemanagerimp.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 9e292a3eee..f420589b70 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -757,12 +757,14 @@ void MWState::StateManager::update(float duration) if (mNewGameRequest) { + MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_MainMenu); newGame(); mNewGameRequest = false; } if (mLoadRequest) { + MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_MainMenu); loadGame(*mLoadRequest); mLoadRequest = std::nullopt; } From 1bb3198b715be752211a5f46d872c27ee8fd3559 Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 28 Feb 2025 11:29:21 +0100 Subject: [PATCH 055/154] Fix crash on LuaManager::clear triggered by vfs See https://gitlab.com/OpenMW/openmw/-/issues/8370#note_2370896069. ================================================================= ==8699==ERROR: AddressSanitizer: heap-use-after-free on address 0x50800060d4b0 at pc 0x7254de50893e bp 0x7fffa97f9700 sp 0x7fffa97f96f0 READ of size 8 at 0x50800060d4b0 thread T0 #0 0x7254de50893d (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x6293d) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #1 0x7254de50ccad in lua_rawgeti (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x66cad) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #2 0x7254de5d4cab in luaL_unref (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x12ecab) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #3 0x5f96378dd1e9 in sol::stateless_reference::deref(lua_State*) const /home/elsid/dev/openmw/extern/sol3/sol/reference.hpp:440 #4 0x5f96378dd1e9 in sol::basic_reference::deref() const /home/elsid/dev/openmw/extern/sol3/sol/reference.hpp:545 #5 0x5f96378dd1e9 in sol::basic_reference::~basic_reference() /home/elsid/dev/openmw/extern/sol3/sol/reference.hpp:635 #6 0x5f96378dd1e9 in sol::basic_object_base >::~basic_object_base() /home/elsid/dev/openmw/extern/sol3/sol/object_base.hpp:33 #7 0x5f96378dd1e9 in sol::basic_object >::~basic_object() /home/elsid/dev/openmw/extern/sol3/sol/object.hpp:35 #8 0x5f96378dd1e9 in ~ /home/elsid/dev/openmw/apps/openmw/mwlua/vfsbindings.cpp:195 #9 0x5f96378dd1e9 in ~functor_function /home/elsid/dev/openmw/extern/sol3/sol/function_types_stateful.hpp:32 #10 0x5f96378dd1e9 in destroy_at::, false, true> > /usr/include/c++/14.2.1/bits/stl_construct.h:88 #11 0x5f96378dd1e9 in destroy::, false, true> > /usr/include/c++/14.2.1/bits/alloc_traits.h:599 #12 0x5f96378dd1e9 in user_alloc_destroy::, false, true> > /home/elsid/dev/openmw/extern/sol3/sol/stack_core.hpp:460 #13 0x5f963a31e305 in int sol::detail::trampoline(lua_State*, int (*&)(lua_State*)) /home/elsid/dev/openmw/extern/sol3/sol/trampoline.hpp:158 #14 0x5f963a31e89c in sol::detail::c_trampoline(lua_State*, int (*)(lua_State*)) /home/elsid/dev/openmw/extern/sol3/sol/trampoline.hpp:183 #15 0x7254de4dc13a (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x3613a) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #16 0x7254de4deac4 (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x38ac4) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #17 0x7254de4df1a2 (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x391a2) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #18 0x7254de4e1cf2 (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x3bcf2) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #19 0x7254de4e2a37 (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x3ca37) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #20 0x7254de50f4a4 in lua_gc (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x694a4) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #21 0x5f96371f615c in MWLua::LuaManager::clear() /home/elsid/dev/openmw/apps/openmw/mwlua/luamanagerimp.cpp:348 #22 0x5f96371f91ea in MWLua::LuaManager::noGame() /home/elsid/dev/openmw/apps/openmw/mwlua/luamanagerimp.cpp:397 #23 0x5f963a1c7170 in MWState::StateManager::cleanup(bool) /home/elsid/dev/openmw/apps/openmw/mwstate/statemanagerimp.cpp:71 #24 0x5f963a1cabfe in MWState::StateManager::newGame(bool) /home/elsid/dev/openmw/apps/openmw/mwstate/statemanagerimp.cpp:169 #25 0x5f963a1c7aa4 in MWState::StateManager::update(float) /home/elsid/dev/openmw/apps/openmw/mwstate/statemanagerimp.cpp:761 #26 0x5f963a230bab in OMW::Engine::frame(unsigned int, float) /home/elsid/dev/openmw/apps/openmw/engine.cpp:238 #27 0x5f963a2442f3 in OMW::Engine::go() /home/elsid/dev/openmw/apps/openmw/engine.cpp:1032 #28 0x5f963633b3a7 in runApplication(int, char**) /home/elsid/dev/openmw/apps/openmw/main.cpp:228 #29 0x5f963b375b45 in Debug::wrapApplication(int (*)(int, char**), int, char**, std::basic_string_view >) /home/elsid/dev/openmw/components/debug/debugging.cpp:457 #30 0x5f9636331695 in main /home/elsid/dev/openmw/apps/openmw/main.cpp:240 #31 0x7254db435487 (/usr/lib/libc.so.6+0x27487) (BuildId: 0b707b217b15b106c25fe51df3724b25848310c0) #32 0x7254db43554b in __libc_start_main (/usr/lib/libc.so.6+0x2754b) (BuildId: 0b707b217b15b106c25fe51df3724b25848310c0) #33 0x5f9636331464 in _start (/home/elsid/dev/openmw/build/gcc/asan/openmw+0x10db464) (BuildId: ac74a52ca60e8913bef6eb6b3b23d6de648cf3c9) 0x50800060d4b0 is located 16 bytes inside of 96-byte region [0x50800060d4a0,0x50800060d500) freed by thread T0 here: #0 0x7254e2afc102 in free /usr/src/debug/gcc/gcc/libsanitizer/asan/asan_malloc_linux.cpp:52 #1 0x5f963a2f84e7 in LuaUtil::LuaState::trackingAllocator(void*, void*, unsigned long, unsigned long) /home/elsid/dev/openmw/components/lua/luastate.cpp:107 #2 0x7254de4f7779 (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x51779) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #3 0x7254de4de7f3 (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x387f3) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #4 0x7254de4e1a9a (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x3ba9a) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #5 0x7254de4e2a37 (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x3ca37) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #6 0x7254de50f4a4 in lua_gc (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x694a4) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #7 0x5f96371f615c in MWLua::LuaManager::clear() /home/elsid/dev/openmw/apps/openmw/mwlua/luamanagerimp.cpp:348 #8 0x5f96371f91ea in MWLua::LuaManager::noGame() /home/elsid/dev/openmw/apps/openmw/mwlua/luamanagerimp.cpp:397 #9 0x5f963a1c7170 in MWState::StateManager::cleanup(bool) /home/elsid/dev/openmw/apps/openmw/mwstate/statemanagerimp.cpp:71 #10 0x5f963a1cabfe in MWState::StateManager::newGame(bool) /home/elsid/dev/openmw/apps/openmw/mwstate/statemanagerimp.cpp:169 #11 0x5f963a1c7aa4 in MWState::StateManager::update(float) /home/elsid/dev/openmw/apps/openmw/mwstate/statemanagerimp.cpp:761 #12 0x5f963a230bab in OMW::Engine::frame(unsigned int, float) /home/elsid/dev/openmw/apps/openmw/engine.cpp:238 #13 0x5f963a2442f3 in OMW::Engine::go() /home/elsid/dev/openmw/apps/openmw/engine.cpp:1032 #14 0x5f963633b3a7 in runApplication(int, char**) /home/elsid/dev/openmw/apps/openmw/main.cpp:228 #15 0x5f963b375b45 in Debug::wrapApplication(int (*)(int, char**), int, char**, std::basic_string_view >) /home/elsid/dev/openmw/components/debug/debugging.cpp:457 #16 0x5f9636331695 in main /home/elsid/dev/openmw/apps/openmw/main.cpp:240 #17 0x7254db435487 (/usr/lib/libc.so.6+0x27487) (BuildId: 0b707b217b15b106c25fe51df3724b25848310c0) #18 0x7254db43554b in __libc_start_main (/usr/lib/libc.so.6+0x2754b) (BuildId: 0b707b217b15b106c25fe51df3724b25848310c0) #19 0x5f9636331464 in _start (/home/elsid/dev/openmw/build/gcc/asan/openmw+0x10db464) (BuildId: ac74a52ca60e8913bef6eb6b3b23d6de648cf3c9) previously allocated by thread T20 here: #0 0x7254e2afc3c2 in realloc /usr/src/debug/gcc/gcc/libsanitizer/asan/asan_malloc_linux.cpp:85 #1 0x5f963a2f7080 in LuaUtil::LuaState::trackingAllocator(void*, void*, unsigned long, unsigned long) /home/elsid/dev/openmw/components/lua/luastate.cpp:110 #2 0x7254de4e2fc8 (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x3cfc8) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #3 0x7254de4f7476 (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x51476) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #4 0x7254de50c456 in lua_newthread (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x66456) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #5 0x7254de5d53e5 (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x12f3e5) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) #6 0x7254de4dc0c5 (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x360c5) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) Thread T20 created by T0 here: #0 0x7254e2af44cb in pthread_create /usr/src/debug/gcc/gcc/libsanitizer/asan/asan_interceptors.cpp:245 #1 0x7254db6e2071 in __gthread_create /usr/src/debug/gcc/gcc-build/x86_64-pc-linux-gnu/libstdc++-v3/include/x86_64-pc-linux-gnu/bits/gthr-default.h:676 #2 0x7254db6e2071 in std::thread::_M_start_thread(std::unique_ptr >, void (*)()) /usr/src/debug/gcc/gcc/libstdc++-v3/src/c++11/thread.cc:172 #3 0x5f96380fa2eb in thread > /usr/include/c++/14.2.1/bits/std_thread.h:173 #4 0x5f96380fa2eb in MWLua::Worker::Worker(MWLua::LuaManager&) /home/elsid/dev/openmw/apps/openmw/mwlua/worker.cpp:18 #5 0x5f963a23faf4 in std::__detail::_MakeUniq::__single_object std::make_unique(MWLua::LuaManager&) /usr/include/c++/14.2.1/bits/unique_ptr.h:1077 #6 0x5f963a23faf4 in OMW::Engine::prepareEngine() /home/elsid/dev/openmw/apps/openmw/engine.cpp:920 #7 0x5f963a2413ae in OMW::Engine::go() /home/elsid/dev/openmw/apps/openmw/engine.cpp:952 #8 0x5f963633b3a7 in runApplication(int, char**) /home/elsid/dev/openmw/apps/openmw/main.cpp:228 #9 0x5f963b375b45 in Debug::wrapApplication(int (*)(int, char**), int, char**, std::basic_string_view >) /home/elsid/dev/openmw/components/debug/debugging.cpp:457 #10 0x5f9636331695 in main /home/elsid/dev/openmw/apps/openmw/main.cpp:240 #11 0x7254db435487 (/usr/lib/libc.so.6+0x27487) (BuildId: 0b707b217b15b106c25fe51df3724b25848310c0) #12 0x7254db43554b in __libc_start_main (/usr/lib/libc.so.6+0x2754b) (BuildId: 0b707b217b15b106c25fe51df3724b25848310c0) #13 0x5f9636331464 in _start (/home/elsid/dev/openmw/build/gcc/asan/openmw+0x10db464) (BuildId: ac74a52ca60e8913bef6eb6b3b23d6de648cf3c9) SUMMARY: AddressSanitizer: heap-use-after-free (/home/elsid/dev/LuaJIT/build/gcc/asan/install/lib/libluajit-5.1.so.2+0x6293d) (BuildId: 1249151684379d19b11900f406fea9704a6375cb) Shadow bytes around the buggy address: 0x50800060d200: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 fa 0x50800060d280: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 fa 0x50800060d300: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 fa 0x50800060d380: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 fa 0x50800060d400: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 fa =>0x50800060d480: fa fa fa fa fd fd[fd]fd fd fd fd fd fd fd fd fd 0x50800060d500: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x50800060d580: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x50800060d600: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x50800060d680: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x50800060d700: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==8699==ABORTING --- apps/openmw/mwlua/vfsbindings.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/openmw/mwlua/vfsbindings.cpp b/apps/openmw/mwlua/vfsbindings.cpp index cf1fa02815..330bf871e7 100644 --- a/apps/openmw/mwlua/vfsbindings.cpp +++ b/apps/openmw/mwlua/vfsbindings.cpp @@ -68,7 +68,7 @@ namespace MWLua Log(Debug::Verbose) << "Read a large data chunk (" << size << " bytes) from '" << file.mFileName << "'."; } - sol::object readFile(sol::this_state lua, FileHandle& file) + sol::object readFile(lua_State* lua, FileHandle& file) { std::ostringstream os; if (file.mFilePtr && file.mFilePtr->peek() != EOF) @@ -79,7 +79,7 @@ namespace MWLua return sol::make_object(lua, std::move(result)); } - sol::object readLineFromFile(sol::this_state lua, FileHandle& file) + sol::object readLineFromFile(lua_State* lua, FileHandle& file) { std::string result; if (file.mFilePtr && std::getline(*file.mFilePtr, result)) @@ -91,7 +91,7 @@ namespace MWLua return sol::nil; } - sol::object readNumberFromFile(sol::this_state lua, Files::IStreamPtr& file) + sol::object readNumberFromFile(lua_State* lua, Files::IStreamPtr& file) { double number = 0; if (file && *file >> number) @@ -100,7 +100,7 @@ namespace MWLua return sol::nil; } - sol::object readCharactersFromFile(sol::this_state lua, FileHandle& file, size_t count) + sol::object readCharactersFromFile(lua_State* lua, FileHandle& file, size_t count) { if (count <= 0 && file.mFilePtr->peek() != EOF) return sol::make_object(lua, std::string()); @@ -189,7 +189,7 @@ namespace MWLua return seek(lua, self, std::ios_base::cur, off); }); - handle["lines"] = [](sol::this_state lua, sol::object self) { + handle["lines"] = [](sol::this_main_state lua, sol::main_object self) { if (!self.is()) throw std::runtime_error("self should be a file handle"); return sol::as_function([lua, self]() -> sol::object { @@ -199,7 +199,7 @@ namespace MWLua }); }; - api["lines"] = [vfs](sol::this_state lua, std::string_view fileName) { + api["lines"] = [vfs](sol::this_main_state lua, std::string_view fileName) { auto normalizedName = VFS::Path::normalizeFilename(fileName); return sol::as_function( [lua, file = FileHandle(vfs->getNormalized(normalizedName), normalizedName)]() mutable { From dc3264a3a5ed6bced089cedec90573d4a9b387be Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 2 Mar 2025 19:10:03 +0300 Subject: [PATCH 056/154] Restore --export-fonts option functionality --- apps/openmw/engine.cpp | 8 +++++++- apps/openmw/engine.hpp | 3 +++ apps/openmw/main.cpp | 1 + apps/openmw/mwgui/windowmanagerimp.cpp | 5 +++-- apps/openmw/mwgui/windowmanagerimp.hpp | 2 +- components/fontloader/fontloader.cpp | 26 ++++++++++++++++++++++++-- components/fontloader/fontloader.hpp | 4 +++- docs/source/reference/modding/font.rst | 2 ++ 8 files changed, 44 insertions(+), 7 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 2736f339e4..609b92a20a 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -373,6 +373,7 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) , mScriptConsoleMode(false) , mActivationDistanceOverride(-1) , mGrab(true) + , mExportFonts(false) , mRandomSeed(0) , mNewGame(false) , mCfgMgr(configurationManager) @@ -807,7 +808,7 @@ void OMW::Engine::prepareEngine() rootNode->addChild(guiRoot); mWindowManager = std::make_unique(mWindow, mViewer, guiRoot, mResourceSystem.get(), - mWorkQueue.get(), mCfgMgr.getLogPath(), mScriptConsoleMode, mTranslationDataStorage, mEncoding, + mWorkQueue.get(), mCfgMgr.getLogPath(), mScriptConsoleMode, mTranslationDataStorage, mEncoding, mExportFonts, Version::getOpenmwVersionDescription(), shadersSupported, mCfgMgr); mEnvironment.setWindowManager(*mWindowManager); @@ -1109,6 +1110,11 @@ void OMW::Engine::setWarningsMode(int mode) mWarningsMode = mode; } +void OMW::Engine::enableFontExport(bool exportFonts) +{ + mExportFonts = exportFonts; +} + void OMW::Engine::setSaveGameFile(const std::filesystem::path& savegame) { mSaveGameFile = savegame; diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 39056af560..1b4620bcf1 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -171,6 +171,7 @@ namespace OMW // Grab mouse? bool mGrab; + bool mExportFonts; unsigned int mRandomSeed; Compiler::Extensions mExtensions; @@ -251,6 +252,8 @@ namespace OMW void setWarningsMode(int mode); + void enableFontExport(bool exportFonts); + /// Set the save game file to load after initialising the engine. void setSaveGameFile(const std::filesystem::path& savegame); diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index 5b89bca960..cbfc0d7d24 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -155,6 +155,7 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati Fallback::Map::init(variables["fallback"].as().mMap); engine.setSoundUsage(!variables["no-sound"].as()); engine.setActivationDistanceOverride(variables["activate-dist"].as()); + engine.enableFontExport(variables["export-fonts"].as()); engine.setRandomSeed(variables["random-seed"].as()); return true; diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index e51350d19f..6ccc1e02f0 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -146,7 +146,7 @@ namespace MWGui WindowManager::WindowManager(SDL_Window* window, osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const std::filesystem::path& logpath, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, ToUTF8::FromType encoding, - const std::string& versionDescription, bool useShaders, Files::ConfigurationManager& cfgMgr) + bool exportFonts, const std::string& versionDescription, bool useShaders, Files::ConfigurationManager& cfgMgr) : mOldUpdateMask(0) , mOldCullMask(0) , mStore(nullptr) @@ -215,7 +215,8 @@ namespace MWGui MyGUI::LanguageManager::getInstance().eventRequestTag = MyGUI::newDelegate(this, &WindowManager::onRetrieveTag); // Load fonts - mFontLoader = std::make_unique(encoding, resourceSystem->getVFS(), mScalingFactor); + mFontLoader + = std::make_unique(encoding, resourceSystem->getVFS(), mScalingFactor, exportFonts); // Register own widgets with MyGUI MyGUI::FactoryManager::getInstance().registerFactory("Widget"); diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 052a269188..409a31a514 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -128,7 +128,7 @@ namespace MWGui WindowManager(SDL_Window* window, osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const std::filesystem::path& logpath, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, - ToUTF8::FromType encoding, const std::string& versionDescription, bool useShaders, + ToUTF8::FromType encoding, bool exportFonts, const std::string& versionDescription, bool useShaders, Files::ConfigurationManager& cfgMgr); virtual ~WindowManager(); diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp index 877d38116b..5f5dd57c2b 100644 --- a/components/fontloader/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -227,9 +227,10 @@ namespace namespace Gui { - FontLoader::FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, float scalingFactor) + FontLoader::FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, float scalingFactor, bool exportFonts) : mVFS(vfs) , mScalingFactor(scalingFactor) + , mExportFonts(exportFonts) { if (encoding == ToUTF8::WINDOWS_1252) mEncoding = ToUTF8::CP437; @@ -407,7 +408,8 @@ namespace Gui file.reset(); // Create the font texture - std::string bitmapFilename = "fonts/" + std::string(name_) + ".tex"; + const std::string name(name_); + const std::string bitmapFilename = "fonts/" + name + ".tex"; Files::IStreamPtr bitmapFile = mVFS->get(bitmapFilename); @@ -428,6 +430,19 @@ namespace Gui fail(*bitmapFile, bitmapFilename, "File too small to be a valid bitmap"); bitmapFile.reset(); + if (mExportFonts) + { + osg::ref_ptr image = new osg::Image; + image->allocateImage(width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE); + assert(image->isDataContiguous()); + memcpy(image->data(), textureData.data(), textureData.size()); + // Convert to OpenGL origin for sensible output + image->flipVertical(); + + Log(Debug::Info) << "Writing " << name + ".png"; + osgDB::writeImageFile(*image, name + ".png"); + } + MyGUI::ITexture* tex = MyGUI::RenderManager::getInstance().createTexture(bitmapFilename); tex->createManual(width, height, MyGUI::TextureUsage::Write, MyGUI::PixelFormat::R8G8B8A8); unsigned char* texData = reinterpret_cast(tex->lock(MyGUI::TextureUsage::Write)); @@ -647,6 +662,13 @@ namespace Gui cursorCode->addAttribute("size", "0 0"); } + if (mExportFonts) + { + Log(Debug::Info) << "Writing " << name + ".xml"; + xmlDocument.createDeclaration(); + xmlDocument.save(name + ".xml"); + } + // Register the font with MyGUI MyGUI::ResourceManualFont* font = static_cast( MyGUI::FactoryManager::getInstance().createObject("Resource", "ResourceManualFont")); diff --git a/components/fontloader/fontloader.hpp b/components/fontloader/fontloader.hpp index 7e9220d58d..a7269f1a56 100644 --- a/components/fontloader/fontloader.hpp +++ b/components/fontloader/fontloader.hpp @@ -25,7 +25,8 @@ namespace Gui class FontLoader { public: - FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, float scalingFactor); + /// @param exportFonts export the converted fonts (Images and XML with glyph metrics) to files? + FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, float scalingFactor, bool exportFonts); void overrideLineHeight(MyGUI::xml::ElementPtr _node, std::string_view _file, MyGUI::Version _version); @@ -35,6 +36,7 @@ namespace Gui ToUTF8::FromType mEncoding; const VFS::Manager* mVFS; float mScalingFactor; + bool mExportFonts; void loadFonts(); void loadFont(const std::string& fontName, const std::string& fontId); diff --git a/docs/source/reference/modding/font.rst b/docs/source/reference/modding/font.rst index ae2a457e44..2c67a940d1 100644 --- a/docs/source/reference/modding/font.rst +++ b/docs/source/reference/modding/font.rst @@ -15,6 +15,8 @@ Morrowind .fnt fonts Morrowind uses a custom ``.fnt`` file format. It is not compatible with the Windows Font File ``.fnt`` format. To our knowledge, the format is undocumented. OpenMW can load this format and convert it on the fly into something usable (see font loader `source code `_). +You can use --export-fonts command line option to write the converted font +(a PNG image and an XML file describing the position of each glyph in the image) to the current directory. They can be used instead of TrueType fonts if needed by specifying their ``.fnt`` files names in the ``openmw.cfg``. For example: From 7670afcba18cc89bcb3480c159078dadc7655d78 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 3 Mar 2025 00:18:39 +0100 Subject: [PATCH 057/154] Direct player attack lower by target's half height To make sure it always hits the target. --- scripts/data/integration_tests/test_lua_api/player.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/data/integration_tests/test_lua_api/player.lua b/scripts/data/integration_tests/test_lua_api/player.lua index 18229a3cd8..16f0f2eea3 100644 --- a/scripts/data/integration_tests/test_lua_api/player.lua +++ b/scripts/data/integration_tests/test_lua_api/player.lua @@ -315,7 +315,8 @@ testing.registerLocalTest('playerWeaponAttack', self.controls.run = true self.controls.movement = 1 else - destination = targetActor.position + local halfExtents = types.Actor.getPathfindingAgentBounds(targetActor).halfExtents + destination = targetActor.position - util.vector3(0, 0, halfExtents.z) if nextTime < time then if use == 0 then From c50d8195bb04b9792ad05a7b2bf5f8807ffb32ef Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Mon, 3 Mar 2025 07:52:17 +0300 Subject: [PATCH 058/154] Remove custom substitutions for glyphs that may exist in the font --- components/fontloader/fontloader.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp index 877d38116b..21a4be42e4 100644 --- a/components/fontloader/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -465,7 +465,7 @@ namespace Gui // € (Euro Sign, 0x80/U+20AC) is replaced with underscore // 0x81 (unused) is replaced with underscore additional.emplace(44, 0x201A); // ‚ (Single Low-9 Quotation Mark, 0x82) => , (comma) - additional.emplace(102, 0x0192); // ƒ (Latin Small Letter F with Hook, 0x83) => f (latin small F) (custom) + // ƒ (Latin Small Letter F with Hook, 0x83) is unavailable, not replaced additional.emplace(44, 0x201E); // „ (Double Low-9 Quotation Mark, 0x84) => , (comma) additional.emplace(46, 0x2026); // … (Horizontal Ellipsis, 0x85) => . (period) additional.emplace(43, 0x2020); // † (Dagger, 0x86) => + (plus sign) @@ -512,7 +512,7 @@ namespace Gui additional.emplace(95, 0x00AF); // ¯ (Macron) => _ (underscore) // ° (Degree Sign, 0xB0) is unavailable, not replaced // ± (Plus-Minus Sign, 0xB1) is unavailable, not replaced - additional.emplace(50, 0x00B2); // ² (Superscript Two) => 2 (two digit) (custom) + // ² (Superscript Two, 0xB2) is unavailable, not replaced additional.emplace(51, 0x00B3); // ³ (Superscript Three) => 3 (three digit) additional.emplace(39, 0x00B4); // ´ (Acute Accent) => ' (apostrophe) // µ (Micro Sign, 0xB5) is unavailable, not replaced @@ -532,7 +532,7 @@ namespace Gui additional.emplace(65, 0x00C3); // à (Latin Capital Letter A with Tilde) => A (latin capital A) // Ä (Latin Capital Letter A with Diaeresis, 0xC4) is available // Å (Latin Capital Letter A with Ring Above, 0xC5) is available - additional.emplace(65, 0x00C6); // Æ (Latin Capital Letter Ae) => A (latin capital A) (custom) + // Æ (Latin Capital Letter Ae, 0xC6) is unavailable, not replaced // Ç (Latin Capital Letter C with Cedilla, 0xC7) is available additional.emplace(69, 0x00C8); // È (Latin Capital Letter E with Grave) => E (latin capital E) // É (Latin Capital Letter E with Acute, 0xC9) is available @@ -543,7 +543,7 @@ namespace Gui additional.emplace(73, 0x00CE); // Î (Latin Capital Letter I with Circumflex) => I (latin capital I) additional.emplace(73, 0x00CF); // Ï (Latin Capital Letter I with Diaeresis) => I (latin capital I) additional.emplace(68, 0x00D0); // Ð (Latin Capital Letter Eth) => D (latin capital D) - additional.emplace(78, 0x00D1); // Ñ (Latin Capital Letter N with Tilde) => N (latin capital N) (custom) + // Ñ (Latin Capital Letter N with Tilde, 0xD1) is unavailable, not replaced additional.emplace(79, 0x00D2); // Ò (Latin Capital Letter O with Grave) => O (latin capital O) additional.emplace(79, 0x00D3); // Ó (Latin Capital Letter O with Acute) => O (latin capital O) additional.emplace(79, 0x00D4); // Ô (Latin Capital Letter O with Circumflex) => O (latin capital O) From c8fe596fc4641a61b7b0fdc536c681c4f7d32087 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Mon, 3 Mar 2025 07:15:58 +0300 Subject: [PATCH 059/154] Add some remaining missing bitmap substitutions --- components/fontloader/fontloader.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp index 21a4be42e4..09b8d3a034 100644 --- a/components/fontloader/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -500,7 +500,7 @@ namespace Gui // £ (Pound Sign, 0xA3) is available but its glyph looks like œ (small oe ligature) omitted.push_back(0x00A4); // ¤ (Currency Sign) // ¥ (Yen Sign, 0xA5) is unavailable, not replaced - // ¦ (Broken Bar, 0xA6) is unavailable, not replaced + additional.emplace(221, 0x00A6); // ¦ (Broken Bar, 0xA6) => ▌ omitted.push_back(0x00A7); // § (Section Sign) additional.emplace(34, 0x00A8); // ¨ (Diaeresis) => " (double quote mark) additional.emplace(99, 0x00A9); // © (Copyright Sign) => c (latin small C) @@ -556,7 +556,12 @@ namespace Gui additional.emplace(85, 0x00DB); // Û (Latin Capital Letter U with Circumflex) => U (latin capital U) // Ü (Latin Capital Letter U with Diaeresis, 0xDC) is available additional.emplace(89, 0x00DD); // Ý (Latin Capital Letter Y with Acute) => Y (latin capital Y) - // 0xDE to 0xFF are not replaced + // 0xDE to 0xFF are generally not replaced with certain exceptions + additional.emplace(97, 0x00E3); // ã (Latin Small Letter A with Tilde) => a (latin small A) + additional.emplace(100, 0x00F0); // ð (Latin Small Letter Eth) => d (latin small D) + additional.emplace(111, 0x00F5); // õ (Latin Small Letter O with Tilde) => o (latin small O) + additional.emplace(111, 0x00F8); // ø (Latin Small Letter O with Stroke) => o (latin small O) + additional.emplace(121, 0x00FD); // ý (Latin Small Letter Y with Acute) => y (latin small Y) // Russian Morrowind which uses Win-1251 encoding only does equivalent (often garbage) Win-1252 replacements // However, we'll provide custom replacements for Cyrillic io letters From 24468fd9658301e0bf7cc1376f5aede129e00b1a Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Mon, 3 Mar 2025 07:49:01 +0300 Subject: [PATCH 060/154] Allow bitmap font texture to end prematurely --- components/fontloader/fontloader.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp index 877d38116b..27de06105d 100644 --- a/components/fontloader/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -425,7 +425,8 @@ namespace Gui textureData.resize(width * height * 4); bitmapFile->read(textureData.data(), width * height * 4); if (!bitmapFile->good()) - fail(*bitmapFile, bitmapFilename, "File too small to be a valid bitmap"); + Log(Debug::Warning) << "Font bitmap " << bitmapFilename << " ended prematurely, using partial data (" + << bitmapFile->gcount() << "/" << (width * height * 4) << " bytes)"; bitmapFile.reset(); MyGUI::ITexture* tex = MyGUI::RenderManager::getInstance().createTexture(bitmapFilename); From fd358396fc88925f3fdd89b2b2ae8ce734c898d8 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 2 Mar 2025 22:13:30 +0300 Subject: [PATCH 061/154] Properly implement bitmap font kerning --- components/fontloader/fontloader.cpp | 89 ++++++++++------------------ 1 file changed, 31 insertions(+), 58 deletions(-) diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp index 877d38116b..476a3da928 100644 --- a/components/fontloader/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -363,8 +363,8 @@ namespace Gui Point bottom_right; float width; float height; - float u2; // appears unused, always 0 - float kerning; + float kerningLeft; + float kerningRight; float ascent; } GlyphInfo; @@ -564,6 +564,12 @@ namespace Gui additional.emplace(69, 0x0401); // Ё (Cyrillic Capital Letter Io) => E (latin capital E) additional.emplace(137, 0x0451); // ё (Cyrillic Small Letter Io) => ë (latin small E-diaeresis) + // ASCII vertical bar, use this as text input cursor + additional.emplace(124, MyGUI::FontCodeType::Cursor); + + // Underscore, use for NotDefined marker (used for glyphs not existing in the font) + additional.emplace(95, MyGUI::FontCodeType::NotDefined); + for (int i = 0; i < 256; i++) { float x1 = data[i].top_left.x * width; @@ -573,64 +579,31 @@ namespace Gui ToUTF8::Utf8Encoder encoder(mEncoding); unsigned long unicodeVal = getUnicode(i, encoder, mEncoding); + const std::string coord = MyGUI::utility::toString(x1) + " " + MyGUI::utility::toString(y1) + " " + + MyGUI::utility::toString(w) + " " + MyGUI::utility::toString(h); + float advance = data[i].width + data[i].kerningRight; + // Yes MyGUI, we really do want an advance of 0 sometimes, thank you. + if (advance == 0.f && data[i].width != 0.f) + advance = std::numeric_limits::min(); + const std::string bearing = MyGUI::utility::toString(data[i].kerningLeft) + ' ' + + MyGUI::utility::toString((fontSize - data[i].ascent)); + const MyGUI::IntSize size(static_cast(data[i].width), static_cast(data[i].height)); MyGUI::xml::ElementPtr code = codes->createChild("Code"); code->addAttribute("index", unicodeVal); - code->addAttribute("coord", - MyGUI::utility::toString(x1) + " " + MyGUI::utility::toString(y1) + " " + MyGUI::utility::toString(w) - + " " + MyGUI::utility::toString(h)); - code->addAttribute("advance", data[i].width); - code->addAttribute("bearing", - MyGUI::utility::toString(data[i].kerning) + " " - + MyGUI::utility::toString((fontSize - data[i].ascent))); - code->addAttribute( - "size", MyGUI::IntSize(static_cast(data[i].width), static_cast(data[i].height))); + code->addAttribute("coord", coord); + code->addAttribute("advance", advance); + code->addAttribute("bearing", bearing); + code->addAttribute("size", size); for (auto [it, end] = additional.equal_range(i); it != end; ++it) { code = codes->createChild("Code"); code->addAttribute("index", it->second); - code->addAttribute("coord", - MyGUI::utility::toString(x1) + " " + MyGUI::utility::toString(y1) + " " - + MyGUI::utility::toString(w) + " " + MyGUI::utility::toString(h)); - code->addAttribute("advance", data[i].width); - code->addAttribute("bearing", - MyGUI::utility::toString(data[i].kerning) + " " - + MyGUI::utility::toString((fontSize - data[i].ascent))); - code->addAttribute( - "size", MyGUI::IntSize(static_cast(data[i].width), static_cast(data[i].height))); - } - - // ASCII vertical bar, use this as text input cursor - if (i == 124) - { - MyGUI::xml::ElementPtr cursorCode = codes->createChild("Code"); - cursorCode->addAttribute("index", MyGUI::FontCodeType::Cursor); - cursorCode->addAttribute("coord", - MyGUI::utility::toString(x1) + " " + MyGUI::utility::toString(y1) + " " - + MyGUI::utility::toString(w) + " " + MyGUI::utility::toString(h)); - cursorCode->addAttribute("advance", data[i].width); - cursorCode->addAttribute("bearing", - MyGUI::utility::toString(data[i].kerning) + " " - + MyGUI::utility::toString((fontSize - data[i].ascent))); - cursorCode->addAttribute( - "size", MyGUI::IntSize(static_cast(data[i].width), static_cast(data[i].height))); - } - - // Underscore, use for NotDefined marker (used for glyphs not existing in the font) - if (i == 95) - { - MyGUI::xml::ElementPtr cursorCode = codes->createChild("Code"); - cursorCode->addAttribute("index", MyGUI::FontCodeType::NotDefined); - cursorCode->addAttribute("coord", - MyGUI::utility::toString(x1) + " " + MyGUI::utility::toString(y1) + " " - + MyGUI::utility::toString(w) + " " + MyGUI::utility::toString(h)); - cursorCode->addAttribute("advance", data[i].width); - cursorCode->addAttribute("bearing", - MyGUI::utility::toString(data[i].kerning) + " " - + MyGUI::utility::toString((fontSize - data[i].ascent))); - cursorCode->addAttribute( - "size", MyGUI::IntSize(static_cast(data[i].width), static_cast(data[i].height))); + code->addAttribute("coord", coord); + code->addAttribute("advance", advance); + code->addAttribute("bearing", bearing); + code->addAttribute("size", size); } } @@ -639,12 +612,12 @@ namespace Gui omitted.push_back(MyGUI::FontCodeType::SelectedBack); for (const UnicodeIndex index : omitted) { - MyGUI::xml::ElementPtr cursorCode = codes->createChild("Code"); - cursorCode->addAttribute("index", index); - cursorCode->addAttribute("coord", "0 0 0 0"); - cursorCode->addAttribute("advance", "0"); - cursorCode->addAttribute("bearing", "0 0"); - cursorCode->addAttribute("size", "0 0"); + MyGUI::xml::ElementPtr code = codes->createChild("Code"); + code->addAttribute("index", index); + code->addAttribute("coord", "0 0 0 0"); + code->addAttribute("advance", "0"); + code->addAttribute("bearing", "0 0"); + code->addAttribute("size", "0 0"); } // Register the font with MyGUI From f0cee09b7c303a3dc40865e7abb5ec39519a9b65 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 3 Mar 2025 19:36:54 +0100 Subject: [PATCH 062/154] Extend lifetime of strings placed on the action queue --- apps/openmw/mwlua/uibindings.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index 9652df1238..069258fd8e 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -168,7 +168,8 @@ namespace MWLua if (index == LuaUi::Layer::count()) throw std::logic_error(std::string("Layer not found")); index++; - context.mLuaManager->addAction([=]() { LuaUi::Layer::insert(index, name, options); }, "Insert UI layer"); + context.mLuaManager->addAction( + [=, name = std::string(name)]() { LuaUi::Layer::insert(index, name, options); }, "Insert UI layer"); }; layersTable["insertBefore"] = [context]( std::string_view beforename, std::string_view name, const sol::object& opt) { @@ -177,7 +178,8 @@ namespace MWLua size_t index = LuaUi::Layer::indexOf(beforename); if (index == LuaUi::Layer::count()) throw std::logic_error(std::string("Layer not found")); - context.mLuaManager->addAction([=]() { LuaUi::Layer::insert(index, name, options); }, "Insert UI layer"); + context.mLuaManager->addAction( + [=, name = std::string(name)]() { LuaUi::Layer::insert(index, name, options); }, "Insert UI layer"); }; sol::table layers = LuaUtil::makeReadOnly(layersTable); sol::table layersMeta = layers[sol::metatable_key]; @@ -285,7 +287,7 @@ namespace MWLua return res; }; api["_setWindowDisabled"] - = [windowManager, luaManager = context.mLuaManager](std::string_view window, bool disabled) { + = [windowManager, luaManager = context.mLuaManager](std::string window, bool disabled) { luaManager->addAction([=]() { windowManager->setDisabledByLua(window, disabled); }); }; From b0e9df013994c4b422b51713f245e91f3c2d424f Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 3 Mar 2025 19:37:07 +0100 Subject: [PATCH 063/154] Ensure class members are tied to the main Lua state --- apps/openmw/mwlua/itemdata.cpp | 3 ++- apps/openmw/mwlua/localscripts.hpp | 2 +- apps/openmw/mwlua/stats.cpp | 20 ++++++++++++-------- apps/openmw/mwlua/uibindings.cpp | 2 +- components/lua/luastate.cpp | 3 ++- components/lua/luastate.hpp | 4 ++-- components/lua/scriptscontainer.cpp | 4 ++-- components/lua/scriptscontainer.hpp | 16 ++++++++-------- components/lua/storage.cpp | 2 +- components/lua/storage.hpp | 2 +- components/lua_ui/content.hpp | 4 ++-- components/lua_ui/element.cpp | 2 +- components/lua_ui/element.hpp | 2 +- components/lua_ui/widget.cpp | 2 +- components/lua_ui/widget.hpp | 16 ++++++++-------- 15 files changed, 45 insertions(+), 39 deletions(-) diff --git a/apps/openmw/mwlua/itemdata.cpp b/apps/openmw/mwlua/itemdata.cpp index 38415ea25e..1b809383db 100644 --- a/apps/openmw/mwlua/itemdata.cpp +++ b/apps/openmw/mwlua/itemdata.cpp @@ -71,7 +71,8 @@ namespace MWLua { SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); - obj->mStatsCache[SelfObject::CachedStat{ &ItemData::setValue, std::monostate{}, prop }] = value; + obj->mStatsCache[SelfObject::CachedStat{ &ItemData::setValue, std::monostate{}, prop }] + = sol::main_object(value); } else throw std::runtime_error("Only global or self scripts can set the value"); diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index b32d8bba9e..adbf20292d 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -54,7 +54,7 @@ namespace MWLua { } MWBase::LuaManager::ActorControls mControls; - std::map mStatsCache; + std::map mStatsCache; bool mIsActive; }; diff --git a/apps/openmw/mwlua/stats.cpp b/apps/openmw/mwlua/stats.cpp index 317ffbd406..637ae0b002 100644 --- a/apps/openmw/mwlua/stats.cpp +++ b/apps/openmw/mwlua/stats.cpp @@ -123,7 +123,8 @@ namespace MWLua SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); - obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, attributeId, "skillIncreasesForAttribute" }] = value; + obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, attributeId, "skillIncreasesForAttribute" }] + = sol::main_object(value); } }; @@ -159,7 +160,7 @@ namespace MWLua SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, specialization, "skillIncreasesForSpecialization" }] - = value; + = sol::main_object(value); } }; @@ -183,7 +184,8 @@ namespace MWLua { SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); - obj->mStatsCache[SelfObject::CachedStat{ &setCreatureValue, std::monostate{}, "current" }] = value; + obj->mStatsCache[SelfObject::CachedStat{ &setCreatureValue, std::monostate{}, "current" }] + = sol::main_object(value); } sol::object getProgress(const Context& context) const @@ -204,7 +206,8 @@ namespace MWLua SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); - obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, std::monostate{}, "progress" }] = value; + obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, std::monostate{}, "progress" }] + = sol::main_object(value); } SkillIncreasesForAttributeStats getSkillIncreasesForAttributeStats() const @@ -258,7 +261,7 @@ namespace MWLua { SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); - obj->mStatsCache[SelfObject::CachedStat{ &DynamicStat::setValue, mIndex, prop }] = value; + obj->mStatsCache[SelfObject::CachedStat{ &DynamicStat::setValue, mIndex, prop }] = sol::main_object(value); } static void setValue(Index i, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) @@ -318,7 +321,7 @@ namespace MWLua { SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); - obj->mStatsCache[SelfObject::CachedStat{ &AttributeStat::setValue, mId, prop }] = value; + obj->mStatsCache[SelfObject::CachedStat{ &AttributeStat::setValue, mId, prop }] = sol::main_object(value); } static void setValue(Index i, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) @@ -402,7 +405,7 @@ namespace MWLua { SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); - obj->mStatsCache[SelfObject::CachedStat{ &SkillStat::setValue, mId, prop }] = value; + obj->mStatsCache[SelfObject::CachedStat{ &SkillStat::setValue, mId, prop }] = sol::main_object(value); } static void setValue(Index index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) @@ -465,7 +468,8 @@ namespace MWLua { SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); - obj->mStatsCache[SelfObject::CachedStat{ &AIStat::setValue, static_cast(mIndex), prop }] = value; + obj->mStatsCache[SelfObject::CachedStat{ &AIStat::setValue, static_cast(mIndex), prop }] + = sol::main_object(value); } static void setValue(Index i, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index 069258fd8e..76bba0c082 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -310,7 +310,7 @@ namespace MWLua return res.str(); }; element["layout"] = sol::property([](const LuaUi::Element& element) { return element.mLayout; }, - [](LuaUi::Element& element, const sol::table& layout) { element.mLayout = layout; }); + [](LuaUi::Element& element, const sol::main_table& layout) { element.mLayout = layout; }); element["update"] = [luaManager = context.mLuaManager](const std::shared_ptr& element) { if (element->mState != LuaUi::Element::Created) return; diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp index 2f5800c521..de876f2c5c 100644 --- a/components/lua/luastate.cpp +++ b/components/lua/luastate.cpp @@ -343,7 +343,8 @@ namespace LuaUtil } sol::protected_function_result LuaState::runInNewSandbox(const VFS::Path::Normalized& path, - const std::string& envName, const std::map& packages, const sol::object& hiddenData) + const std::string& envName, const std::map& packages, + const sol::main_object& hiddenData) { // TODO sol::protected_function script = loadScriptAndCache(path); diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp index 32c3151c88..cf8e62690a 100644 --- a/components/lua/luastate.hpp +++ b/components/lua/luastate.hpp @@ -148,8 +148,8 @@ namespace LuaUtil // should be either a sol::table or a sol::function. If it is a function, it will be evaluated // (once per sandbox) with the argument 'hiddenData' the first time when requested. sol::protected_function_result runInNewSandbox(const VFS::Path::Normalized& path, - const std::string& envName = "unnamed", const std::map& packages = {}, - const sol::object& hiddenData = sol::nil); + const std::string& envName = "unnamed", const std::map& packages = {}, + const sol::main_object& hiddenData = sol::nil); void dropScriptCache() { mCompiledScripts.clear(); } diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index 88f4b9c33c..5eff211894 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -52,7 +52,7 @@ namespace LuaUtil Log(Debug::Error) << mNamePrefix << "[" << scriptPath(scriptId) << "] " << msg << ": " << e.what(); } - void ScriptsContainer::addPackage(std::string packageName, sol::object package) + void ScriptsContainer::addPackage(std::string packageName, sol::main_object package) { if (!package.is()) throw std::logic_error("Expected package to be read-only: " + packageName); @@ -312,7 +312,7 @@ namespace LuaUtil { if (next->mOnOverride) { - sol::object prevInterface = sol::nil; + sol::main_object prevInterface = sol::nil; if (prev) prevInterface = *prev->mInterface; try diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 2059983c1f..956b4d4317 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -90,7 +90,7 @@ namespace LuaUtil // Adds package that will be available (via `require`) for all scripts in the container. // Automatically applies LuaUtil::makeReadOnly to the package. - void addPackage(std::string packageName, sol::object package); + void addPackage(std::string packageName, sol::main_object package); // Gets script with given id from ScriptsConfiguration, finds the source in the virtual file system, starts as a // new script, adds it to the container, and calls onInit for this script. Returns `true` if the script was @@ -168,7 +168,7 @@ namespace LuaUtil struct Handler { int mScriptId; - sol::function mFn; + sol::main_function mFn; }; struct EngineHandlerList @@ -212,11 +212,11 @@ namespace LuaUtil private: struct Script { - std::optional mOnSave; - std::optional mOnOverride; - std::optional mInterface; + std::optional mOnSave; + std::optional mOnOverride; + std::optional mInterface; std::string mInterfaceName; - sol::table mHiddenData; + sol::main_table mHiddenData; std::map mRegisteredCallbacks; std::map mTemporaryCallbacks; VFS::Path::Normalized mPath; @@ -268,11 +268,11 @@ namespace LuaUtil const UserdataSerializer* mSerializer = nullptr; const UserdataSerializer* mSavedDataDeserializer = nullptr; - std::map mAPI; + std::map mAPI; struct LoadedData { std::map mScripts; - sol::table mPublicInterfaces; + sol::main_table mPublicInterfaces; std::map> mEventHandlers; diff --git a/components/lua/storage.cpp b/components/lua/storage.cpp index 9f3a159b3a..9d3d233fb7 100644 --- a/components/lua/storage.cpp +++ b/components/lua/storage.cpp @@ -37,7 +37,7 @@ namespace LuaUtil sol::object LuaStorage::Value::getReadOnly(lua_State* L) const { if (mReadOnlyValue == sol::nil && !mSerializedValue.empty()) - mReadOnlyValue = deserialize(L, mSerializedValue, nullptr, true); + mReadOnlyValue = sol::main_object(deserialize(L, mSerializedValue, nullptr, true)); return mReadOnlyValue; } diff --git a/components/lua/storage.hpp b/components/lua/storage.hpp index 5ca151c30d..f86feddb2d 100644 --- a/components/lua/storage.hpp +++ b/components/lua/storage.hpp @@ -74,7 +74,7 @@ namespace LuaUtil private: std::string mSerializedValue; - mutable sol::object mReadOnlyValue = sol::nil; + mutable sol::main_object mReadOnlyValue = sol::nil; }; struct Section diff --git a/components/lua_ui/content.hpp b/components/lua_ui/content.hpp index 1a0379b817..aff38fca57 100644 --- a/components/lua_ui/content.hpp +++ b/components/lua_ui/content.hpp @@ -18,7 +18,7 @@ namespace LuaUi { public: // accepts only Lua tables returned by ui.content - explicit ContentView(sol::table table) + explicit ContentView(sol::main_table table) : mTable(std::move(table)) { if (!isValidContent(mTable)) @@ -94,7 +94,7 @@ namespace LuaUi sol::table getMetatable() const { return mTable[sol::metatable_key].get(); } private: - sol::table mTable; + sol::main_table mTable; template sol::object callMethod(std::string_view name, Arg&&... arg) const diff --git a/components/lua_ui/element.cpp b/components/lua_ui/element.cpp index 2d8462e273..ce6d57e154 100644 --- a/components/lua_ui/element.cpp +++ b/components/lua_ui/element.cpp @@ -324,7 +324,7 @@ namespace LuaUi destroyRoot(mRoot); mRoot = nullptr; } - mLayout = sol::make_object(mLayout.lua_state(), sol::nil); + mLayout.reset(); } mState = Destroyed; } diff --git a/components/lua_ui/element.hpp b/components/lua_ui/element.hpp index 39a1fdd769..ffd15ac950 100644 --- a/components/lua_ui/element.hpp +++ b/components/lua_ui/element.hpp @@ -19,7 +19,7 @@ namespace LuaUi } WidgetExtension* mRoot; - sol::object mLayout; + sol::main_object mLayout; std::string mLayer; enum State diff --git a/components/lua_ui/widget.cpp b/components/lua_ui/widget.cpp index 71416be8c8..acbdbcd03a 100644 --- a/components/lua_ui/widget.cpp +++ b/components/lua_ui/widget.cpp @@ -281,7 +281,7 @@ namespace LuaUi updateChildrenCoord(); } - void WidgetExtension::setProperties(const sol::object& props) + void WidgetExtension::setProperties(const sol::main_object& props) { mProperties = props; updateProperties(); diff --git a/components/lua_ui/widget.hpp b/components/lua_ui/widget.hpp index 0ec688d3bb..5fcf86d110 100644 --- a/components/lua_ui/widget.hpp +++ b/components/lua_ui/widget.hpp @@ -50,10 +50,10 @@ namespace LuaUi void setCallback(const std::string&, const LuaUtil::Callback&); void clearCallbacks(); - void setProperties(const sol::object& props); - void setTemplateProperties(const sol::object& props) { mTemplateProperties = props; } + void setProperties(const sol::main_object& props); + void setTemplateProperties(const sol::main_object& props) { mTemplateProperties = props; } - void setExternal(const sol::object& external) { mExternal = external; } + void setExternal(const sol::main_object& external) { mExternal = external; } MyGUI::IntCoord forcedCoord(); void forceCoord(const MyGUI::IntCoord& offset); @@ -63,7 +63,7 @@ namespace LuaUi virtual void updateCoord(); - const sol::table& getLayout() { return mLayout; } + const sol::main_table& getLayout() { return mLayout; } void setLayout(const sol::table& layout) { mLayout = layout; } template @@ -150,10 +150,10 @@ namespace LuaUi std::vector mTemplateChildren; WidgetExtension* mSlot; std::map> mCallbacks; - sol::table mLayout; - sol::object mProperties; - sol::object mTemplateProperties; - sol::object mExternal; + sol::main_table mLayout; + sol::main_object mProperties; + sol::main_object mTemplateProperties; + sol::main_object mExternal; WidgetExtension* mParent; bool mTemplateChild; bool mElementRoot; From 990096ff9b551382c6e8c88adf06424565dcdedb Mon Sep 17 00:00:00 2001 From: uramer Date: Tue, 4 Mar 2025 21:03:15 +0100 Subject: [PATCH 064/154] Fix in-game actions not clearing because of input bindings only initializing in menu context --- apps/openmw/mwlua/corebindings.cpp | 4 +- apps/openmw/mwlua/inputbindings.cpp | 180 ++++++++++++++++------------ 2 files changed, 103 insertions(+), 81 deletions(-) diff --git a/apps/openmw/mwlua/corebindings.cpp b/apps/openmw/mwlua/corebindings.cpp index 445bcdd617..9df435c00d 100644 --- a/apps/openmw/mwlua/corebindings.cpp +++ b/apps/openmw/mwlua/corebindings.cpp @@ -148,7 +148,7 @@ namespace MWLua }; } - sol::table readOnly = LuaUtil::makeReadOnly(api); - return context.setTypePackage(readOnly, "openmw_core"); + sol::table readOnlyApi = LuaUtil::makeReadOnly(api); + return context.setTypePackage(readOnlyApi, "openmw_core"); } } diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp index 27c6714bb4..c3b47c5061 100644 --- a/apps/openmw/mwlua/inputbindings.cpp +++ b/apps/openmw/mwlua/inputbindings.cpp @@ -38,94 +38,116 @@ namespace MWLua sol::table initInputPackage(const Context& context) { + sol::object cached = context.getTypePackage("openmw_input"); + if (cached != sol::nil) + return cached; sol::state_view lua = context.sol(); - { - if (lua["openmw_input"] != sol::nil) - return lua["openmw_input"]; - } - sol::usertype keyEvent = lua.new_usertype("KeyEvent"); - keyEvent["symbol"] = sol::readonly_property([](const SDL_Keysym& e) { - if (e.sym > 0 && e.sym <= 255) - return std::string(1, static_cast(e.sym)); - else - return std::string(); + context.cachePackage("openmw_input_keyevent", [&lua]() { + sol::usertype keyEvent = lua.new_usertype("KeyEvent"); + keyEvent["symbol"] = sol::readonly_property([](const SDL_Keysym& e) { + if (e.sym > 0 && e.sym <= 255) + return std::string(1, static_cast(e.sym)); + else + return std::string(); + }); + keyEvent["code"] = sol::readonly_property([](const SDL_Keysym& e) -> int { return e.scancode; }); + keyEvent["withShift"] + = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_SHIFT; }); + keyEvent["withCtrl"] + = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_CTRL; }); + keyEvent["withAlt"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_ALT; }); + keyEvent["withSuper"] + = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_GUI; }); + + return sol::table(lua, sol::create); }); - keyEvent["code"] = sol::readonly_property([](const SDL_Keysym& e) -> int { return e.scancode; }); - keyEvent["withShift"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_SHIFT; }); - keyEvent["withCtrl"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_CTRL; }); - keyEvent["withAlt"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_ALT; }); - keyEvent["withSuper"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_GUI; }); - auto touchpadEvent = lua.new_usertype("TouchpadEvent"); - touchpadEvent["device"] = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> int { return e.mDevice; }); - touchpadEvent["finger"] = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> int { return e.mFinger; }); - touchpadEvent["position"] = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> osg::Vec2f { - return { e.mX, e.mY }; + context.cachePackage("openmw_input_touchpadevent", [&lua]() { + auto touchpadEvent = lua.new_usertype("TouchpadEvent"); + touchpadEvent["device"] + = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> int { return e.mDevice; }); + touchpadEvent["finger"] + = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> int { return e.mFinger; }); + touchpadEvent["position"] = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> osg::Vec2f { + return { e.mX, e.mY }; + }); + touchpadEvent["pressure"] + = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> float { return e.mPressure; }); + return sol::table(lua, sol::create); }); - touchpadEvent["pressure"] - = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> float { return e.mPressure; }); - auto inputActions = lua.new_usertype("InputActions"); - inputActions[sol::meta_function::index] - = [](LuaUtil::InputAction::Registry& registry, std::string_view key) { return registry[key]; }; - { - auto pairs = [](LuaUtil::InputAction::Registry& registry) { - auto next - = [](LuaUtil::InputAction::Registry& registry, - std::string_view key) -> sol::optional> { - std::optional nextKey(registry.nextKey(key)); - if (!nextKey.has_value()) - return sol::nullopt; - else - return std::make_tuple(*nextKey, registry[*nextKey].value()); + context.cachePackage("openmw_input_inputactions", [&lua]() { + auto inputActions = lua.new_usertype("InputActions"); + inputActions[sol::meta_function::index] + = [](LuaUtil::InputAction::Registry& registry, std::string_view key) { return registry[key]; }; + { + auto pairs = [](LuaUtil::InputAction::Registry& registry) { + auto next = [](LuaUtil::InputAction::Registry& registry, std::string_view key) + -> sol::optional> { + std::optional nextKey(registry.nextKey(key)); + if (!nextKey.has_value()) + return sol::nullopt; + else + return std::make_tuple(*nextKey, registry[*nextKey].value()); + }; + return std::make_tuple(next, registry, registry.firstKey()); }; - return std::make_tuple(next, registry, registry.firstKey()); - }; - inputActions[sol::meta_function::pairs] = pairs; - } + inputActions[sol::meta_function::pairs] = pairs; + } + return sol::table(lua, sol::create); + }); - auto actionInfo = lua.new_usertype("ActionInfo"); - actionInfo["key"] = sol::readonly_property( - [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mKey; }); - actionInfo["name"] = sol::readonly_property( - [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mName; }); - actionInfo["description"] = sol::readonly_property( - [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mDescription; }); - actionInfo["l10n"] = sol::readonly_property( - [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mL10n; }); - actionInfo["type"] = sol::readonly_property([](const LuaUtil::InputAction::Info& info) { return info.mType; }); - actionInfo["defaultValue"] - = sol::readonly_property([](const LuaUtil::InputAction::Info& info) { return info.mDefaultValue; }); + context.cachePackage("openmw_input_actioninfo", [&lua]() { + auto actionInfo = lua.new_usertype("ActionInfo"); + actionInfo["key"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mKey; }); + actionInfo["name"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mName; }); + actionInfo["description"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mDescription; }); + actionInfo["l10n"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mL10n; }); + actionInfo["type"] + = sol::readonly_property([](const LuaUtil::InputAction::Info& info) { return info.mType; }); + actionInfo["defaultValue"] + = sol::readonly_property([](const LuaUtil::InputAction::Info& info) { return info.mDefaultValue; }); + return sol::table(lua, sol::create); + }); - auto inputTriggers = lua.new_usertype("InputTriggers"); - inputTriggers[sol::meta_function::index] - = [](LuaUtil::InputTrigger::Registry& registry, std::string_view key) { return registry[key]; }; - { - auto pairs = [](LuaUtil::InputTrigger::Registry& registry) { - auto next - = [](LuaUtil::InputTrigger::Registry& registry, - std::string_view key) -> sol::optional> { - std::optional nextKey(registry.nextKey(key)); - if (!nextKey.has_value()) - return sol::nullopt; - else - return std::make_tuple(*nextKey, registry[*nextKey].value()); + context.cachePackage("openmw_input_inputtriggers", [&lua]() { + auto inputTriggers = lua.new_usertype("InputTriggers"); + inputTriggers[sol::meta_function::index] + = [](LuaUtil::InputTrigger::Registry& registry, std::string_view key) { return registry[key]; }; + { + auto pairs = [](LuaUtil::InputTrigger::Registry& registry) { + auto next = [](LuaUtil::InputTrigger::Registry& registry, std::string_view key) + -> sol::optional> { + std::optional nextKey(registry.nextKey(key)); + if (!nextKey.has_value()) + return sol::nullopt; + else + return std::make_tuple(*nextKey, registry[*nextKey].value()); + }; + return std::make_tuple(next, registry, registry.firstKey()); }; - return std::make_tuple(next, registry, registry.firstKey()); - }; - inputTriggers[sol::meta_function::pairs] = pairs; - } + inputTriggers[sol::meta_function::pairs] = pairs; + } + return sol::table(lua, sol::create); + }); - auto triggerInfo = lua.new_usertype("TriggerInfo"); - triggerInfo["key"] = sol::readonly_property( - [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mKey; }); - triggerInfo["name"] = sol::readonly_property( - [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mName; }); - triggerInfo["description"] = sol::readonly_property( - [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mDescription; }); - triggerInfo["l10n"] = sol::readonly_property( - [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mL10n; }); + context.cachePackage("openmw_input_triggerinfo", [&lua]() { + auto triggerInfo = lua.new_usertype("TriggerInfo"); + triggerInfo["key"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mKey; }); + triggerInfo["name"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mName; }); + triggerInfo["description"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mDescription; }); + triggerInfo["l10n"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mL10n; }); + return sol::table(lua, sol::create); + }); MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); sol::table api(lua, sol::create); @@ -449,8 +471,8 @@ namespace MWLua { "Tab", SDL_SCANCODE_TAB }, })); - lua["openmw_input"] = LuaUtil::makeReadOnly(api); - return lua["openmw_input"]; + sol::table readOnlyApi = LuaUtil::makeReadOnly(api); + return context.setTypePackage(readOnlyApi, "openmw_input"); } } From 124ada8d1414e22ba0ef9ee77a6121c874e7b1fe Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Thu, 6 Mar 2025 00:18:10 +0300 Subject: [PATCH 065/154] Add addressed Korean font issue (#8378) to the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a731a63179..ae65cbcc36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -228,6 +228,7 @@ Bug #8295: Post-processing chain is case-sensitive Bug #8299: Crash while smoothing landscape Bug #8364: Crash when clicking scrollbar without handle (divide by zero) + Bug #8378: Korean bitmap fonts are unusable Feature #1415: Infinite fall failsafe Feature #2566: Handle NAM9 records for manual cell references Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking From f80c7b2355deffe4c1db891626d4ee2ac95372d9 Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 22:31:38 +0100 Subject: [PATCH 066/154] Expect openmw.cfg to exist --- scripts/integration_tests.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/integration_tests.py b/scripts/integration_tests.py index 17f89fbe2f..d742ac8bf9 100755 --- a/scripts/integration_tests.py +++ b/scripts/integration_tests.py @@ -58,10 +58,8 @@ def runTest(name): ) for path in content_paths: omw_cfg.write(f'content={path.name}\n') - if (test_dir / "openmw.cfg").exists(): - omw_cfg.write(open(test_dir / "openmw.cfg").read()) - elif (test_dir / "test.omwscripts").exists(): - omw_cfg.write("content=test.omwscripts\n") + with open(test_dir / "openmw.cfg") as stream: + omw_cfg.write(stream.read()) with open(config_dir / "settings.cfg", "a", encoding="utf-8") as settings_cfg: settings_cfg.write( "[Video]\n" From c298210844d9c5e60a90886178d80af549539a4f Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 22:35:51 +0100 Subject: [PATCH 067/154] Make integration_tests.py output more verbose * Make it look more like googletest. * Print total and failed number of tests. * Print failed tests names. * Print duration of each test and total. * Hide all logs by default. --- scripts/integration_tests.py | 79 +++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/scripts/integration_tests.py b/scripts/integration_tests.py index d742ac8bf9..850ac30b71 100755 --- a/scripts/integration_tests.py +++ b/scripts/integration_tests.py @@ -1,6 +1,13 @@ #!/usr/bin/env python3 -import argparse, datetime, os, subprocess, sys, shutil +import argparse +import datetime +import os +import shutil +import subprocess +import sys +import time + from pathlib import Path parser = argparse.ArgumentParser(description="OpenMW integration tests.") @@ -40,12 +47,13 @@ testing_util_dir = tests_dir / "testing_util" time_str = datetime.datetime.now().strftime("%Y-%m-%d-%H.%M.%S") -def runTest(name): - print(f"Start {name}") +def run_test(test_name): + start = time.time() + print(f'[----------] Running tests from {test_name}') shutil.rmtree(config_dir, ignore_errors=True) config_dir.mkdir() shutil.copyfile(example_suite_dir / "settings.cfg", config_dir / "settings.cfg") - test_dir = tests_dir / name + test_dir = tests_dir / test_name with open(config_dir / "openmw.cfg", "w", encoding="utf-8") as omw_cfg: for path in content_paths: omw_cfg.write(f'data="{path.parent}"\n') @@ -72,61 +80,76 @@ def runTest(name): f"memory limit = {1024 * 1024 * 256}\n" ) stdout_lines = list() - exit_ok = True test_success = True + fatal_errors = list() with subprocess.Popen( [openmw_binary, "--replace=config", "--config", config_dir, "--skip-menu", "--no-grab", "--no-sound"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8", env={ - "OPENMW_OSG_STATS_FILE": str(work_dir / f"{name}.{time_str}.osg_stats.log"), + "OPENMW_OSG_STATS_FILE": str(work_dir / f"{test_name}.{time_str}.osg_stats.log"), "OPENMW_OSG_STATS_LIST": "times", **os.environ, }, ) as process: quit_requested = False + running_test_number = None + running_test_name = None + count = 0 + failed_tests = list() + test_start = None for line in process.stdout: if args.verbose: sys.stdout.write(line) else: stdout_lines.append(line) - words = line.split(" ") - if len(words) > 1 and words[1] == "E]": - print(line, end="") - elif "Quit requested by a Lua script" in line: + if "Quit requested by a Lua script" in line: quit_requested = True elif "TEST_START" in line: - w = line.split("TEST_START")[1].split("\t") - print(f"TEST {w[2].strip()}\t\t", end="") + test_start = time.time() + number, name = line.split("TEST_START")[1].strip().split("\t", maxsplit=1) + running_test_number = int(number) + running_test_name = name + count += 1 + print(f"[ RUN ] {running_test_name}") elif "TEST_OK" in line: - print(f"OK") + duration = (time.time() - test_start) * 1000 + number, name = line.split("TEST_OK")[1].strip().split("\t", maxsplit=1) + assert running_test_number == int(number) + print(f"[ OK ] {running_test_name} ({duration:.3f} ms)") elif "TEST_FAILED" in line: - w = line.split("TEST_FAILED")[1].split("\t") - print(f"FAILED {w[3]}\t\t") - test_success = False + duration = (time.time() - test_start) * 1000 + number, name, error = line.split("TEST_FAILED")[1].strip().split("\t", maxsplit=2) + assert running_test_number == int(number) + print(error) + print(f"[ FAILED ] {running_test_name} ({duration:.3f} ms)") + failed_tests.append(running_test_name) process.wait(5) if not quit_requested: - print("ERROR: Unexpected termination") - exit_ok = False + fatal_errors.append("unexpected termination") if process.returncode != 0: - print(f"ERROR: openmw exited with code {process.returncode}") - exit_ok = False + fatal_errors.append(f"openmw exited with code {process.returncode}") if os.path.exists(config_dir / "openmw.log"): - shutil.copyfile(config_dir / "openmw.log", work_dir / f"{name}.{time_str}.log") - if not exit_ok and not args.verbose: + shutil.copyfile(config_dir / "openmw.log", work_dir / f"{test_name}.{time_str}.log") + if fatal_errors and not args.verbose: sys.stdout.writelines(stdout_lines) - if test_success and exit_ok: - print(f"{name} succeeded") - else: - print(f"{name} failed") - return test_success and exit_ok + total_duration = (time.time() - start) * 1000 + print(f'\n[----------] {count} tests from {test_name} ({total_duration:.3f} ms total)') + print(f"[ PASSED ] {count - len(failed_tests)} tests.") + if fatal_errors: + print(f"[ FAILED ] fatal error: {'; '.join(fatal_errors)}") + if failed_tests: + print(f"[ FAILED ] {len(failed_tests)} tests, listed below:") + for failed_test in failed_tests: + print(f"[ FAILED ] {failed_test}") + return len(failed_tests) == 0 and not fatal_errors status = 0 for entry in tests_dir.glob("test_*"): if entry.is_dir(): - if not runTest(entry.name): + if not run_test(entry.name): status = -1 if status == 0: shutil.rmtree(config_dir, ignore_errors=True) From 8b62f025231565a4beb9a07d93d6164caee8108f Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 22:55:35 +0100 Subject: [PATCH 068/154] Use world.players to initialize player in global tests --- .../integration_tests/test_lua_api/test.lua | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/scripts/data/integration_tests/test_lua_api/test.lua b/scripts/data/integration_tests/test_lua_api/test.lua index ff2cd9bb33..5686065698 100644 --- a/scripts/data/integration_tests/test_lua_api/test.lua +++ b/scripts/data/integration_tests/test_lua_api/test.lua @@ -42,6 +42,7 @@ local function testTimers() end local function testTeleport() + local player = world.players[1] player:teleport('', util.vector3(100, 50, 500), util.transform.rotateZ(math.rad(90))) coroutine.yield() testing.expect(player.cell.isExterior, 'teleport to exterior failed') @@ -221,8 +222,10 @@ local function testMemoryLimit() end local function initPlayer() + local player = world.players[1] player:teleport('', util.vector3(4096, 4096, 1745), util.transform.identity) coroutine.yield() + return player end local function testVFS() @@ -272,8 +275,7 @@ local function testVFS() end local function testCommitCrime() - initPlayer() - local player = world.players[1] + local player = initPlayer() testing.expectEqual(player == nil, false, 'A viable player reference should exist to run `testCommitCrime`') testing.expectEqual(I.Crimes == nil, false, 'Crimes interface should be available in global contexts') @@ -295,51 +297,50 @@ local function testCommitCrime() end local function testRecordModelProperty() - initPlayer() - local player = world.players[1] + local player = initPlayer() testing.expectEqual(types.NPC.record(player).model, 'meshes/basicplayer.dae') end tests = { {'timers', testTimers}, {'rotating player with controls.yawChange should change rotation', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'playerYawRotation') end}, {'rotating player with controls.pitchChange should change rotation', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'playerPitchRotation') end}, {'rotating player with controls.pitchChange and controls.yawChange should change rotation', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'playerPitchAndYawRotation') end}, {'rotating player should not lead to nan rotation', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'playerRotation') end}, {'playerForwardRunning', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'playerForwardRunning') end}, {'playerDiagonalWalking', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'playerDiagonalWalking') end}, {'findPath', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'findPath') end}, {'findRandomPointAroundCircle', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'findRandomPointAroundCircle') end}, {'castNavigationRay', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'castNavigationRay') end}, {'findNearestNavMeshPosition', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'findNearestNavMeshPosition') end}, {'teleport', testTeleport}, @@ -351,11 +352,11 @@ tests = { {'mwscript', testMWScript}, {'testMemoryLimit', testMemoryLimit}, {'playerMemoryLimit', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'playerMemoryLimit') end}, {'player with equipped weapon on attack should damage health of other actors', function() - initPlayer() + local player = initPlayer() world.createObject('basic_dagger1h', 1):moveInto(player) testing.runLocalTest(player, 'playerWeaponAttack') end}, @@ -367,7 +368,6 @@ tests = { return { engineHandlers = { onUpdate = testing.testRunner(tests), - onPlayerAdded = function(p) player = p end, }, eventHandlers = testing.eventHandlers, } From 981ca957c1755fd59bfbdd3ef95d74ea790e15a5 Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 23:12:17 +0100 Subject: [PATCH 069/154] Register global tests to run them --- .../integration_tests/test_lua_api/test.lua | 135 +++++++----------- .../testing_util/testing_util.lua | 51 ++++--- scripts/data/morrowind_tests/global.lua | 23 +-- .../data/morrowind_tests/global_dialogues.lua | 66 ++++----- .../data/morrowind_tests/global_issues.lua | 66 ++++----- .../data/morrowind_tests/global_mwscript.lua | 64 ++++----- 6 files changed, 181 insertions(+), 224 deletions(-) diff --git a/scripts/data/integration_tests/test_lua_api/test.lua b/scripts/data/integration_tests/test_lua_api/test.lua index 5686065698..f55c44df63 100644 --- a/scripts/data/integration_tests/test_lua_api/test.lua +++ b/scripts/data/integration_tests/test_lua_api/test.lua @@ -7,7 +7,7 @@ local vfs = require('openmw.vfs') local world = require('openmw.world') local I = require('openmw.interfaces') -local function testTimers() +testing.registerGlobalTest('testTimers', function() testing.expectAlmostEqual(core.getGameTimeScale(), 30, 'incorrect getGameTimeScale() result') testing.expectAlmostEqual(core.getSimulationTimeScale(), 1, 'incorrect getSimulationTimeScale result') @@ -39,9 +39,9 @@ local function testTimers() testing.expectGreaterOrEqual(ts1, 0.5, 'async:newSimulationTimer failed') testing.expectGreaterOrEqual(th2, 72, 'async:newUnsavableGameTimer failed') testing.expectGreaterOrEqual(ts2, 1, 'async:newUnsavableSimulationTimer failed') -end +end) -local function testTeleport() +testing.registerGlobalTest('testTeleport', function() local player = world.players[1] player:teleport('', util.vector3(100, 50, 500), util.transform.rotateZ(math.rad(90))) coroutine.yield() @@ -72,16 +72,16 @@ local function testTeleport() testing.expectEqualWithDelta(player.position.x, 50, 1, 'incorrect position after teleporting') testing.expectEqualWithDelta(player.position.y, -100, 1, 'incorrect position after teleporting') testing.expectEqualWithDelta(player.rotation:getYaw(), math.rad(-90), 0.05, 'teleporting changes rotation') -end +end) -local function testGetGMST() +testing.registerGlobalTest('testGetGMST', function() testing.expectEqual(core.getGMST('non-existed gmst'), nil) testing.expectEqual(core.getGMST('Water_RippleFrameCount'), 4) testing.expectEqual(core.getGMST('Inventory_DirectionalDiffuseR'), 0.5) testing.expectEqual(core.getGMST('Level_Up_Level2'), 'something') -end +end) -local function testMWScript() +testing.registerGlobalTest('testMWScript', function() local variableStoreCount = 18 local variableStore = world.mwscript.getGlobalVariables(player) testing.expectEqual(variableStoreCount, #variableStore) @@ -101,7 +101,7 @@ local function testMWScript() indexCheck = indexCheck + 1 end testing.expectEqual(variableStoreCount, indexCheck) -end +end) local function testRecordStore(store, storeName, skipPairs) testing.expect(store.records) @@ -122,7 +122,7 @@ local function testRecordStore(store, storeName, skipPairs) testing.expectEqual(status, true, storeName) end -local function testRecordStores() +testing.registerGlobalTest('testRecordStores', function() for key, type in pairs(types) do if type.records then testRecordStore(type, key) @@ -141,9 +141,9 @@ local function testRecordStores() testRecordStore(types.NPC.classes, "classes") testRecordStore(types.NPC.races, "races") testRecordStore(types.Player.birthSigns, "birthSigns") -end +end) -local function testRecordCreation() +testing.registerGlobalTest('testRecordCreation', function() local newLight = { isCarriable = true, isDynamic = true, @@ -166,9 +166,9 @@ local function testRecordCreation() for key, value in pairs(newLight) do testing.expectEqual(record[key], value) end -end +end) -local function testUTF8Chars() +testing.registerGlobalTest('testUTF8Chars', function() testing.expectEqual(utf8.codepoint("😀"), 0x1F600) local chars = {} @@ -193,9 +193,9 @@ local function testUTF8Chars() testing.expectEqual(utf8.codepoint(char), codepoint) testing.expectEqual(utf8.len(char), 1) end -end +end) -local function testUTF8Strings() +testing.registerGlobalTest('testUTF8Strings', function() local utf8str = "Hello, 你好, 🌎!" local str = "" @@ -206,9 +206,9 @@ local function testUTF8Strings() testing.expectEqual(utf8.len(utf8str), 13) testing.expectEqual(utf8.offset(utf8str, 9), 11) -end +end) -local function testMemoryLimit() +testing.registerGlobalTest('testMemoryLimit', function() local ok, err = pcall(function() local t = {} local n = 1 @@ -219,7 +219,7 @@ local function testMemoryLimit() end) testing.expectEqual(ok, false, 'Script reaching memory limit should fail') testing.expectEqual(err, 'not enough memory') -end +end) local function initPlayer() local player = world.players[1] @@ -228,7 +228,7 @@ local function initPlayer() return player end -local function testVFS() +testing.registerGlobalTest('testVFS', function() local file = 'test_vfs_dir/lines.txt' local nosuchfile = 'test_vfs_dir/nosuchfile' testing.expectEqual(vfs.fileExists(file), true, 'lines.txt should exist') @@ -272,9 +272,9 @@ local function testVFS() for _,v in pairs(expectedLines) do testing.expectEqual(getLine(), v) end -end +end) -local function testCommitCrime() +testing.registerGlobalTest('testCommitCrime', function() local player = initPlayer() testing.expectEqual(player == nil, false, 'A viable player reference should exist to run `testCommitCrime`') testing.expectEqual(I.Crimes == nil, false, 'Crimes interface should be available in global contexts') @@ -294,80 +294,41 @@ local function testCommitCrime() types.Player.setCrimeLevel(player, 0) testing.expectEqual(I.Crimes.commitCrime(player, { victim = victim, type = types.Player.OFFENSE_TYPE.Theft, arg = 50 }).wasCrimeSeen, true, "Running a crime with a valid victim should notify them when the player is not sneaking, even if it's not explicitly passed in") testing.expectEqual(types.Player.getCrimeLevel(player), 0, "Crime level should not change if the victim's alarm value is low and there's no other witnesses") -end +end) -local function testRecordModelProperty() +testing.registerGlobalTest('testRecordModelProperty', function() local player = initPlayer() testing.expectEqual(types.NPC.record(player).model, 'meshes/basicplayer.dae') +end) + +local function registerPlayerTest(name) + testing.registerGlobalTest(name, function() + local player = initPlayer() + testing.runLocalTest(player, name) + end) end -tests = { - {'timers', testTimers}, - {'rotating player with controls.yawChange should change rotation', function() - local player = initPlayer() - testing.runLocalTest(player, 'playerYawRotation') - end}, - {'rotating player with controls.pitchChange should change rotation', function() - local player = initPlayer() - testing.runLocalTest(player, 'playerPitchRotation') - end}, - {'rotating player with controls.pitchChange and controls.yawChange should change rotation', function() - local player = initPlayer() - testing.runLocalTest(player, 'playerPitchAndYawRotation') - end}, - {'rotating player should not lead to nan rotation', function() - local player = initPlayer() - testing.runLocalTest(player, 'playerRotation') - end}, - {'playerForwardRunning', function() - local player = initPlayer() - testing.runLocalTest(player, 'playerForwardRunning') - end}, - {'playerDiagonalWalking', function() - local player = initPlayer() - testing.runLocalTest(player, 'playerDiagonalWalking') - end}, - {'findPath', function() - local player = initPlayer() - testing.runLocalTest(player, 'findPath') - end}, - {'findRandomPointAroundCircle', function() - local player = initPlayer() - testing.runLocalTest(player, 'findRandomPointAroundCircle') - end}, - {'castNavigationRay', function() - local player = initPlayer() - testing.runLocalTest(player, 'castNavigationRay') - end}, - {'findNearestNavMeshPosition', function() - local player = initPlayer() - testing.runLocalTest(player, 'findNearestNavMeshPosition') - end}, - {'teleport', testTeleport}, - {'getGMST', testGetGMST}, - {'recordStores', testRecordStores}, - {'recordCreation', testRecordCreation}, - {'utf8Chars', testUTF8Chars}, - {'utf8Strings', testUTF8Strings}, - {'mwscript', testMWScript}, - {'testMemoryLimit', testMemoryLimit}, - {'playerMemoryLimit', function() - local player = initPlayer() - testing.runLocalTest(player, 'playerMemoryLimit') - end}, - {'player with equipped weapon on attack should damage health of other actors', function() - local player = initPlayer() - world.createObject('basic_dagger1h', 1):moveInto(player) - testing.runLocalTest(player, 'playerWeaponAttack') - end}, - {'vfs', testVFS}, - {'testCommitCrime', testCommitCrime}, - {'recordModelProperty', testRecordModelProperty}, -} +registerPlayerTest('playerYawRotation') +registerPlayerTest('playerPitchRotation') +registerPlayerTest('playerPitchAndYawRotation') +registerPlayerTest('playerRotation') +registerPlayerTest('playerForwardRunning') +registerPlayerTest('playerDiagonalWalking') +registerPlayerTest('findPath') +registerPlayerTest('findRandomPointAroundCircle') +registerPlayerTest('castNavigationRay') +registerPlayerTest('findNearestNavMeshPosition') +registerPlayerTest('playerMemoryLimit') + +testing.registerGlobalTest('playerWeaponAttack', function() + local player = initPlayer() + world.createObject('basic_dagger1h', 1):moveInto(player) + testing.runLocalTest(player, 'playerWeaponAttack') +end) return { engineHandlers = { - onUpdate = testing.testRunner(tests), + onUpdate = testing.makeUpdateGlobal(), }, eventHandlers = testing.eventHandlers, } diff --git a/scripts/data/integration_tests/testing_util/testing_util.lua b/scripts/data/integration_tests/testing_util/testing_util.lua index 7b886636ed..2d67c6ca8a 100644 --- a/scripts/data/integration_tests/testing_util/testing_util.lua +++ b/scripts/data/integration_tests/testing_util/testing_util.lua @@ -2,12 +2,19 @@ local core = require('openmw.core') local util = require('openmw.util') local M = {} + +local globalTestsOrder = {} +local globalTests = {} +local globalTestRunner = nil + +local localTests = {} +local localTestRunner = nil local currentLocalTest = nil local currentLocalTestError = nil -function M.testRunner(tests) +function M.makeUpdateGlobal() local fn = function() - for i, test in ipairs(tests) do + for i, test in ipairs(globalTestsOrder) do local name, fn = unpack(test) print('TEST_START', i, name) local status, err = pcall(fn) @@ -27,6 +34,11 @@ function M.testRunner(tests) end end +function M.registerGlobalTest(name, fn) + globalTests[name] = fn + table.insert(globalTestsOrder, {name, fn}) +end + function M.runLocalTest(obj, name) currentLocalTest = name currentLocalTestError = nil @@ -39,7 +51,21 @@ function M.runLocalTest(obj, name) end end -function M.expect(cond, delta, msg) +function M.registerLocalTest(name, fn) + localTests[name] = fn +end + +function M.updateLocal() + if localTestRunner and coroutine.status(localTestRunner) ~= 'dead' then + if not core.isWorldPaused() then + coroutine.resume(localTestRunner) + end + else + localTestRunner = nil + end +end + +function M.expect(cond, msg) if not cond then error(msg or '"true" expected', 2) end @@ -182,28 +208,11 @@ function M.formatActualExpected(actual, expected) return string.format('actual: %s, expected: %s', actual, expected) end -local localTests = {} -local localTestRunner = nil - -function M.registerLocalTest(name, fn) - localTests[name] = fn -end - -function M.updateLocal() - if localTestRunner and coroutine.status(localTestRunner) ~= 'dead' then - if not core.isWorldPaused() then - coroutine.resume(localTestRunner) - end - else - localTestRunner = nil - end -end - M.eventHandlers = { runLocalTest = function(name) -- used only in local scripts fn = localTests[name] if not fn then - core.sendGlobalEvent('localTestFinished', {name=name, errMsg='Test not found'}) + core.sendGlobalEvent('localTestFinished', {name=name, errMsg='Local test is not found'}) return end localTestRunner = coroutine.create(function() diff --git a/scripts/data/morrowind_tests/global.lua b/scripts/data/morrowind_tests/global.lua index fb7113d1b1..a78d90e408 100644 --- a/scripts/data/morrowind_tests/global.lua +++ b/scripts/data/morrowind_tests/global.lua @@ -8,28 +8,13 @@ if not core.contentFiles.has('Morrowind.esm') then error('This test requires Morrowind.esm') end -function makeTests(modules) - local tests = {} - - for _, moduleName in ipairs(modules) do - local module = require(moduleName) - for _, v in ipairs(module) do - table.insert(tests, {string.format('[%s] %s', moduleName, v[1]), v[2]}) - end - end - - return tests -end - -local testModules = { - 'global_issues', - 'global_dialogues', - 'global_mwscript', -} +require('global_issues') +require('global_dialogues') +require('global_mwscript') return { engineHandlers = { - onUpdate = testing.testRunner(makeTests(testModules)), + onUpdate = testing.makeUpdateGlobal(), }, eventHandlers = testing.eventHandlers, } diff --git a/scripts/data/morrowind_tests/global_dialogues.lua b/scripts/data/morrowind_tests/global_dialogues.lua index 397eb8461c..68f4f4a747 100644 --- a/scripts/data/morrowind_tests/global_dialogues.lua +++ b/scripts/data/morrowind_tests/global_dialogues.lua @@ -13,35 +13,37 @@ function iterateOverRecords(records) return firstRecordId, lastRecordId, count end -return { - {'Should support iteration over journal dialogues', function() - local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.journal.records) - testing.expectEqual(firstRecordId, '11111 test journal') - testing.expectEqual(lastRecordId, 'va_vamprich') - testing.expectEqual(count, 632) - end}, - {'Should support iteration over topic dialogues', function() - local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.topic.records) - testing.expectEqual(firstRecordId, '1000-drake pledge') - testing.expectEqual(lastRecordId, 'zenithar') - testing.expectEqual(count, 1698) - end}, - {'Should support iteration over greeting dialogues', function() - local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.greeting.records) - testing.expectEqual(firstRecordId, 'greeting 0') - testing.expectEqual(lastRecordId, 'greeting 9') - testing.expectEqual(count, 10) - end}, - {'Should support iteration over persuasion dialogues', function() - local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.persuasion.records) - testing.expectEqual(firstRecordId, 'admire fail') - testing.expectEqual(lastRecordId, 'taunt success') - testing.expectEqual(count, 10) - end}, - {'Should support iteration over voice dialogues', function() - local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.voice.records) - testing.expectEqual(firstRecordId, 'alarm') - testing.expectEqual(lastRecordId, 'thief') - testing.expectEqual(count, 8) - end}, -} +testing.registerGlobalTest('[dialogues] Should support iteration over journal dialogues', function() + local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.journal.records) + testing.expectEqual(firstRecordId, '11111 test journal') + testing.expectEqual(lastRecordId, 'va_vamprich') + testing.expectEqual(count, 632) +end) + +testing.registerGlobalTest('[dialogues] Should support iteration over topic dialogues', function() + local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.topic.records) + testing.expectEqual(firstRecordId, '1000-drake pledge') + testing.expectEqual(lastRecordId, 'zenithar') + testing.expectEqual(count, 1698) +end) + +testing.registerGlobalTest('[dialogues] Should support iteration over greeting dialogues', function() + local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.greeting.records) + testing.expectEqual(firstRecordId, 'greeting 0') + testing.expectEqual(lastRecordId, 'greeting 9') + testing.expectEqual(count, 10) +end) + +testing.registerGlobalTest('[dialogues] Should support iteration over persuasion dialogues', function() + local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.persuasion.records) + testing.expectEqual(firstRecordId, 'admire fail') + testing.expectEqual(lastRecordId, 'taunt success') + testing.expectEqual(count, 10) +end) + +testing.registerGlobalTest('[dialogues] Should support iteration over voice dialogues', function() + local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.voice.records) + testing.expectEqual(firstRecordId, 'alarm') + testing.expectEqual(lastRecordId, 'thief') + testing.expectEqual(count, 8) +end) diff --git a/scripts/data/morrowind_tests/global_issues.lua b/scripts/data/morrowind_tests/global_issues.lua index 2afad085b0..a3400da87c 100644 --- a/scripts/data/morrowind_tests/global_issues.lua +++ b/scripts/data/morrowind_tests/global_issues.lua @@ -4,37 +4,37 @@ local world = require('openmw.world') local core = require('openmw.core') local types = require('openmw.types') -return { - {'Player should be able to walk up stairs in Ebonheart docks (#4247)', function() - world.players[1]:teleport('', util.vector3(19867, -102180, -79), util.transform.rotateZ(math.rad(91))) - coroutine.yield() - testing.runLocalTest(world.players[1], 'Player should be able to walk up stairs in Ebonheart docks (#4247)') - end}, - {'Guard in Imperial Prison Ship should find path (#7241)', function() - world.players[1]:teleport('Imperial Prison Ship', util.vector3(61, -135, -105), util.transform.rotateZ(math.rad(-20))) - coroutine.yield() - testing.runLocalTest(world.players[1], 'Guard in Imperial Prison Ship should find path (#7241)') - end}, - {'Should keep reference to an object moved into container (#7663)', function() - world.players[1]:teleport('ToddTest', util.vector3(2176, 3648, -191), util.transform.rotateZ(math.rad(0))) - coroutine.yield() - local barrel = world.createObject('barrel_01', 1) - local fargothRing = world.createObject('ring_keley', 1) - coroutine.yield() - testing.expectEqual(types.Container.inventory(barrel):find('ring_keley'), nil) - fargothRing:moveInto(types.Container.inventory(barrel)) - coroutine.yield() - testing.expectEqual(fargothRing.recordId, 'ring_keley') - local isFargothRing = function(actual) - if actual == nil then - return 'ring_keley is not found' - end - if actual.id ~= fargothRing.id then - return 'found ring_keley id does not match expected: actual=' .. tostring(actual.id) - .. ', expected=' .. tostring(fargothRing.id) - end - return '' +testing.registerGlobalTest('[issues] Player should be able to walk up stairs in Ebonheart docks (#4247)', function() + world.players[1]:teleport('', util.vector3(19867, -102180, -79), util.transform.rotateZ(math.rad(91))) + coroutine.yield() + testing.runLocalTest(world.players[1], 'Player should be able to walk up stairs in Ebonheart docks (#4247)') +end) + +testing.registerGlobalTest('[issues] Guard in Imperial Prison Ship should find path (#7241)', function() + world.players[1]:teleport('Imperial Prison Ship', util.vector3(61, -135, -105), util.transform.rotateZ(math.rad(-20))) + coroutine.yield() + testing.runLocalTest(world.players[1], 'Guard in Imperial Prison Ship should find path (#7241)') +end) + +testing.registerGlobalTest('[issues] Should keep reference to an object moved into container (#7663)', function() + world.players[1]:teleport('ToddTest', util.vector3(2176, 3648, -191), util.transform.rotateZ(math.rad(0))) + coroutine.yield() + local barrel = world.createObject('barrel_01', 1) + local fargothRing = world.createObject('ring_keley', 1) + coroutine.yield() + testing.expectEqual(types.Container.inventory(barrel):find('ring_keley'), nil) + fargothRing:moveInto(types.Container.inventory(barrel)) + coroutine.yield() + testing.expectEqual(fargothRing.recordId, 'ring_keley') + local isFargothRing = function(actual) + if actual == nil then + return 'ring_keley is not found' end - testing.expectThat(types.Container.inventory(barrel):find('ring_keley'), isFargothRing) - end}, -} + if actual.id ~= fargothRing.id then + return 'found ring_keley id does not match expected: actual=' .. tostring(actual.id) + .. ', expected=' .. tostring(fargothRing.id) + end + return '' + end + testing.expectThat(types.Container.inventory(barrel):find('ring_keley'), isFargothRing) +end) diff --git a/scripts/data/morrowind_tests/global_mwscript.lua b/scripts/data/morrowind_tests/global_mwscript.lua index a4347ea66e..fec9fcdba5 100644 --- a/scripts/data/morrowind_tests/global_mwscript.lua +++ b/scripts/data/morrowind_tests/global_mwscript.lua @@ -14,38 +14,38 @@ function iterateOverVariables(variables) return first, last, count end -return { - {'Should support iteration over an empty set of script variables', function() - local mainVars = world.mwscript.getGlobalScript('main').variables - local first, last, count = iterateOverVariables(mainVars) - testing.expectEqual(first, nil) - testing.expectEqual(last, nil) - testing.expectEqual(count, 0) - testing.expectEqual(count, #mainVars) - end}, - {'Should support iteration of script variables', function() - local jiub = world.getObjectByFormId(core.getFormId('Morrowind.esm', 172867)) - local jiubVars = world.mwscript.getLocalScript(jiub).variables - local first, last, count = iterateOverVariables(jiubVars) +testing.registerGlobalTest('[mwscript] Should support iteration over an empty set of script variables', function() + local mainVars = world.mwscript.getGlobalScript('main').variables + local first, last, count = iterateOverVariables(mainVars) + testing.expectEqual(first, nil) + testing.expectEqual(last, nil) + testing.expectEqual(count, 0) + testing.expectEqual(count, #mainVars) +end) - testing.expectEqual(first, 'state') - testing.expectEqual(last, 'timer') - testing.expectEqual(count, 3) - testing.expectEqual(count, #jiubVars) - end}, - {'Should support numeric and string indices for getting and setting', function() - local jiub = world.getObjectByFormId(core.getFormId('Morrowind.esm', 172867)) - local jiubVars = world.mwscript.getLocalScript(jiub).variables +testing.registerGlobalTest('[mwscript] Should support iteration of script variables', function() + local jiub = world.getObjectByFormId(core.getFormId('Morrowind.esm', 172867)) + local jiubVars = world.mwscript.getLocalScript(jiub).variables + local first, last, count = iterateOverVariables(jiubVars) - testing.expectEqual(jiubVars[1], jiubVars.state) - testing.expectEqual(jiubVars[2], jiubVars.wandering) - testing.expectEqual(jiubVars[3], jiubVars.timer) + testing.expectEqual(first, 'state') + testing.expectEqual(last, 'timer') + testing.expectEqual(count, 3) + testing.expectEqual(count, #jiubVars) +end) - jiubVars[1] = 123; - testing.expectEqual(jiubVars.state, 123) - jiubVars.wandering = 42; - testing.expectEqual(jiubVars[2], 42) - jiubVars[3] = 1.25; - testing.expectEqual(jiubVars.timer, 1.25) - end}, -} +testing.registerGlobalTest('[mwscript] Should support numeric and string indices for getting and setting', function() + local jiub = world.getObjectByFormId(core.getFormId('Morrowind.esm', 172867)) + local jiubVars = world.mwscript.getLocalScript(jiub).variables + + testing.expectEqual(jiubVars[1], jiubVars.state) + testing.expectEqual(jiubVars[2], jiubVars.wandering) + testing.expectEqual(jiubVars[3], jiubVars.timer) + + jiubVars[1] = 123; + testing.expectEqual(jiubVars.state, 123) + jiubVars.wandering = 42; + testing.expectEqual(jiubVars[2], 42) + jiubVars[3] = 1.25; + testing.expectEqual(jiubVars.timer, 1.25) +end) From 7a9c2d5e88e7e8ab9e4355776dd0b8263851de04 Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 23:18:07 +0100 Subject: [PATCH 070/154] Split local and global event handlers --- .../integration_tests/test_lua_api/player.lua | 2 +- .../integration_tests/test_lua_api/test.lua | 2 +- .../testing_util/testing_util.lua | 23 +++++++++++-------- scripts/data/morrowind_tests/global.lua | 2 +- scripts/data/morrowind_tests/player.lua | 2 +- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/scripts/data/integration_tests/test_lua_api/player.lua b/scripts/data/integration_tests/test_lua_api/player.lua index 16f0f2eea3..9620866e6e 100644 --- a/scripts/data/integration_tests/test_lua_api/player.lua +++ b/scripts/data/integration_tests/test_lua_api/player.lua @@ -346,5 +346,5 @@ return { engineHandlers = { onFrame = testing.updateLocal, }, - eventHandlers = testing.eventHandlers + eventHandlers = testing.localEventHandlers, } diff --git a/scripts/data/integration_tests/test_lua_api/test.lua b/scripts/data/integration_tests/test_lua_api/test.lua index f55c44df63..2f336de8d9 100644 --- a/scripts/data/integration_tests/test_lua_api/test.lua +++ b/scripts/data/integration_tests/test_lua_api/test.lua @@ -330,5 +330,5 @@ return { engineHandlers = { onUpdate = testing.makeUpdateGlobal(), }, - eventHandlers = testing.eventHandlers, + eventHandlers = testing.globalEventHandlers, } diff --git a/scripts/data/integration_tests/testing_util/testing_util.lua b/scripts/data/integration_tests/testing_util/testing_util.lua index 2d67c6ca8a..f5de16fb2d 100644 --- a/scripts/data/integration_tests/testing_util/testing_util.lua +++ b/scripts/data/integration_tests/testing_util/testing_util.lua @@ -208,8 +208,20 @@ function M.formatActualExpected(actual, expected) return string.format('actual: %s, expected: %s', actual, expected) end -M.eventHandlers = { - runLocalTest = function(name) -- used only in local scripts +-- used only in global scripts +M.globalEventHandlers = { + localTestFinished = function(data) + if data.name ~= currentLocalTest then + error(string.format('localTestFinished with incorrect name %s, expected %s', data.name, currentLocalTest)) + end + currentLocalTest = nil + currentLocalTestError = data.errMsg + end, +} + +-- used only in local scripts +M.localEventHandlers = { + runLocalTest = function(name) fn = localTests[name] if not fn then core.sendGlobalEvent('localTestFinished', {name=name, errMsg='Local test is not found'}) @@ -223,13 +235,6 @@ M.eventHandlers = { core.sendGlobalEvent('localTestFinished', {name=name, errMsg=err}) end) end, - localTestFinished = function(data) -- used only in global scripts - if data.name ~= currentLocalTest then - error(string.format('localTestFinished with incorrect name %s, expected %s', data.name, currentLocalTest)) - end - currentLocalTest = nil - currentLocalTestError = data.errMsg - end, } return M diff --git a/scripts/data/morrowind_tests/global.lua b/scripts/data/morrowind_tests/global.lua index a78d90e408..f17e72cda9 100644 --- a/scripts/data/morrowind_tests/global.lua +++ b/scripts/data/morrowind_tests/global.lua @@ -16,5 +16,5 @@ return { engineHandlers = { onUpdate = testing.makeUpdateGlobal(), }, - eventHandlers = testing.eventHandlers, + eventHandlers = testing.globalEventHandlers, } diff --git a/scripts/data/morrowind_tests/player.lua b/scripts/data/morrowind_tests/player.lua index 7435b49553..226d9754f0 100644 --- a/scripts/data/morrowind_tests/player.lua +++ b/scripts/data/morrowind_tests/player.lua @@ -80,5 +80,5 @@ return { engineHandlers = { onFrame = testing.updateLocal, }, - eventHandlers = testing.eventHandlers + eventHandlers = testing.localEventHandlers, } From 9bf6a15ff53a69bd69f206f2e44341b81b97c7a4 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 6 Mar 2025 17:32:56 +0100 Subject: [PATCH 071/154] Force move the captured string --- apps/openmw/mwlua/uibindings.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index 76bba0c082..677a51a887 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -288,7 +288,8 @@ namespace MWLua }; api["_setWindowDisabled"] = [windowManager, luaManager = context.mLuaManager](std::string window, bool disabled) { - luaManager->addAction([=]() { windowManager->setDisabledByLua(window, disabled); }); + luaManager->addAction( + [=, window = std::move(window)]() { windowManager->setDisabledByLua(window, disabled); }); }; // TODO From 5776eea1b0bef3694f0be758e5e844d332e31a00 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 8 Mar 2025 00:25:47 +0100 Subject: [PATCH 072/154] Avoid accessing removed character on deleting last save --- apps/openmw/mwstate/charactermanager.cpp | 3 ++- apps/openmw/mwstate/charactermanager.hpp | 2 +- apps/openmw/mwstate/statemanagerimp.cpp | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwstate/charactermanager.cpp b/apps/openmw/mwstate/charactermanager.cpp index c41ddd42c0..32f0fe0aef 100644 --- a/apps/openmw/mwstate/charactermanager.cpp +++ b/apps/openmw/mwstate/charactermanager.cpp @@ -38,7 +38,7 @@ MWState::Character* MWState::CharacterManager::getCurrentCharacter() return mCurrent; } -void MWState::CharacterManager::deleteSlot(const MWState::Character* character, const MWState::Slot* slot) +void MWState::CharacterManager::deleteSlot(const MWState::Slot* slot, const MWState::Character*& character) { std::list::iterator it = findCharacter(character); @@ -51,6 +51,7 @@ void MWState::CharacterManager::deleteSlot(const MWState::Character* character, if (character == mCurrent) mCurrent = nullptr; mCharacters.erase(it); + character = nullptr; } } diff --git a/apps/openmw/mwstate/charactermanager.hpp b/apps/openmw/mwstate/charactermanager.hpp index dac189e68a..015144d820 100644 --- a/apps/openmw/mwstate/charactermanager.hpp +++ b/apps/openmw/mwstate/charactermanager.hpp @@ -33,7 +33,7 @@ namespace MWState Character* getCurrentCharacter(); ///< @note May return null - void deleteSlot(const MWState::Character* character, const MWState::Slot* slot); + void deleteSlot(const MWState::Slot* slot, const Character*& character); Character* createCharacter(const std::string& name); ///< Create new character within saved game management diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index f420589b70..b997d124c8 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -706,10 +706,10 @@ void MWState::StateManager::quickLoad() void MWState::StateManager::deleteGame(const MWState::Character* character, const MWState::Slot* slot) { const std::filesystem::path savePath = slot->mPath; - mCharacterManager.deleteSlot(character, slot); + mCharacterManager.deleteSlot(slot, character); if (mLastSavegame == savePath) { - if (character->begin() != character->end()) + if (character != nullptr) mLastSavegame = character->begin()->mPath; else mLastSavegame.clear(); From f800f63ee54c35e152d4664446a169f880c6d2db Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 8 Mar 2025 12:47:07 +0100 Subject: [PATCH 073/154] Merge deleted refs when unloading a cell To unload objects scheduled to be teleported. --- apps/openmw/mwworld/cellstore.cpp | 18 +++++++++--------- apps/openmw/mwworld/cellstore.hpp | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 4cd189bb20..fca0135e13 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -337,13 +337,13 @@ namespace // helper function for forEachInternal template - bool forEachImp(Visitor& visitor, List& list, MWWorld::CellStore* cellStore) + bool forEachImp(Visitor& visitor, List& list, MWWorld::CellStore& cellStore, bool includeDeleted) { - for (typename List::List::iterator iter(list.mList.begin()); iter != list.mList.end(); ++iter) + for (auto& v : list.mList) { - if (!MWWorld::CellStore::isAccessible(iter->mData, iter->mRef)) + if (!includeDeleted && !MWWorld::CellStore::isAccessible(v.mData, v.mRef)) continue; - if (!visitor(MWWorld::Ptr(&*iter, cellStore))) + if (!visitor(MWWorld::Ptr(&v, &cellStore))) return false; } return true; @@ -399,12 +399,12 @@ namespace MWWorld // listing only objects owned by this cell. Internal use only, you probably want to use forEach() so that moved // objects are accounted for. template - static bool forEachInternal(Visitor& visitor, MWWorld::CellStore& cellStore) + static bool forEachInternal(Visitor& visitor, MWWorld::CellStore& cellStore, bool includeDeleted) { bool returnValue = true; - Misc::tupleForEach(cellStore.mCellStoreImp->mRefLists, [&visitor, &returnValue, &cellStore](auto& store) { - returnValue = returnValue && forEachImp(visitor, store, &cellStore); + Misc::tupleForEach(cellStore.mCellStoreImp->mRefLists, [&](auto& store) { + returnValue = returnValue && forEachImp(visitor, store, cellStore, includeDeleted); }); return returnValue; @@ -583,11 +583,11 @@ namespace MWWorld mMergedRefsNeedsUpdate = true; } - void CellStore::updateMergedRefs() const + void CellStore::updateMergedRefs(bool includeDeleted) const { mMergedRefs.clear(); MergeVisitor visitor(mMergedRefs, mMovedHere, mMovedToAnotherCell); - CellStoreImp::forEachInternal(visitor, const_cast(*this)); + CellStoreImp::forEachInternal(visitor, const_cast(*this), includeDeleted); visitor.merge(); mMergedRefsNeedsUpdate = false; } diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index 2cc9b106a6..126935ace5 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -219,7 +219,7 @@ namespace MWWorld return false; if (mMergedRefsNeedsUpdate) - updateMergedRefs(); + updateMergedRefs(includeDeleted); if (mMergedRefs.empty()) return true; @@ -248,7 +248,7 @@ namespace MWWorld return false; if (mMergedRefsNeedsUpdate) - updateMergedRefs(); + updateMergedRefs(includeDeleted); for (const LiveCellRefBase* mergedRef : mMergedRefs) { @@ -273,7 +273,7 @@ namespace MWWorld return false; if (mMergedRefsNeedsUpdate) - updateMergedRefs(); + updateMergedRefs(includeDeleted); if (mMergedRefs.empty()) return true; @@ -403,7 +403,7 @@ namespace MWWorld /// Repopulate mMergedRefs. void requestMergedRefsUpdate(); - void updateMergedRefs() const; + void updateMergedRefs(bool includeDeleted = false) const; // (item, max charge) typedef std::vector> TRechargingItems; From 0e19b1dd7510133bf024d4f79dd3e74ef4bf6b1c Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 00:55:22 +0100 Subject: [PATCH 074/154] Run Lua integration tests starting with menu script This allows writing tests for menu scripts. Keep global script as entry point to morrowind tests. Fix menu.newGame and menu.loadGame to hide main menu. --- .../test_lua_api/{test.lua => global.lua} | 48 ++++----- .../integration_tests/test_lua_api/menu.lua | 43 ++++++++ .../integration_tests/test_lua_api/openmw.cfg | 2 +- .../integration_tests/test_lua_api/player.lua | 16 +-- .../test_lua_api/test.omwscripts | 2 - .../test_lua_api/test_lua_api.omwscripts | 3 + .../testing_util/testing_util.lua | 102 +++++++++++++++--- scripts/integration_tests.py | 2 +- 8 files changed, 167 insertions(+), 51 deletions(-) rename scripts/data/integration_tests/test_lua_api/{test.lua => global.lua} (91%) create mode 100644 scripts/data/integration_tests/test_lua_api/menu.lua delete mode 100644 scripts/data/integration_tests/test_lua_api/test.omwscripts create mode 100644 scripts/data/integration_tests/test_lua_api/test_lua_api.omwscripts diff --git a/scripts/data/integration_tests/test_lua_api/test.lua b/scripts/data/integration_tests/test_lua_api/global.lua similarity index 91% rename from scripts/data/integration_tests/test_lua_api/test.lua rename to scripts/data/integration_tests/test_lua_api/global.lua index 2f336de8d9..cc8240554a 100644 --- a/scripts/data/integration_tests/test_lua_api/test.lua +++ b/scripts/data/integration_tests/test_lua_api/global.lua @@ -7,7 +7,7 @@ local vfs = require('openmw.vfs') local world = require('openmw.world') local I = require('openmw.interfaces') -testing.registerGlobalTest('testTimers', function() +testing.registerGlobalTest('timers', function() testing.expectAlmostEqual(core.getGameTimeScale(), 30, 'incorrect getGameTimeScale() result') testing.expectAlmostEqual(core.getSimulationTimeScale(), 1, 'incorrect getSimulationTimeScale result') @@ -41,7 +41,7 @@ testing.registerGlobalTest('testTimers', function() testing.expectGreaterOrEqual(ts2, 1, 'async:newUnsavableSimulationTimer failed') end) -testing.registerGlobalTest('testTeleport', function() +testing.registerGlobalTest('teleport', function() local player = world.players[1] player:teleport('', util.vector3(100, 50, 500), util.transform.rotateZ(math.rad(90))) coroutine.yield() @@ -74,14 +74,14 @@ testing.registerGlobalTest('testTeleport', function() testing.expectEqualWithDelta(player.rotation:getYaw(), math.rad(-90), 0.05, 'teleporting changes rotation') end) -testing.registerGlobalTest('testGetGMST', function() +testing.registerGlobalTest('getGMST', function() testing.expectEqual(core.getGMST('non-existed gmst'), nil) testing.expectEqual(core.getGMST('Water_RippleFrameCount'), 4) testing.expectEqual(core.getGMST('Inventory_DirectionalDiffuseR'), 0.5) testing.expectEqual(core.getGMST('Level_Up_Level2'), 'something') end) -testing.registerGlobalTest('testMWScript', function() +testing.registerGlobalTest('MWScript', function() local variableStoreCount = 18 local variableStore = world.mwscript.getGlobalVariables(player) testing.expectEqual(variableStoreCount, #variableStore) @@ -122,7 +122,7 @@ local function testRecordStore(store, storeName, skipPairs) testing.expectEqual(status, true, storeName) end -testing.registerGlobalTest('testRecordStores', function() +testing.registerGlobalTest('record stores', function() for key, type in pairs(types) do if type.records then testRecordStore(type, key) @@ -143,7 +143,7 @@ testing.registerGlobalTest('testRecordStores', function() testRecordStore(types.Player.birthSigns, "birthSigns") end) -testing.registerGlobalTest('testRecordCreation', function() +testing.registerGlobalTest('record creation', function() local newLight = { isCarriable = true, isDynamic = true, @@ -168,7 +168,7 @@ testing.registerGlobalTest('testRecordCreation', function() end end) -testing.registerGlobalTest('testUTF8Chars', function() +testing.registerGlobalTest('UTF-8 characters', function() testing.expectEqual(utf8.codepoint("😀"), 0x1F600) local chars = {} @@ -195,7 +195,7 @@ testing.registerGlobalTest('testUTF8Chars', function() end end) -testing.registerGlobalTest('testUTF8Strings', function() +testing.registerGlobalTest('UTF-8 strings', function() local utf8str = "Hello, 你好, 🌎!" local str = "" @@ -208,7 +208,7 @@ testing.registerGlobalTest('testUTF8Strings', function() testing.expectEqual(utf8.offset(utf8str, 9), 11) end) -testing.registerGlobalTest('testMemoryLimit', function() +testing.registerGlobalTest('memory limit', function() local ok, err = pcall(function() local t = {} local n = 1 @@ -228,7 +228,7 @@ local function initPlayer() return player end -testing.registerGlobalTest('testVFS', function() +testing.registerGlobalTest('vfs', function() local file = 'test_vfs_dir/lines.txt' local nosuchfile = 'test_vfs_dir/nosuchfile' testing.expectEqual(vfs.fileExists(file), true, 'lines.txt should exist') @@ -274,9 +274,9 @@ testing.registerGlobalTest('testVFS', function() end end) -testing.registerGlobalTest('testCommitCrime', function() +testing.registerGlobalTest('commit crime', function() local player = initPlayer() - testing.expectEqual(player == nil, false, 'A viable player reference should exist to run `testCommitCrime`') + testing.expectEqual(player == nil, false, 'A viable player reference should exist to run `commit crime`') testing.expectEqual(I.Crimes == nil, false, 'Crimes interface should be available in global contexts') -- Reset crime level to have a clean slate @@ -296,8 +296,8 @@ testing.registerGlobalTest('testCommitCrime', function() testing.expectEqual(types.Player.getCrimeLevel(player), 0, "Crime level should not change if the victim's alarm value is low and there's no other witnesses") end) -testing.registerGlobalTest('testRecordModelProperty', function() - local player = initPlayer() +testing.registerGlobalTest('record model property', function() + local player = world.players[1] testing.expectEqual(types.NPC.record(player).model, 'meshes/basicplayer.dae') end) @@ -308,27 +308,27 @@ local function registerPlayerTest(name) end) end -registerPlayerTest('playerYawRotation') -registerPlayerTest('playerPitchRotation') -registerPlayerTest('playerPitchAndYawRotation') -registerPlayerTest('playerRotation') -registerPlayerTest('playerForwardRunning') -registerPlayerTest('playerDiagonalWalking') +registerPlayerTest('player yaw rotation') +registerPlayerTest('player pitch rotation') +registerPlayerTest('player pitch and yaw rotation') +registerPlayerTest('player rotation') +registerPlayerTest('player forward running') +registerPlayerTest('player diagonal walking') registerPlayerTest('findPath') registerPlayerTest('findRandomPointAroundCircle') registerPlayerTest('castNavigationRay') registerPlayerTest('findNearestNavMeshPosition') -registerPlayerTest('playerMemoryLimit') +registerPlayerTest('player memory limit') -testing.registerGlobalTest('playerWeaponAttack', function() +testing.registerGlobalTest('player weapon attack', function() local player = initPlayer() world.createObject('basic_dagger1h', 1):moveInto(player) - testing.runLocalTest(player, 'playerWeaponAttack') + testing.runLocalTest(player, 'player weapon attack') end) return { engineHandlers = { - onUpdate = testing.makeUpdateGlobal(), + onUpdate = testing.updateGlobal, }, eventHandlers = testing.globalEventHandlers, } diff --git a/scripts/data/integration_tests/test_lua_api/menu.lua b/scripts/data/integration_tests/test_lua_api/menu.lua new file mode 100644 index 0000000000..37f7cb826e --- /dev/null +++ b/scripts/data/integration_tests/test_lua_api/menu.lua @@ -0,0 +1,43 @@ +local testing = require('testing_util') +local menu = require('openmw.menu') + +local function registerGlobalTest(name, description) + testing.registerMenuTest(description or name, function() + menu.newGame() + coroutine.yield() + testing.runGlobalTest(name) + end) +end + +registerGlobalTest('timers') +registerGlobalTest('teleport') +registerGlobalTest('getGMST') +registerGlobalTest('MWScript') +registerGlobalTest('record stores') +registerGlobalTest('record creation') +registerGlobalTest('UTF-8 characters') +registerGlobalTest('UTF-8 strings') +registerGlobalTest('memory limit') +registerGlobalTest('vfs') +registerGlobalTest('commit crime') +registerGlobalTest('record model property') + +registerGlobalTest('player yaw rotation', 'rotating player with controls.yawChange should change rotation') +registerGlobalTest('player pitch rotation', 'rotating player with controls.pitchChange should change rotation') +registerGlobalTest('player pitch and yaw rotation', 'rotating player with controls.pitchChange and controls.yawChange should change rotation') +registerGlobalTest('player rotation', 'rotating player should not lead to nan rotation') +registerGlobalTest('player forward running') +registerGlobalTest('player diagonal walking') +registerGlobalTest('findPath') +registerGlobalTest('findRandomPointAroundCircle') +registerGlobalTest('castNavigationRay') +registerGlobalTest('findNearestNavMeshPosition') +registerGlobalTest('player memory limit') +registerGlobalTest('player weapon attack', 'player with equipped weapon on attack should damage health of other actors') + +return { + engineHandlers = { + onFrame = testing.makeUpdateMenu(), + }, + eventHandlers = testing.menuEventHandlers, +} diff --git a/scripts/data/integration_tests/test_lua_api/openmw.cfg b/scripts/data/integration_tests/test_lua_api/openmw.cfg index 68e50644a0..efd1cf331c 100644 --- a/scripts/data/integration_tests/test_lua_api/openmw.cfg +++ b/scripts/data/integration_tests/test_lua_api/openmw.cfg @@ -1,4 +1,4 @@ -content=test.omwscripts +content=test_lua_api.omwscripts # Needed to test `core.getGMST` fallback=Water_RippleFrameCount,4 diff --git a/scripts/data/integration_tests/test_lua_api/player.lua b/scripts/data/integration_tests/test_lua_api/player.lua index 9620866e6e..74769da8e0 100644 --- a/scripts/data/integration_tests/test_lua_api/player.lua +++ b/scripts/data/integration_tests/test_lua_api/player.lua @@ -40,7 +40,7 @@ local function rotateByPitch(object, target) rotate(object, target, nil) end -testing.registerLocalTest('playerYawRotation', +testing.registerLocalTest('player yaw rotation', function() local initialAlphaXZ, initialGammaXZ = self.rotation:getAnglesXZ() local initialAlphaZYX, initialBetaZYX, initialGammaZYX = self.rotation:getAnglesZYX() @@ -60,7 +60,7 @@ testing.registerLocalTest('playerYawRotation', testing.expectEqualWithDelta(gamma2, initialGammaZYX, 0.05, 'Gamma rotation in ZYX convention should not change') end) -testing.registerLocalTest('playerPitchRotation', +testing.registerLocalTest('player pitch rotation', function() local initialAlphaXZ, initialGammaXZ = self.rotation:getAnglesXZ() local initialAlphaZYX, initialBetaZYX, initialGammaZYX = self.rotation:getAnglesZYX() @@ -80,7 +80,7 @@ testing.registerLocalTest('playerPitchRotation', testing.expectEqualWithDelta(gamma2, targetPitch, 0.05, 'Incorrect gamma rotation in ZYX convention') end) -testing.registerLocalTest('playerPitchAndYawRotation', +testing.registerLocalTest('player pitch and yaw rotation', function() local targetPitch = math.rad(-30) local targetYaw = math.rad(-60) @@ -99,7 +99,7 @@ testing.registerLocalTest('playerPitchAndYawRotation', testing.expectEqualWithDelta(gamma2, math.rad(-16), 0.05, 'Incorrect gamma rotation in ZYX convention') end) -testing.registerLocalTest('playerRotation', +testing.registerLocalTest('player rotation', function() local rotation = math.sqrt(2) local endTime = core.getSimulationTime() + 3 @@ -123,7 +123,7 @@ testing.registerLocalTest('playerRotation', end end) -testing.registerLocalTest('playerForwardRunning', +testing.registerLocalTest('player forward running', function() local startPos = self.position local endTime = core.getSimulationTime() + 1 @@ -141,7 +141,7 @@ testing.registerLocalTest('playerForwardRunning', testing.expectEqualWithDelta(direction.y, 1, 0.1, 'Run forward, Y coord') end) -testing.registerLocalTest('playerDiagonalWalking', +testing.registerLocalTest('player diagonal walking', function() local startPos = self.position local endTime = core.getSimulationTime() + 1 @@ -220,7 +220,7 @@ testing.registerLocalTest('findNearestNavMeshPosition', 'Navigation mesh position ' .. testing.formatActualExpected(result, expected)) end) -testing.registerLocalTest('playerMemoryLimit', +testing.registerLocalTest('player memory limit', function() local ok, err = pcall(function() local str = 'a' @@ -232,7 +232,7 @@ testing.registerLocalTest('playerMemoryLimit', testing.expectEqual(err, 'not enough memory') end) -testing.registerLocalTest('playerWeaponAttack', +testing.registerLocalTest('player weapon attack', function() camera.setMode(camera.MODE.ThirdPerson) diff --git a/scripts/data/integration_tests/test_lua_api/test.omwscripts b/scripts/data/integration_tests/test_lua_api/test.omwscripts deleted file mode 100644 index 80507392f7..0000000000 --- a/scripts/data/integration_tests/test_lua_api/test.omwscripts +++ /dev/null @@ -1,2 +0,0 @@ -GLOBAL: test.lua -PLAYER: player.lua diff --git a/scripts/data/integration_tests/test_lua_api/test_lua_api.omwscripts b/scripts/data/integration_tests/test_lua_api/test_lua_api.omwscripts new file mode 100644 index 0000000000..4ce925e61d --- /dev/null +++ b/scripts/data/integration_tests/test_lua_api/test_lua_api.omwscripts @@ -0,0 +1,3 @@ +MENU: menu.lua +GLOBAL: global.lua +PLAYER: player.lua diff --git a/scripts/data/integration_tests/testing_util/testing_util.lua b/scripts/data/integration_tests/testing_util/testing_util.lua index f5de16fb2d..5a69cb89ed 100644 --- a/scripts/data/integration_tests/testing_util/testing_util.lua +++ b/scripts/data/integration_tests/testing_util/testing_util.lua @@ -3,29 +3,21 @@ local util = require('openmw.util') local M = {} +local menuTestsOrder = {} +local menuTests = {} + local globalTestsOrder = {} local globalTests = {} local globalTestRunner = nil +local currentGlobalTest = nil +local currentGlobalTestError = nil local localTests = {} local localTestRunner = nil local currentLocalTest = nil local currentLocalTestError = nil -function M.makeUpdateGlobal() - local fn = function() - for i, test in ipairs(globalTestsOrder) do - local name, fn = unpack(test) - print('TEST_START', i, name) - local status, err = pcall(fn) - if status then - print('TEST_OK', i, name) - else - print('TEST_FAILED', i, name, err) - end - end - core.quit() - end +local function makeTestCoroutine(fn) local co = coroutine.create(fn) return function() if coroutine.status(co) ~= 'dead' then @@ -34,11 +26,64 @@ function M.makeUpdateGlobal() end end +local function runTests(tests) + for i, test in ipairs(tests) do + local name, fn = unpack(test) + print('TEST_START', i, name) + local status, err = pcall(fn) + if status then + print('TEST_OK', i, name) + else + print('TEST_FAILED', i, name, err) + end + end + core.quit() +end + +function M.makeUpdateMenu() + return makeTestCoroutine(function() + print('Running menu tests...') + runTests(menuTestsOrder) + end) +end + +function M.makeUpdateGlobal() + return makeTestCoroutine(function() + print('Running global tests...') + runTests(globalTestsOrder) + end) +end + +function M.registerMenuTest(name, fn) + menuTests[name] = fn + table.insert(menuTestsOrder, {name, fn}) +end + +function M.runGlobalTest(name) + currentGlobalTest = name + currentGlobalTestError = nil + core.sendGlobalEvent('runGlobalTest', name) + while currentGlobalTest do + coroutine.yield() + end + if currentGlobalTestError then + error(currentGlobalTestError, 2) + end +end + function M.registerGlobalTest(name, fn) globalTests[name] = fn table.insert(globalTestsOrder, {name, fn}) end +function M.updateGlobal() + if globalTestRunner and coroutine.status(globalTestRunner) ~= 'dead' then + coroutine.resume(globalTestRunner) + else + globalTestRunner = nil + end +end + function M.runLocalTest(obj, name) currentLocalTest = name currentLocalTestError = nil @@ -208,11 +253,38 @@ function M.formatActualExpected(actual, expected) return string.format('actual: %s, expected: %s', actual, expected) end +-- used only in menu scripts +M.menuEventHandlers = { + globalTestFinished = function(data) + if data.name ~= currentGlobalTest then + error(string.format('globalTestFinished with incorrect name %s, expected %s', data.name, currentGlobalTest), 2) + end + currentGlobalTest = nil + currentGlobalTestError = data.errMsg + end, +} + -- used only in global scripts M.globalEventHandlers = { + runGlobalTest = function(name) + fn = globalTests[name] + local types = require('openmw.types') + local world = require('openmw.world') + if not fn then + types.Player.sendMenuEvent(world.players[1], 'globalTestFinished', {name=name, errMsg='Global test is not found'}) + return + end + globalTestRunner = coroutine.create(function() + local status, err = pcall(fn) + if status then + err = nil + end + types.Player.sendMenuEvent(world.players[1], 'globalTestFinished', {name=name, errMsg=err}) + end) + end, localTestFinished = function(data) if data.name ~= currentLocalTest then - error(string.format('localTestFinished with incorrect name %s, expected %s', data.name, currentLocalTest)) + error(string.format('localTestFinished with incorrect name %s, expected %s', data.name, currentLocalTest), 2) end currentLocalTest = nil currentLocalTestError = data.errMsg diff --git a/scripts/integration_tests.py b/scripts/integration_tests.py index 850ac30b71..80c97f8b73 100755 --- a/scripts/integration_tests.py +++ b/scripts/integration_tests.py @@ -83,7 +83,7 @@ def run_test(test_name): test_success = True fatal_errors = list() with subprocess.Popen( - [openmw_binary, "--replace=config", "--config", config_dir, "--skip-menu", "--no-grab", "--no-sound"], + [openmw_binary, "--replace=config", "--config", config_dir, "--no-grab"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8", From 2892e19c4346d30a2b5f86ef0c04e7b03d3d8126 Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 23:42:16 +0100 Subject: [PATCH 075/154] Run integration tests with verbose output --- CI/run_integration_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CI/run_integration_tests.sh b/CI/run_integration_tests.sh index e79408926a..6cc3bd55bf 100755 --- a/CI/run_integration_tests.sh +++ b/CI/run_integration_tests.sh @@ -9,7 +9,7 @@ git checkout FETCH_HEAD cd .. xvfb-run --auto-servernum --server-args='-screen 0 640x480x24x60' \ - scripts/integration_tests.py --omw build/install/bin/openmw --workdir integration_tests_output example-suite/ + scripts/integration_tests.py --verbose --omw build/install/bin/openmw --workdir integration_tests_output example-suite/ ls integration_tests_output/*.osg_stats.log | while read v; do scripts/osg_stats.py --stats '.*' --regexp_match < "${v}" From 8cb1838c4afc74b7a878355a03ee71bc9c887ec2 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 9 Mar 2025 00:52:00 +0300 Subject: [PATCH 076/154] Don't require a reference for GetSoundPlaying (#8389) --- apps/openmw/mwscript/soundextensions.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwscript/soundextensions.cpp b/apps/openmw/mwscript/soundextensions.cpp index c248c30520..0f9d85cc38 100644 --- a/apps/openmw/mwscript/soundextensions.cpp +++ b/apps/openmw/mwscript/soundextensions.cpp @@ -162,11 +162,17 @@ namespace MWScript public: void execute(Interpreter::Runtime& runtime) override { - MWWorld::Ptr ptr = R()(runtime); + MWWorld::Ptr ptr = R()(runtime, false); int index = runtime[0].mInteger; runtime.pop(); + if (ptr.isEmpty()) + { + runtime.push(0); + return; + } + bool ret = MWBase::Environment::get().getSoundManager()->getSoundPlaying( ptr, ESM::RefId::stringRefId(runtime.getStringLiteral(index))); From 51d73e37df4e225f9df01afc0f72290880645a21 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 9 Mar 2025 17:07:06 +0100 Subject: [PATCH 077/154] Fix msvc warnings components\lua\configuration.cpp(133): warning C4267: 'argument': conversion from 'size_t' to 'int', possible loss of data components\esm3\effectlist.cpp(35): warning C4267: '=': conversion from 'size_t' to 'uint32_t', possible loss of data components_tests\misc\testmathutil.cpp(54): warning C4305: 'argument': truncation from 'const double' to 'osg::Vec3f::value_type' components_tests\misc\testmathutil.cpp(62): warning C4305: 'argument': truncation from 'const double' to 'osg::Vec3f::value_type' components_tests\misc\testmathutil.cpp(131): warning C4305: 'argument': truncation from 'const double' to 'osg::Vec3f::value_type' components_tests\misc\testmathutil.cpp(135): warning C4305: 'argument': truncation from 'const double' to 'osg::Vec3f::value_type' components_tests\misc\testmathutil.cpp(135): warning C4305: 'argument': truncation from 'const double' to 'osg::Vec3f::value_type' components_tests\misc\testmathutil.cpp(139): warning C4305: 'argument': truncation from 'const double' to 'osg::Vec3f::value_type' --- apps/components_tests/misc/testmathutil.cpp | 10 +++++----- components/esm3/effectlist.cpp | 2 +- components/lua/l10n.cpp | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/components_tests/misc/testmathutil.cpp b/apps/components_tests/misc/testmathutil.cpp index c4b545c2f4..099bd6db9e 100644 --- a/apps/components_tests/misc/testmathutil.cpp +++ b/apps/components_tests/misc/testmathutil.cpp @@ -51,7 +51,7 @@ namespace Misc const std::pair eulerAnglesXZQuat[] = { { osg::Quat(1, 0, 0, 0), - osg::Vec3f(0, 0, osg::PI), + osg::Vec3f(0, 0, osg::PIf), }, { osg::Quat(0, 1, 0, 0), @@ -59,7 +59,7 @@ namespace Misc }, { osg::Quat(0, 0, 1, 0), - osg::Vec3f(0, 0, osg::PI), + osg::Vec3f(0, 0, osg::PIf), }, { osg::Quat(0, 0, 0, 1), @@ -128,15 +128,15 @@ namespace Misc const std::pair eulerAnglesZYXQuat[] = { { osg::Quat(1, 0, 0, 0), - osg::Vec3f(osg::PI, 0, 0), + osg::Vec3f(osg::PIf, 0, 0), }, { osg::Quat(0, 1, 0, 0), - osg::Vec3f(osg::PI, 0, osg::PI), + osg::Vec3f(osg::PIf, 0, osg::PIf), }, { osg::Quat(0, 0, 1, 0), - osg::Vec3f(0, 0, osg::PI), + osg::Vec3f(0, 0, osg::PIf), }, { osg::Quat(0, 0, 0, 1), diff --git a/components/esm3/effectlist.cpp b/components/esm3/effectlist.cpp index a71eccfb84..5a1fa91f28 100644 --- a/components/esm3/effectlist.cpp +++ b/components/esm3/effectlist.cpp @@ -32,7 +32,7 @@ namespace ESM void EffectList::updateIndexes() { for (size_t i = 0; i < mList.size(); i++) - mList[i].mIndex = i; + mList[i].mIndex = static_cast(i); } void EffectList::add(ESMReader& esm) diff --git a/components/lua/l10n.cpp b/components/lua/l10n.cpp index 542c81009a..15177bea65 100644 --- a/components/lua/l10n.cpp +++ b/components/lua/l10n.cpp @@ -32,7 +32,8 @@ namespace // Argument names const auto str = LuaUtil::cast(key); - argNames.push_back(icu::UnicodeString::fromUTF8(icu::StringPiece(str.data(), str.size()))); + argNames.push_back( + icu::UnicodeString::fromUTF8(icu::StringPiece(str.data(), static_cast(str.size())))); } } } From c6919171723338f2fe4ad930f8caa61f8de93daf Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 10 Mar 2025 16:37:13 +0100 Subject: [PATCH 078/154] Distinguish between I.Controls and I.GamepadControls --- docs/source/reference/lua-scripting/tables/interfaces.rst | 2 +- files/data/scripts/omw/input/gamepadcontrols.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/reference/lua-scripting/tables/interfaces.rst b/docs/source/reference/lua-scripting/tables/interfaces.rst index 3a074c1ad7..944a483dda 100644 --- a/docs/source/reference/lua-scripting/tables/interfaces.rst +++ b/docs/source/reference/lua-scripting/tables/interfaces.rst @@ -21,7 +21,7 @@ - by player scripts - | Allows to alter behavior of the built-in script | that handles player controls. - * - :ref:`Controls ` + * - :ref:`GamepadControls ` - by player scripts - | Allows to alter behavior of the built-in script | that handles player gamepad controls. diff --git a/files/data/scripts/omw/input/gamepadcontrols.lua b/files/data/scripts/omw/input/gamepadcontrols.lua index b55b806b4a..1f944fc708 100644 --- a/files/data/scripts/omw/input/gamepadcontrols.lua +++ b/files/data/scripts/omw/input/gamepadcontrols.lua @@ -6,7 +6,7 @@ return { --- -- Gamepad control interface -- @module GamepadControls - + -- @usage require('openmw.interfaces').GamepadControls interface = { --- Interface version -- @field [parent=#GamepadControls] #number version From 5354a5f78650f9b06e8118ecbe46a2b84c30263e Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Tue, 11 Mar 2025 00:30:56 +0300 Subject: [PATCH 079/154] Don't use attack strength as "hit ready" flag This unbreaks follow animations' strength dependence --- apps/openmw/mwmechanics/character.cpp | 16 +++++++++------- apps/openmw/mwmechanics/character.hpp | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index c22308bce4..d13f3c9926 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1104,10 +1104,10 @@ namespace MWMechanics attackType = ESM::Weapon::AT_Thrust; // We want to avoid hit keys that come out of nowhere (e.g. in the follow animation) // and processing multiple hit keys for a single attack - if (mAttackStrength != -1.f) + if (mReadyToHit) { charClass.hit(mPtr, mAttackStrength, attackType, mAttackVictim, mAttackHitPos, mAttackSuccess); - mAttackStrength = -1.f; + mReadyToHit = false; } } else if (isRandomAttackAnimation(groupname) && action == "start") @@ -1153,10 +1153,10 @@ namespace MWMechanics else if (action == "shoot release") { // See notes for melee release above - if (mAttackStrength != -1.f) + if (mReadyToHit) { mAnimation->releaseArrow(mAttackStrength); - mAttackStrength = -1.f; + mReadyToHit = false; } } else if (action == "shoot follow attach") @@ -1246,7 +1246,7 @@ namespace MWMechanics void CharacterController::prepareHit() { - if (mAttackStrength != -1.f) + if (mReadyToHit) return; auto& prng = MWBase::Environment::get().getWorld()->getPrng(); @@ -1261,6 +1261,8 @@ namespace MWMechanics mAttackStrength = 0.f; playSwishSound(); } + + mReadyToHit = true; } bool CharacterController::updateWeaponState() @@ -1520,6 +1522,7 @@ namespace MWMechanics && (mHitState == CharState_None || mHitState == CharState_Block)) { mAttackStrength = -1.f; + mReadyToHit = false; // Randomize attacks for non-bipedal creatures if (!cls.isBipedal(mPtr) @@ -1806,8 +1809,7 @@ namespace MWMechanics stop = strength + ' ' + stop; } - // Reset attack strength to make extra sure hits that come out of nowhere aren't processed - mAttackStrength = -1.f; + mReadyToHit = false; if (animPlaying) mAnimation->disable(mCurrentWeapon); diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index f043419a81..d5c642c883 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -172,6 +172,7 @@ namespace MWMechanics std::string mCurrentWeapon; float mAttackStrength{ -1.f }; + bool mReadyToHit{ false }; MWWorld::Ptr mAttackVictim; osg::Vec3f mAttackHitPos; bool mAttackSuccess{ false }; From 9f85e5193486b498a3d396882c06523bf0bde866 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Tue, 11 Mar 2025 17:37:18 +0300 Subject: [PATCH 080/154] Only log ripples pipeline once --- apps/openmw/mwrender/ripples.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwrender/ripples.cpp b/apps/openmw/mwrender/ripples.cpp index bb8248217a..309fc7b305 100644 --- a/apps/openmw/mwrender/ripples.cpp +++ b/apps/openmw/mwrender/ripples.cpp @@ -49,10 +49,17 @@ namespace MWRender && exts.glslLanguageVersion >= minimumGLVersionRequiredForCompute; #endif - if (mUseCompute) - Log(Debug::Info) << "Initialized compute shader pipeline for water ripples"; - else - Log(Debug::Info) << "Initialized fallback fragment shader pipeline for water ripples"; + static bool pipelineLogged = false; + + if (!pipelineLogged) + { + if (mUseCompute) + Log(Debug::Info) << "Initialized compute shader pipeline for water ripples"; + else + Log(Debug::Info) << "Initialized fallback fragment shader pipeline for water ripples"; + + pipelineLogged = true; + } for (size_t i = 0; i < mState.size(); ++i) { From e5ad1cd2144cfedf0418a30a125bf057734150f0 Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 21 Feb 2025 19:14:18 +0100 Subject: [PATCH 081/154] Do not use no longer supported std::char_traits /usr/bin/../include/c++/v1/string_view:300:42: error: implicit instantiation of undefined template 'std::char_traits' 300 | static_assert(is_same<_CharT, typename traits_type::char_type>::value, | ^ /home/elsid/dev/openmw/components/to_utf8/to_utf8.cpp:55:41: note: in instantiation of template class 'std::basic_string_view' requested here 55 | std::basic_string_view getTranslationArray(FromType sourceEncoding) | ^ /usr/bin/../include/c++/v1/__fwd/string.h:23:29: note: template is declared here 23 | struct _LIBCPP_TEMPLATE_VIS char_traits; | ^ std::char_traits support for non char types was removed from libc++19: https://reviews.llvm.org/D157058. --- components/to_utf8/to_utf8.cpp | 2 +- components/to_utf8/to_utf8.hpp | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/components/to_utf8/to_utf8.cpp b/components/to_utf8/to_utf8.cpp index 15fb8b26c0..7e34147b1c 100644 --- a/components/to_utf8/to_utf8.cpp +++ b/components/to_utf8/to_utf8.cpp @@ -52,7 +52,7 @@ namespace return std::find_if(input.begin(), input.end(), [](unsigned char v) { return v == 0 || v >= 128; }); } - std::basic_string_view getTranslationArray(FromType sourceEncoding) + std::span getTranslationArray(FromType sourceEncoding) { switch (sourceEncoding) { diff --git a/components/to_utf8/to_utf8.hpp b/components/to_utf8/to_utf8.hpp index 80af6586c9..0dde0fced6 100644 --- a/components/to_utf8/to_utf8.hpp +++ b/components/to_utf8/to_utf8.hpp @@ -2,6 +2,7 @@ #define COMPONENTS_TOUTF8_H #include +#include #include #include #include @@ -50,7 +51,7 @@ namespace ToUTF8 inline void copyFromArrayLegacyEnc( std::string_view::iterator& chp, std::string_view::iterator end, char*& out) const; - const std::basic_string_view mTranslationArray; + const std::span mTranslationArray; }; class Utf8Encoder From ced142da92031be583557b52ac1f9096e2cb9027 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Tue, 11 Mar 2025 20:34:04 +0300 Subject: [PATCH 082/154] Lift upstream sol::optional::emplace Clang 19 build fix --- extern/sol3/README.md | 2 ++ extern/sol3/sol/optional_implementation.hpp | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/extern/sol3/README.md b/extern/sol3/README.md index 1674dcf599..202b2ca08b 100644 --- a/extern/sol3/README.md +++ b/extern/sol3/README.md @@ -1,3 +1,5 @@ The code in this directory is copied from https://github.com/ThePhD/sol2.git (64096348465b980e2f1d0e5ba9cbeea8782e8f27) +Additional changes include cherry-picking upstream commit d805d027e0a0a7222e936926139f06e23828ce9f to fix compilation under Clang 19. + License: MIT diff --git a/extern/sol3/sol/optional_implementation.hpp b/extern/sol3/sol/optional_implementation.hpp index b7673b17b1..a35df4ec87 100644 --- a/extern/sol3/sol/optional_implementation.hpp +++ b/extern/sol3/sol/optional_implementation.hpp @@ -2191,7 +2191,8 @@ namespace sol { static_assert(std::is_constructible::value, "T must be constructible with Args"); *this = nullopt; - this->construct(std::forward(args)...); + new (static_cast(this)) optional(std::in_place, std::forward(args)...); + return **this; } /// Swaps this optional with the other. From e5f6b77c29c7bd40cf6bae4405a98fefed31eb1c Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 15 Mar 2025 13:04:18 +0100 Subject: [PATCH 083/154] Skip SLSD, SCVR, SCRV subrecords in QUST record Present in: Fallout 3 GOTY English/Data/Anchorage.esm Fallout 3 GOTY English/Data/BrokenSteel.esm Fallout 3 GOTY English/Data/PointLookout.esm Fallout 3 GOTY English/Data/ThePitt.esm Fallout 3 GOTY English/Data/Zeta.esm --- components/esm4/loadqust.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/esm4/loadqust.cpp b/components/esm4/loadqust.cpp index 27c23d92f1..bdbc94e07a 100644 --- a/components/esm4/loadqust.cpp +++ b/components/esm4/loadqust.cpp @@ -114,6 +114,9 @@ void ESM4::Quest::load(ESM4::Reader& reader) case ESM::fourCC("NNAM"): // FO3 case ESM::fourCC("QOBJ"): // FO3 case ESM::fourCC("NAM0"): // FO3 + case ESM::fourCC("SLSD"): // FO3 + case ESM::fourCC("SCVR"): // FO3 + case ESM::fourCC("SCRV"): // FO3 case ESM::fourCC("ANAM"): // TES5 case ESM::fourCC("DNAM"): // TES5 case ESM::fourCC("ENAM"): // TES5 From e4ae0c9a95e47a6e77d2fbe192163dd34bd7aa60 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 16 Mar 2025 09:06:01 +0300 Subject: [PATCH 084/154] Don't assume there is a GUI mode in exitCurrentGuiMode (#8380) --- apps/openmw/mwgui/windowmanagerimp.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index e51350d19f..2d14c25cfc 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -729,6 +729,9 @@ namespace MWGui return; } + if (mGuiModes.empty()) + return; + GuiModeState& state = mGuiModeStates[mGuiModes.back()]; for (const auto& window : state.mWindows) { From b5a2a4e52d5251ad3875d76eac2fcad3c1977c69 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Mon, 17 Mar 2025 22:03:38 +0300 Subject: [PATCH 085/154] Render no-break space in books, don't consider narrow NBSP breaking --- apps/openmw/mwgui/bookpage.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwgui/bookpage.cpp b/apps/openmw/mwgui/bookpage.cpp index 1966442513..2984f035a4 100644 --- a/apps/openmw/mwgui/bookpage.cpp +++ b/apps/openmw/mwgui/bookpage.cpp @@ -1343,12 +1343,13 @@ namespace MWGui return codePoint == '\r'; } + // Normal no-break space (0x00A0) is ignored here + // because Morrowind compatibility requires us to render its glyph static bool ucsSpace(int codePoint) { switch (codePoint) { case 0x0020: // SPACE - case 0x00A0: // NO-BREAK SPACE case 0x1680: // OGHAM SPACE MARK case 0x180E: // MONGOLIAN VOWEL SEPARATOR case 0x2000: // EN QUAD @@ -1373,12 +1374,13 @@ namespace MWGui } } + // No-break spaces (0x00A0, 0x202F, 0xFEFF - normal, narrow, zero width) + // are ignored here for obvious reasons static bool ucsBreakingSpace(int codePoint) { switch (codePoint) { case 0x0020: // SPACE - // case 0x00A0: // NO-BREAK SPACE case 0x1680: // OGHAM SPACE MARK case 0x180E: // MONGOLIAN VOWEL SEPARATOR case 0x2000: // EN QUAD @@ -1393,10 +1395,8 @@ namespace MWGui case 0x2009: // THIN SPACE case 0x200A: // HAIR SPACE case 0x200B: // ZERO WIDTH SPACE - case 0x202F: // NARROW NO-BREAK SPACE case 0x205F: // MEDIUM MATHEMATICAL SPACE case 0x3000: // IDEOGRAPHIC SPACE - // case 0xFEFF: // ZERO WIDTH NO-BREAK SPACE return true; default: return false; From 8d0dcb774f60321b881063be5c4fe8f5d6008eb1 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Tue, 18 Mar 2025 00:34:14 +0300 Subject: [PATCH 086/154] Add no-break space to MysticCards --- files/data/fonts/MysticCards.omwfont | 2 +- files/data/fonts/MysticCards.ttf | Bin 33108 -> 33140 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/files/data/fonts/MysticCards.omwfont b/files/data/fonts/MysticCards.omwfont index 3458614ddc..de4ffaff39 100644 --- a/files/data/fonts/MysticCards.omwfont +++ b/files/data/fonts/MysticCards.omwfont @@ -8,7 +8,7 @@ - + diff --git a/files/data/fonts/MysticCards.ttf b/files/data/fonts/MysticCards.ttf index 1b93f5aadc264fdc1f5c3f634d702e8f6a06ac85..c98597d74974209c923ce3f830a1ea8bcbc36672 100644 GIT binary patch delta 3481 zcmcc8#Pp?!sh)w6fq{XKp@D&!A;HZp#COW`G(!f4#s~%m1{rr3S2qTA#-$7ljRp)1 z3~K(t`bN>lOePErj2R3J3<=4(i3K%Z=A|((upePyV0x5XR-(Y5#MHsSz_o&bfq^AG zvAE#>e+Fg-hDHqr1_qAwoXWIYQ{`D07u$C$_< z$(YC>%4or$!Q{bUz;J^@htT>7?{C!$S|-l>;bumL4g5e1201z!$O7? z3_BR>m`*U=W?^LUX9<*>ry!yrtDyG(KLY~;FM|TZ0)`a~YZ>-49cQ}1!pI^3Rw1Au zp`h~r|Nn3QpZq`ke=h^W|F!>885sWR|5f~}@aW3JWCn(Z5nwwR7#Q3b7#Ji#{$S7o zlMq4;Or zLoP!BLleV)Mh3<*#&X6g#%i!5moP9etYK80T*x7An$J+qkjGHLP|8rnP|Z-oP|481 z(8$2RP|wiL(818ga1U%;I>eI<*$fPf`iwf0=W(5{|04QNOh8OROi9c^tWIo_*cq{F zVo$_AiTxAj5*HI!5??01N&JBLIq_TK&&0n-FiG%9NJ*$k7)dxt_(?=bq)8M>R7sqW zxF+#L;)BE=NiInV$q305$pXnb$sWlWl1rqdq|~I0r0k@;q{5_9q>7{(q z44D#{2ALk288QoG*2wITIU;jG=8nt@nJ=<&vRSfavQ4rRWN*kmk^Lb1M~*{IL{3Rg zPtHotOD;?`oDX39>=e#%kGY05>)b;@1J)07t} zZ&E&_d`|h6@{4-qZz?P*LMn19Ix1EwZYn`4aVk4hj;I__d8W#vDxs>N+M?Q{dPPk^ zO+)Q~x|+J4x|O<<`U3S8>KoK|XmDu=X-H`(X#{AT)3~N7rNvlBXgtme9 z6zxAcEIKYa9XeNZg>gRHCx_XB*j-@uKHFrFxScm+>f}7T`;1wW{p^i( zQyKm-W(lz|Sb~gW(EIgJ(EEmPVG0|DQqm|9{4tOtk`P3>lj*IdFoUamP`UF>5lLlRjhWWHTq_*n40@EEyOW zrJ1G(q%mZr*H{-@_zAH=^3(tS3~CGvjG9b^0%;6R!l4>m4h%?gLJSOyl1wfFY7D8O z`Y>?@W(E%i2Bw8f9h0{?t!K=doaXG$m^yizvnpfSdGfw&+Bf#bciADzH{|$^c z8T$p)Kp`r?&Zy6@>3`b){|t)%&olKgPXNb<4&zD(24i6%HU@RD7i1Y27$-9=0m)5% z=h`W$!_dN*{r^9MFarbQdZvj2Y7Fk6l*Fhrd4iibqt4_NZkmj>lP|fcd!{pNV%#Ob z#-Ir{N1cIzaW<0`$aA{s0&EPP&^Xokznp0n^9iu$>KIGV)YpMLH`&SEp7G-3Cilyn zrHr!~7lS-DInF~|Qimavag`9v25|-k#?4HV1=JXICii(9megZd#ONcy#-I(h-{OA` z;}@nSVDqv(-6gXbmi(KCCOb_)pCNnlDbH-i^vPyk=7RSU82O4%-8wf$F!08pny6< z*5o=Lamg%(2u3{tHiiJGtSZw+rh5YF4B3;{`G_-SZ$9Qz%~+qsc#sj4AdSF^WEdEj z1etb%j9_@j5Fy0I5Cj(Y`R~Kr&U6W+nb8L91gQG|K1_m)8$jX=0$_2N`mfCG%#8x- zj5-W&8B>MW7@+FG=J$f)z_1Wx`sM?ErHqn#3|AP71lSmi!Inw=|HgQOsTmv{~pY?5Vuaw3lf*i z{@?R&29oSgW?Sa@AeT*^7bGsJ!xasY1M6k~e}ZWV^D=O1J{Kg-m^JxTkhf$uLkie$ z=3q5?|NR&X7-xd~JlQW;+*5}kk#U|78v{g^m4ShAAyc7{8bdfF-9HNj>tgzE#B`tO zBS;rR9N0Y;U~z;0I*e_M*FfTv*9C)e$gyC3$#jM-|Bnfpk!=B$ zn3Kx)<3=IsXARZ$JgP8yuLnPS6rvFQso-;iLr`fIlL7@k8 zM>FFKMo`G=PqqtfmrP|i$7l^Nx26AYVrpcb2oBkMp`eic73$5Xv)L_7o{?3GQJvx7 zQ>TLcOUChSlySXT7w*cde&3eU*O#B>74$xL18v{2} z;$*v0SAGE|Jy83Ug_D7eL5#_B^Q2OHCN74s)S}|d{5%DQ$&TgXlfRbpb2EX86$Xa? zD;Ojv7jlR&G1gC3t2oBN$jq>qp^uSe@~4Vkdsap^Ms`LHMova9hQAE|7`YjF7L((U8%I(U{SM(Uj4Q(VWqO(UQ@M(VEeQ;Ss}QMq5TZMtepFMn^^`MrTGBMps5R zMt4RJMo&gBMsJ1<3>z7J7=0Q282uRo7y}uD7=sx@7(*F;GyGwA!tj(aj4_-sf-#aY ziZPlohB1~gjxnAwfiaPBa%`2L1YNX&lw&v7BJjqcrdxGs!N4& z8^b1s%?#TZwli#D*vhz_aRRFS*!XXt2YA!<1VanC>N)l_)SMF?BF7a4le9U|>m4 zEH3!}pMjZyp^<}ufq^4Er!wu9EZ+wP2HqtM4CxLTsfqO|A}qHzFfcH@VPIe|%g9Jg zWPir0#lXO*!oa|wl95|dk#A)MvhV~01M82R{N%(349EEx7`T=&FfjegO{^$jJj*D? zz`*Fiz`&r8mzbOC_xAWV28NO+3=GV21^LA#eK+QnGB8xIFfed3gFVa0z_70{DLkIv z<|~7|5DNncToej>4x;aOIxL(lz-Z212U5ydx7m-8jcIZOvpnl|M%VxMCO0sPY@WdE z&QgDc0b-a80}I0r1_lN#1_cHd1}+9(hB}6Y3@aGcGHhhn!mxv}j_Cx`4W`>Hj4b{v zfpYT{L=+?yWEIr@|7T!e;AK#Ns$CCK%dns6I8?0wSgnA9go2EM%K!iWzx{vm|Iq(E z|9AY~{(tNLP5;+8{Y&{5`Y-se-d}~k@{cY*OnMmqF!o_M$k8Bg5Q8_*;oxShFJLHS zC}t>SsAXtk=wRq&=waw%n847_Fp*(0!&HVT4AU5yn!$w@7ZC+%CCWa?j*G$^DaeldlhwkCRW6FOsj5?~fSxi|;Sx?zY*-be}IZioCxlFl9xlehP z@+#$B%Ey#1DL+ttSFij>g-bh&(8sXF@RZ>XBORkAqkTrljJb?ejCG97j2(<4jFXJBj7yAbj9ZK+7%yO8*t~}K zw)W=fmNty++Zkpt+A}6jzGbzXF>`X0wKrqR*cb@Q*P|h>gLLf#Lsu2EG4(nLL?01kxEY z8EqKj92l(BgxDBt!E#XyjFXSq?*>`w;4hhyq#?w{;0j9R|Nk>6|NqZ;lc`ofjUj#W zD+f-HGrl-#GGj=j;#ix3el^>f|rZ z7a21rA9K;yOer%LVq=H|>G=Pj!ScU6<71{q0d6~MBPNP*iVz!vF4#my1_nk;CToz7-ed@{K}z2L{}~|aArWH(ju?n~ z4h9CsOi&tS)cv3dQm?=OHb?e<4dWrE2Vh?X{!bQwnQzVYe-5Jrled5xquypdZ+Aw? zEQSgHwg|8>=!5ks{P$t}&9qKHogr&-o{zX@6j)T}e;?CE=7XR_$8dr%L4b|H04%N# zO4iK%Ao0oTe8eTQ7?S^WBV_*@F#Ka$2{LQ*J)dgE`b@@yjGz>21lBCWz`!KPv=gM6 z;T=PS5F0}fSls8o4|6-yC6G2on}3@G*chPd|NAfrGHw8gGYEjiVd}p!w=*|_B8uTH zW2z8bJ=pwS0X0T#!$Oehn{W7)GBWB;_6rcV*HzXRU}Fe|rWfn~UzsJC?}D7d$ii3y zrZ+Lx2(U32gPkPx{~O~Crbz;744E~CAlc1J0yZ*AW-?ARvi}*6GL?heI{93XxNIh)FyjLU z2K@~}Yz*dL)Abk_7z>!X!5N1s*qbqHvR|;cWY+%!;K&OHt6~3tf@ul!E>K8Jt_zkG z)_xWW7H9fz#B`tOBiQbBU|skAO%h;Zh=9ueVY&|qzsdK4^%>J9^M!~rrcG80ac4}M zoEIW4ndX%yfGB}X85o$JGcN&UuF3O4#3gmL3^8P51=JbRC!Y%uuTPKHLMpu=de4Hw zh~XDw4v1!4z?dTdE2`E0|7U!{G#w<)(7<2{q8T|D%pm6f|Ic9hzl7;I(_?V5-U`mF zkRo2~e-q;i#`$3Jga0$(>a`}PgjO=9OuiQi3R6SIZ4L~EH-y+2?7%)Z`G1@79a9fD zOohU{8MQaZg~>CrN;0Z59Gct|?$4RRaDp*gh>g*7^Q~}e7DnyOVll;Rj6R#Yl6DI) zPT%ZS%*e#g$>aoWJF+owGo?%pD|O`;V$uUOa#=VT*cil^d^WEtwP)g1U|oqd21kqa>pgqco!o!w-g^ zjIxY!jPi^MjEam(jLM8EjH--kjOvUUjGByEjM|Jk48ItzGwM$MQ7Oa5@QC3tqs?Tg zDj6k)4GbF@y%~KNeHr~2{TTxo0~v!DgBe2@elz@Gc*5|MF?4c7m6im%^uYc zSv?v5|7TzVhawLH1A`cY9GJ(*Sj)i30P1CcLJHK`0%1@ff$94KHFrQ9 Date: Tue, 18 Mar 2025 00:46:52 +0300 Subject: [PATCH 087/154] Change substitute character in Mystic Cards from question mark to underscore --- files/data/fonts/MysticCards.omwfont | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/data/fonts/MysticCards.omwfont b/files/data/fonts/MysticCards.omwfont index de4ffaff39..b0dccb1445 100644 --- a/files/data/fonts/MysticCards.omwfont +++ b/files/data/fonts/MysticCards.omwfont @@ -2,7 +2,7 @@ - + From cd3980eca4e4d7ba3f68f2c4afe68ec0dc74beb0 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Tue, 18 Mar 2025 10:29:29 +0300 Subject: [PATCH 088/154] Make figure space non-breaking --- apps/openmw/mwgui/bookpage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwgui/bookpage.cpp b/apps/openmw/mwgui/bookpage.cpp index 2984f035a4..47e85b1f4b 100644 --- a/apps/openmw/mwgui/bookpage.cpp +++ b/apps/openmw/mwgui/bookpage.cpp @@ -1376,6 +1376,7 @@ namespace MWGui // No-break spaces (0x00A0, 0x202F, 0xFEFF - normal, narrow, zero width) // are ignored here for obvious reasons + // Figure space (0x2007) is not a breaking space either static bool ucsBreakingSpace(int codePoint) { switch (codePoint) @@ -1390,7 +1391,6 @@ namespace MWGui case 0x2004: // THREE-PER-EM SPACE case 0x2005: // FOUR-PER-EM SPACE case 0x2006: // SIX-PER-EM SPACE - case 0x2007: // FIGURE SPACE case 0x2008: // PUNCTUATION SPACE case 0x2009: // THIN SPACE case 0x200A: // HAIR SPACE From e5e21eef20266590e891f620dc3df1acdb6669d3 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 20 Mar 2025 20:08:31 +0100 Subject: [PATCH 089/154] Fix minor documentation errors --- files/lua_api/openmw/self.lua | 2 +- files/lua_api/openmw/types.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/files/lua_api/openmw/self.lua b/files/lua_api/openmw/self.lua index 4da5fa3ee9..3a45bd68d8 100644 --- a/files/lua_api/openmw/self.lua +++ b/files/lua_api/openmw/self.lua @@ -45,7 +45,7 @@ -- @field [parent=#ActorControls] #boolean run true - run, false - walk -- @field [parent=#ActorControls] #boolean sneak If true - sneak -- @field [parent=#ActorControls] #boolean jump If true - initiate a jump --- @field [parent=#ActorControls] #ATTACK_TYPE use Activates the readied weapon/spell according to a provided value. For weapons, keeping this value modified will charge the attack until set to @{#ATTACK_TYPE.NoAttack}. If an @#ATTACK_TYPE} not appropriate for a currently equipped weapon provided - an appropriate @{#ATTACK_TYPE} will be used instead. +-- @field [parent=#ActorControls] #ATTACK_TYPE use Activates the readied weapon/spell according to a provided value. For weapons, keeping this value modified will charge the attack until set to @{#ATTACK_TYPE.NoAttack}. If an @{#ATTACK_TYPE} not appropriate for a currently equipped weapon provided - an appropriate @{#ATTACK_TYPE} will be used instead. --- -- Enables or disables standard AI (enabled by default). diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index e1b814f8b3..0012a51c41 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -1123,7 +1123,7 @@ -- @field #string id The record ID of the NPC -- @field #string name -- @field #string race --- @field #string class Name of the NPC's class (e. g. Acrobat) +-- @field #string class ID of the NPC's class (e.g. acrobat) -- @field #string model Path to the model associated with this NPC, used for animations. -- @field #string mwscript MWScript on this NPC (can be nil) -- @field #string hair Path to the hair body part model From 51258662b5152dc22e1ee262443b0ec7bf561ae6 Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 21 Mar 2025 14:32:33 +0100 Subject: [PATCH 090/154] Support max log level for Recast via env variable Do not write to log if log message level is greater than one speficied in the OPENMW_RECAST_MAX_LOG_LEVEL env variable. Use Error by default. --- apps/navmeshtool/main.cpp | 3 +- apps/openmw/engine.cpp | 2 +- apps/openmw/engine.hpp | 9 +++-- apps/openmw/main.cpp | 2 ++ apps/openmw/mwworld/worldimp.cpp | 6 ++-- apps/openmw/mwworld/worldimp.hpp | 5 +-- components/debug/debugging.cpp | 38 +++++++++++++------- components/debug/debugging.hpp | 2 ++ components/detournavigator/makenavmesh.cpp | 2 +- components/detournavigator/recastcontext.cpp | 17 +++++---- components/detournavigator/recastcontext.hpp | 5 ++- components/detournavigator/settings.cpp | 7 ++-- components/detournavigator/settings.hpp | 5 ++- 13 files changed, 68 insertions(+), 35 deletions(-) diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp index 27f84104ac..a05babfbe8 100644 --- a/apps/navmeshtool/main.cpp +++ b/apps/navmeshtool/main.cpp @@ -225,7 +225,8 @@ namespace NavMeshTool Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager, &bgsmFileManager, expiryDelay); Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager, expiryDelay); DetourNavigator::RecastGlobalAllocator::init(); - DetourNavigator::Settings navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager(); + DetourNavigator::Settings navigatorSettings + = DetourNavigator::makeSettingsFromSettingsManager(Debug::getRecastMaxLogLevel()); navigatorSettings.mRecast.mSwimHeightScale = EsmLoader::getGameSetting(esmData.mGameSettings, "fSwimHeightScale").getFloat(); diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 2736f339e4..bfdf58ce06 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -847,7 +847,7 @@ void OMW::Engine::prepareEngine() } listener->loadingOff(); - mWorld->init(mViewer, std::move(rootNode), mWorkQueue.get(), *mUnrefQueue); + mWorld->init(mMaxRecastLogLevel, mViewer, std::move(rootNode), mWorkQueue.get(), *mUnrefQueue); mEnvironment.setWorldScene(mWorld->getWorldScene()); mWorld->setupPlayer(); mWorld->setRandomSeed(mRandomSeed); diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 39056af560..69fdd3869c 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -172,6 +173,7 @@ namespace OMW bool mGrab; unsigned int mRandomSeed; + Debug::Level mMaxRecastLogLevel = Debug::Error; Compiler::Extensions mExtensions; std::unique_ptr mScriptContext; @@ -180,6 +182,9 @@ namespace OMW Translation::Storage mTranslationDataStorage; bool mNewGame; + Files::ConfigurationManager& mCfgMgr; + int mGlMaxTextureImageUnits; + // not implemented Engine(const Engine&); Engine& operator=(const Engine&); @@ -256,9 +261,7 @@ namespace OMW void setRandomSeed(unsigned int seed); - private: - Files::ConfigurationManager& mCfgMgr; - int mGlMaxTextureImageUnits; + void setRecastMaxLogLevel(Debug::Level value) { mMaxRecastLogLevel = value; } }; } diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index 5b89bca960..7ed3292b55 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -220,6 +220,8 @@ int runApplication(int argc, char* argv[]) Files::ConfigurationManager cfgMgr; std::unique_ptr engine = std::make_unique(cfgMgr); + engine->setRecastMaxLogLevel(Debug::getRecastMaxLogLevel()); + if (parseOptions(argc, argv, *engine, cfgMgr)) { if (!Misc::checkRequiredOSGPluginsArePresent()) diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 7b2f178343..57d794c535 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -290,14 +290,14 @@ namespace MWWorld mSwimHeightScale = mStore.get().find("fSwimHeightScale")->mValue.getFloat(); } - void World::init(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, SceneUtil::WorkQueue* workQueue, - SceneUtil::UnrefQueue& unrefQueue) + void World::init(Debug::Level maxRecastLogLevel, osgViewer::Viewer* viewer, osg::ref_ptr rootNode, + SceneUtil::WorkQueue* workQueue, SceneUtil::UnrefQueue& unrefQueue) { mPhysics = std::make_unique(mResourceSystem, rootNode); if (Settings::navigator().mEnable) { - auto navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager(); + auto navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager(maxRecastLogLevel); navigatorSettings.mRecast.mSwimHeightScale = mSwimHeightScale; mNavigator = DetourNavigator::makeNavigator(navigatorSettings, mUserDataPath); } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 6f06812e20..983682a98f 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -201,8 +202,8 @@ namespace MWWorld Loading::Listener* listener); // Must be called after `loadData`. - void init(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, SceneUtil::WorkQueue* workQueue, - SceneUtil::UnrefQueue& unrefQueue); + void init(Debug::Level maxRecastLogLevel, osgViewer::Viewer* viewer, osg::ref_ptr rootNode, + SceneUtil::WorkQueue* workQueue, SceneUtil::UnrefQueue& unrefQueue); virtual ~World(); diff --git a/components/debug/debugging.cpp b/components/debug/debugging.cpp index 32aec8f0fc..6006e1abe5 100644 --- a/components/debug/debugging.cpp +++ b/components/debug/debugging.cpp @@ -324,6 +324,22 @@ namespace Debug First mFirst; Second mSecond; }; + + Level toLevel(std::string_view value) + { + if (value == "ERROR") + return Error; + if (value == "WARNING") + return Warning; + if (value == "INFO") + return Info; + if (value == "VERBOSE") + return Verbose; + if (value == "DEBUG") + return Debug; + + return Verbose; + } } #endif @@ -359,23 +375,19 @@ namespace Debug Level getDebugLevel() { if (const char* env = getenv("OPENMW_DEBUG_LEVEL")) - { - const std::string_view value(env); - if (value == "ERROR") - return Error; - if (value == "WARNING") - return Warning; - if (value == "INFO") - return Info; - if (value == "VERBOSE") - return Verbose; - if (value == "DEBUG") - return Debug; - } + return toLevel(env); return Verbose; } + Level getRecastMaxLogLevel() + { + if (const char* env = getenv("OPENMW_RECAST_MAX_LOG_LEVEL")) + return toLevel(env); + + return Error; + } + void setupLogging(const std::filesystem::path& logDir, std::string_view appName) { Log::sMinDebugLevel = getDebugLevel(); diff --git a/components/debug/debugging.hpp b/components/debug/debugging.hpp index 68cdec93a2..2f9d7aa2d7 100644 --- a/components/debug/debugging.hpp +++ b/components/debug/debugging.hpp @@ -36,6 +36,8 @@ namespace Debug Level getDebugLevel(); + Level getRecastMaxLogLevel(); + // Redirect cout and cerr to the log file void setupLogging(const std::filesystem::path& logDir, std::string_view appName); diff --git a/components/detournavigator/makenavmesh.cpp b/components/detournavigator/makenavmesh.cpp index f037da69f8..11751e631c 100644 --- a/components/detournavigator/makenavmesh.cpp +++ b/components/detournavigator/makenavmesh.cpp @@ -523,7 +523,7 @@ namespace DetourNavigator std::unique_ptr prepareNavMeshTileData(const RecastMesh& recastMesh, ESM::RefId worldspace, const TilePosition& tilePosition, const AgentBounds& agentBounds, const RecastSettings& settings) { - RecastContext context(worldspace, tilePosition, agentBounds); + RecastContext context(worldspace, tilePosition, agentBounds, settings.mMaxLogLevel); const auto [minZ, maxZ] = getBoundsByZ(recastMesh, agentBounds.mHalfExtents.z(), settings); diff --git a/components/detournavigator/recastcontext.cpp b/components/detournavigator/recastcontext.cpp index 225f251f4d..89bb268b16 100644 --- a/components/detournavigator/recastcontext.cpp +++ b/components/detournavigator/recastcontext.cpp @@ -1,7 +1,7 @@ #include "recastcontext.hpp" #include "debug.hpp" -#include "components/debug/debuglog.hpp" +#include #include @@ -33,15 +33,20 @@ namespace DetourNavigator } } - RecastContext::RecastContext( - ESM::RefId worldspace, const TilePosition& tilePosition, const AgentBounds& agentBounds) - : mPrefix(formatPrefix(worldspace, tilePosition, agentBounds)) + RecastContext::RecastContext(ESM::RefId worldspace, const TilePosition& tilePosition, + const AgentBounds& agentBounds, Debug::Level maxLogLevel) + : mMaxLogLevel(maxLogLevel) + , mPrefix(formatPrefix(worldspace, tilePosition, agentBounds)) { } void RecastContext::doLog(const rcLogCategory category, const char* msg, const int len) { - if (len > 0) - Log(getLogLevel(category)) << mPrefix << std::string_view(msg, static_cast(len)); + if (msg == nullptr || len <= 0) + return; + const Debug::Level level = getLogLevel(category); + if (level > mMaxLogLevel) + return; + Log(level) << mPrefix << std::string_view(msg, static_cast(len)); } } diff --git a/components/detournavigator/recastcontext.hpp b/components/detournavigator/recastcontext.hpp index 8e75f50b34..7c9d50951b 100644 --- a/components/detournavigator/recastcontext.hpp +++ b/components/detournavigator/recastcontext.hpp @@ -3,6 +3,7 @@ #include "tileposition.hpp" +#include #include #include @@ -16,11 +17,13 @@ namespace DetourNavigator class RecastContext final : public rcContext { public: - explicit RecastContext(ESM::RefId worldspace, const TilePosition& tilePosition, const AgentBounds& agentBounds); + explicit RecastContext(ESM::RefId worldspace, const TilePosition& tilePosition, const AgentBounds& agentBounds, + Debug::Level maxLogLevel); const std::string& getPrefix() const { return mPrefix; } private: + Debug::Level mMaxLogLevel; std::string mPrefix; void doLog(rcLogCategory category, const char* msg, int len) override; diff --git a/components/detournavigator/settings.cpp b/components/detournavigator/settings.cpp index 5e555050f7..d71b3d12bc 100644 --- a/components/detournavigator/settings.cpp +++ b/components/detournavigator/settings.cpp @@ -44,7 +44,7 @@ namespace DetourNavigator }; } - RecastSettings makeRecastSettingsFromSettingsManager() + RecastSettings makeRecastSettingsFromSettingsManager(Debug::Level maxLogLevel) { RecastSettings result; @@ -63,6 +63,7 @@ namespace DetourNavigator result.mRegionMergeArea = ::Settings::navigator().mRegionMergeArea; result.mRegionMinArea = ::Settings::navigator().mRegionMinArea; result.mTileSize = ::Settings::navigator().mTileSize; + result.mMaxLogLevel = maxLogLevel; return result; } @@ -80,11 +81,11 @@ namespace DetourNavigator } } - Settings makeSettingsFromSettingsManager() + Settings makeSettingsFromSettingsManager(Debug::Level maxLogLevel) { Settings result; - result.mRecast = makeRecastSettingsFromSettingsManager(); + result.mRecast = makeRecastSettingsFromSettingsManager(maxLogLevel); result.mDetour = makeDetourSettingsFromSettingsManager(); const NavMeshLimits limits = getNavMeshTileLimits(result.mDetour); diff --git a/components/detournavigator/settings.hpp b/components/detournavigator/settings.hpp index 1d1f6f5847..ecd9cf073b 100644 --- a/components/detournavigator/settings.hpp +++ b/components/detournavigator/settings.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SETTINGS_H #define OPENMW_COMPONENTS_DETOURNAVIGATOR_SETTINGS_H +#include + #include #include @@ -23,6 +25,7 @@ namespace DetourNavigator int mRegionMergeArea = 0; int mRegionMinArea = 0; int mTileSize = 0; + Debug::Level mMaxLogLevel = Debug::Error; }; struct DetourSettings @@ -55,7 +58,7 @@ namespace DetourNavigator inline constexpr std::int64_t navMeshFormatVersion = 2; - Settings makeSettingsFromSettingsManager(); + Settings makeSettingsFromSettingsManager(Debug::Level maxLogLevel); } #endif From 87a2f776b79f842940375d4e371b8d85d699c303 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 22 Mar 2025 02:49:48 +0100 Subject: [PATCH 091/154] Add version to the recast log prefix --- components/detournavigator/makenavmesh.cpp | 2 +- components/detournavigator/recastcontext.cpp | 10 +++++----- components/detournavigator/recastcontext.hpp | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/components/detournavigator/makenavmesh.cpp b/components/detournavigator/makenavmesh.cpp index 11751e631c..fe63d27a1e 100644 --- a/components/detournavigator/makenavmesh.cpp +++ b/components/detournavigator/makenavmesh.cpp @@ -523,7 +523,7 @@ namespace DetourNavigator std::unique_ptr prepareNavMeshTileData(const RecastMesh& recastMesh, ESM::RefId worldspace, const TilePosition& tilePosition, const AgentBounds& agentBounds, const RecastSettings& settings) { - RecastContext context(worldspace, tilePosition, agentBounds, settings.mMaxLogLevel); + RecastContext context(worldspace, tilePosition, agentBounds, recastMesh.getVersion(), settings.mMaxLogLevel); const auto [minZ, maxZ] = getBoundsByZ(recastMesh, agentBounds.mHalfExtents.z(), settings); diff --git a/components/detournavigator/recastcontext.cpp b/components/detournavigator/recastcontext.cpp index 89bb268b16..732517e423 100644 --- a/components/detournavigator/recastcontext.cpp +++ b/components/detournavigator/recastcontext.cpp @@ -23,20 +23,20 @@ namespace DetourNavigator return Debug::Debug; } - std::string formatPrefix( - ESM::RefId worldspace, const TilePosition& tilePosition, const AgentBounds& agentBounds) + std::string formatPrefix(ESM::RefId worldspace, const TilePosition& tilePosition, + const AgentBounds& agentBounds, const Version& version) { std::ostringstream stream; stream << "Worldspace: " << worldspace << "; tile position: " << tilePosition.x() << ", " - << tilePosition.y() << "; agent bounds: " << agentBounds << "; "; + << tilePosition.y() << "; agent bounds: " << agentBounds << "; version: " << version << "; "; return stream.str(); } } RecastContext::RecastContext(ESM::RefId worldspace, const TilePosition& tilePosition, - const AgentBounds& agentBounds, Debug::Level maxLogLevel) + const AgentBounds& agentBounds, const Version& version, Debug::Level maxLogLevel) : mMaxLogLevel(maxLogLevel) - , mPrefix(formatPrefix(worldspace, tilePosition, agentBounds)) + , mPrefix(formatPrefix(worldspace, tilePosition, agentBounds, version)) { } diff --git a/components/detournavigator/recastcontext.hpp b/components/detournavigator/recastcontext.hpp index 7c9d50951b..b36c4b9842 100644 --- a/components/detournavigator/recastcontext.hpp +++ b/components/detournavigator/recastcontext.hpp @@ -13,12 +13,13 @@ namespace DetourNavigator { struct AgentBounds; + struct Version; class RecastContext final : public rcContext { public: explicit RecastContext(ESM::RefId worldspace, const TilePosition& tilePosition, const AgentBounds& agentBounds, - Debug::Level maxLogLevel); + const Version& version, Debug::Level maxLogLevel); const std::string& getPrefix() const { return mPrefix; } From 7112217adc33cc5bae81db0495bf76de07fd7fa4 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 22 Mar 2025 14:36:55 +0100 Subject: [PATCH 092/154] Use temporary directory for tests output --- apps/components_tests/files/hash.cpp | 4 +-- apps/components_tests/main.cpp | 7 ++++- .../components_tests/shader/shadermanager.cpp | 2 +- apps/opencs_tests/main.cpp | 7 ++++- apps/openmw_tests/main.cpp | 7 ++++- components/testing/util.hpp | 30 +++++++++++++------ 6 files changed, 42 insertions(+), 15 deletions(-) diff --git a/apps/components_tests/files/hash.cpp b/apps/components_tests/files/hash.cpp index 793965112b..f94cb1969d 100644 --- a/apps/components_tests/files/hash.cpp +++ b/apps/components_tests/files/hash.cpp @@ -30,7 +30,7 @@ namespace TEST(FilesGetHash, shouldClearErrors) { - const auto fileName = temporaryFilePath("fileName"); + const auto fileName = outputFilePath("fileName"); std::string content; std::fill_n(std::back_inserter(content), 1, 'a'); std::istringstream stream(content); @@ -41,7 +41,7 @@ namespace TEST_P(FilesGetHash, shouldReturnHashForStringStream) { - const auto fileName = temporaryFilePath("fileName"); + const auto fileName = outputFilePath("fileName"); std::string content; std::fill_n(std::back_inserter(content), GetParam().mSize, 'a'); std::istringstream stream(content); diff --git a/apps/components_tests/main.cpp b/apps/components_tests/main.cpp index dcfb2e9ba9..c1b41d184a 100644 --- a/apps/components_tests/main.cpp +++ b/apps/components_tests/main.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include @@ -24,5 +25,9 @@ int main(int argc, char** argv) Settings::StaticValues::init(); testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); + + const int result = RUN_ALL_TESTS(); + if (result == 0) + std::filesystem::remove_all(TestingOpenMW::outputDir()); + return result; } diff --git a/apps/components_tests/shader/shadermanager.cpp b/apps/components_tests/shader/shadermanager.cpp index 5b11d31a44..b80839d0ec 100644 --- a/apps/components_tests/shader/shadermanager.cpp +++ b/apps/components_tests/shader/shadermanager.cpp @@ -16,7 +16,7 @@ namespace ShaderManager mManager; ShaderManager::DefineMap mDefines; - ShaderManagerTest() { mManager.setShaderPath("tests_output"); } + ShaderManagerTest() { mManager.setShaderPath(TestingOpenMW::outputDir()); } template void withShaderFile(const std::string& content, F&& f) diff --git a/apps/opencs_tests/main.cpp b/apps/opencs_tests/main.cpp index fd7d4900c8..fed1cd2bb1 100644 --- a/apps/opencs_tests/main.cpp +++ b/apps/opencs_tests/main.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -7,5 +8,9 @@ int main(int argc, char* argv[]) Log::sMinDebugLevel = Debug::getDebugLevel(); testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); + + const int result = RUN_ALL_TESTS(); + if (result == 0) + std::filesystem::remove_all(TestingOpenMW::outputDir()); + return result; } diff --git a/apps/openmw_tests/main.cpp b/apps/openmw_tests/main.cpp index 6b7298596a..485298c863 100644 --- a/apps/openmw_tests/main.cpp +++ b/apps/openmw_tests/main.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include @@ -24,5 +25,9 @@ int main(int argc, char* argv[]) Settings::StaticValues::init(); testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); + + const int result = RUN_ALL_TESTS(); + if (result == 0) + std::filesystem::remove_all(TestingOpenMW::outputDir()); + return result; } diff --git a/components/testing/util.hpp b/components/testing/util.hpp index 941f495458..53331d6d37 100644 --- a/components/testing/util.hpp +++ b/components/testing/util.hpp @@ -1,6 +1,7 @@ #ifndef OPENMW_COMPONENTS_TESTING_UTIL_H #define OPENMW_COMPONENTS_TESTING_UTIL_H +#include #include #include #include @@ -14,26 +15,37 @@ namespace TestingOpenMW { - inline std::filesystem::path outputFilePath(const std::string name) + inline std::filesystem::path outputDir() { - std::filesystem::path dir("tests_output"); - std::filesystem::create_directory(dir); + static const std::string run + = std::to_string(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())); + std::filesystem::path dir = std::filesystem::temp_directory_path() / "openmw" / "tests" / run; + std::filesystem::create_directories(dir); + return dir; + } + + inline std::filesystem::path outputFilePath(std::string_view name) + { + std::filesystem::path dir = outputDir(); return dir / Misc::StringUtils::stringToU8String(name); } + inline std::filesystem::path outputDirPath(const std::filesystem::path& subpath) + { + std::filesystem::path path = outputDir(); + path /= subpath; + std::filesystem::create_directories(path); + return path; + } + inline std::filesystem::path outputFilePathWithSubDir(const std::filesystem::path& subpath) { - std::filesystem::path path("tests_output"); + std::filesystem::path path = outputDir(); path /= subpath; std::filesystem::create_directories(path.parent_path()); return path; } - inline std::filesystem::path temporaryFilePath(const std::string name) - { - return std::filesystem::temp_directory_path() / name; - } - class VFSTestFile : public VFS::File { public: From ada48d90215fbe5647c16d68d6f1e9b7395d25e0 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 22 Mar 2025 14:39:51 +0100 Subject: [PATCH 093/154] Reduce a chance to have a deadlock in the AsyncNavMeshUpdater * Do not fail tile generation if debug mesh writing fails. * Mark some functions as noexcept to better crash than have a deadlock. * Unlock tile and remove job if there on exception while processing it. --- .../detournavigator/asyncnavmeshupdater.cpp | 102 ++++++++++++++++++ .../detournavigator/asyncnavmeshupdater.cpp | 45 ++++++-- .../detournavigator/asyncnavmeshupdater.hpp | 4 +- 3 files changed, 138 insertions(+), 13 deletions(-) diff --git a/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp b/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp index ea9efc3df2..3094e1cea6 100644 --- a/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp +++ b/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include #include @@ -372,6 +374,106 @@ namespace } } + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_recast_mesh) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteRecastMeshToFile = true; + const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); + mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_TRUE(std::filesystem::exists(dir / "0.0.recastmesh.obj")); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_recast_mesh_with_revision) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteRecastMeshToFile = true; + mSettings.mEnableRecastMeshFileNameRevision = true; + const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); + mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_TRUE(std::filesystem::exists(dir / "0.0.recastmesh.1.2.obj")); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, writing_recast_mesh_to_absent_file_should_not_fail_tile_generation) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteRecastMeshToFile = true; + const std::filesystem::path dir = TestingOpenMW::outputDir() / "absent"; + mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0u); + EXPECT_FALSE(std::filesystem::exists(dir)); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_navmesh) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteNavMeshToFile = true; + const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); + mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_TRUE(std::filesystem::exists(dir / "all_tiles_navmesh.bin")); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_navmesh_with_revision) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteNavMeshToFile = true; + mSettings.mEnableNavMeshFileNameRevision = true; + const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); + mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_TRUE(std::filesystem::exists(dir / "all_tiles_navmesh.1.1.bin")); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, writing_navmesh_to_absent_file_should_not_fail_tile_generation) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteNavMeshToFile = true; + const std::filesystem::path dir = TestingOpenMW::outputDir() / "absent"; + mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0u); + EXPECT_FALSE(std::filesystem::exists(dir)); + } + struct DetourNavigatorSpatialJobQueueTest : Test { const AgentBounds mAgentBounds{ CollisionShapeType::Aabb, osg::Vec3f(1, 1, 1) }; diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index 71c8bbc2d3..5f82c85cf9 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -453,9 +453,9 @@ namespace DetourNavigator Misc::setCurrentThreadIdlePriority(); while (!mShouldStop) { - try + if (JobIt job = getNextJob(); job != mJobs.end()) { - if (JobIt job = getNextJob(); job != mJobs.end()) + try { const JobStatus status = processJob(*job); Log(Debug::Debug) << "Processed job " << job->mId << " with status=" << status @@ -480,12 +480,20 @@ namespace DetourNavigator } } } - else - cleanupLastUpdates(); + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to process navmesh job " << job->mId + << " for worldspace=" << job->mWorldspace << " agent=" << job->mAgentBounds + << " changedTile=(" << job->mChangedTile << ")" + << " changeType=" << job->mChangeType + << " by thread=" << std::this_thread::get_id() << ": " << e.what(); + unlockTile(job->mId, job->mAgentBounds, job->mChangedTile); + removeJob(job); + } } - catch (const std::exception& e) + else { - Log(Debug::Error) << "AsyncNavMeshUpdater::process exception: " << e.what(); + cleanupLastUpdates(); } } Log(Debug::Debug) << "Stop navigator jobs processing by thread=" << std::this_thread::get_id(); @@ -493,7 +501,8 @@ namespace DetourNavigator JobStatus AsyncNavMeshUpdater::processJob(Job& job) { - Log(Debug::Debug) << "Processing job " << job.mId << " for agent=(" << job.mAgentBounds << ")" + Log(Debug::Debug) << "Processing job " << job.mId << " for worldspace=" << job.mWorldspace + << " agent=" << job.mAgentBounds << "" << " changedTile=(" << job.mChangedTile << ")" << " changeType=" << job.mChangeType << " by thread=" << std::this_thread::get_id(); @@ -543,7 +552,14 @@ namespace DetourNavigator return JobStatus::Done; } - writeDebugRecastMesh(mSettings, job.mChangedTile, *recastMesh); + try + { + writeDebugRecastMesh(mSettings, job.mChangedTile, *recastMesh); + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to write debug recast mesh: " << e.what(); + } NavMeshTilesCache::Value cachedNavMeshData = mNavMeshTilesCache.get(job.mAgentBounds, job.mChangedTile, *recastMesh); @@ -666,12 +682,19 @@ namespace DetourNavigator mPresentTiles.insert(std::make_tuple(job.mAgentBounds, job.mChangedTile)); } - writeDebugNavMesh(mSettings, navMeshCacheItem, navMeshVersion); + try + { + writeDebugNavMesh(mSettings, navMeshCacheItem, navMeshVersion); + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to write debug navmesh: " << e.what(); + } return isSuccess(status) ? JobStatus::Done : JobStatus::Fail; } - JobIt AsyncNavMeshUpdater::getNextJob() + JobIt AsyncNavMeshUpdater::getNextJob() noexcept { std::unique_lock lock(mMutex); @@ -746,7 +769,7 @@ namespace DetourNavigator return mJobs.size(); } - void AsyncNavMeshUpdater::cleanupLastUpdates() + void AsyncNavMeshUpdater::cleanupLastUpdates() noexcept { const auto now = std::chrono::steady_clock::now(); diff --git a/components/detournavigator/asyncnavmeshupdater.hpp b/components/detournavigator/asyncnavmeshupdater.hpp index 7877ff8082..95919ed770 100644 --- a/components/detournavigator/asyncnavmeshupdater.hpp +++ b/components/detournavigator/asyncnavmeshupdater.hpp @@ -244,7 +244,7 @@ namespace DetourNavigator inline JobStatus handleUpdateNavMeshStatus(UpdateNavMeshStatus status, const Job& job, const GuardedNavMeshCacheItem& navMeshCacheItem, const RecastMesh& recastMesh); - JobIt getNextJob(); + inline JobIt getNextJob() noexcept; void postThreadJob(JobIt job, std::deque& queue); @@ -254,7 +254,7 @@ namespace DetourNavigator inline std::size_t getTotalJobs() const; - void cleanupLastUpdates(); + inline void cleanupLastUpdates() noexcept; inline void waitUntilJobsDoneForNotPresentTiles(Loading::Listener* listener); From f8be5fdd2a143171ff5eca55ea568f4ef915506d Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Mon, 24 Mar 2025 21:52:21 +0300 Subject: [PATCH 094/154] Give point lights a minimum radius of 16 --- components/sceneutil/lightutil.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/sceneutil/lightutil.cpp b/components/sceneutil/lightutil.cpp index 08992b1ae3..90dc8237f2 100644 --- a/components/sceneutil/lightutil.cpp +++ b/components/sceneutil/lightutil.cpp @@ -119,7 +119,8 @@ namespace SceneUtil osg::ref_ptr light(new osg::Light); lightSource->setNodeMask(lightMask); - float radius = esmLight.mRadius; + // The minimum scene light radius is 16 in Morrowind + const float radius = std::max(esmLight.mRadius, 16.f); lightSource->setRadius(radius); configureLight(light, radius, isExterior); From 166852254f2ae303f27e3996c24d45995abda253 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Tue, 25 Mar 2025 01:32:44 +0000 Subject: [PATCH 095/154] Use non-deprecated known folder API SHGetFolderPathW was deprecated in Windows Vista nearly two decades ago. ShGetKnownFolderPath is the replacement. Also log if there was an error. Someone seemed to be getting an error on Discord, despite other apps being able to get the path just fine with these functions. Also don't pass the flags to create the folders if they don't exist. We probably don't have the right permissions and if they don't exist, then there are bigger problems. Maybe this will fix the issue the user was having. Also add a comment about global config on Windows being fundamentally wrong. --- components/files/windowspath.cpp | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/components/files/windowspath.cpp b/components/files/windowspath.cpp index bbe0325b58..6fb9845976 100644 --- a/components/files/windowspath.cpp +++ b/components/files/windowspath.cpp @@ -36,12 +36,14 @@ namespace Files { std::filesystem::path userPath = std::filesystem::current_path(); - WCHAR path[MAX_PATH + 1] = {}; + PWSTR cString; + HRESULT result = SHGetKnownFolderPath(FOLDERID_Documents, 0, nullptr, &cString); + if (SUCCEEDED(result)) + userPath = std::filesystem::path(cString); + else + Log(Debug::Error) << "Error " << result << " when getting Documents path"; - if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_PERSONAL | CSIDL_FLAG_CREATE, nullptr, 0, path))) - { - userPath = std::filesystem::path(path); - } + CoTaskMemFree(cString); return userPath / "My Games" / mName; } @@ -54,14 +56,19 @@ namespace Files std::filesystem::path WindowsPath::getGlobalConfigPath() const { + // The concept of a global config path is absurd on Windows. + // Always use local config instead. + // The virtual base class requires that we provide this, though. std::filesystem::path globalPath = std::filesystem::current_path(); - WCHAR path[MAX_PATH + 1] = {}; + PWSTR cString; + HRESULT result = SHGetKnownFolderPath(FOLDERID_ProgramFiles, 0, nullptr, &cString); + if (SUCCEEDED(result)) + globalPath = std::filesystem::path(cString); + else + Log(Debug::Error) << "Error " << result << " when getting Program Files path"; - if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_PROGRAM_FILES | CSIDL_FLAG_CREATE, nullptr, 0, path))) - { - globalPath = std::filesystem::path(path); - } + CoTaskMemFree(cString); return globalPath / mName; } From cbcd4f6acdd6f19d592a0cc494082247370d26aa Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 28 Feb 2025 23:58:35 +0100 Subject: [PATCH 096/154] Move matchers to different module --- .../integration_tests/test_lua_api/player.lua | 11 +-- .../testing_util/matchers.lua | 79 +++++++++++++++++++ .../testing_util/testing_util.lua | 70 ---------------- scripts/data/morrowind_tests/player.lua | 51 ++++++------ 4 files changed, 111 insertions(+), 100 deletions(-) create mode 100644 scripts/data/integration_tests/testing_util/matchers.lua diff --git a/scripts/data/integration_tests/test_lua_api/player.lua b/scripts/data/integration_tests/test_lua_api/player.lua index 74769da8e0..0b481fba2b 100644 --- a/scripts/data/integration_tests/test_lua_api/player.lua +++ b/scripts/data/integration_tests/test_lua_api/player.lua @@ -6,6 +6,7 @@ local input = require('openmw.input') local types = require('openmw.types') local nearby = require('openmw.nearby') local camera = require('openmw.camera') +local matchers = require('matchers') types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Controls, false) types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Fighting, false) @@ -113,13 +114,13 @@ testing.registerLocalTest('player rotation', coroutine.yield() local alpha1, gamma1 = self.rotation:getAnglesXZ() - testing.expectThat(alpha1, testing.isNotNan(), 'Alpha rotation in XZ convention is nan') - testing.expectThat(gamma1, testing.isNotNan(), 'Gamma rotation in XZ convention is nan') + testing.expectThat(alpha1, matchers.isNotNan(), 'Alpha rotation in XZ convention is nan') + testing.expectThat(gamma1, matchers.isNotNan(), 'Gamma rotation in XZ convention is nan') local alpha2, beta2, gamma2 = self.rotation:getAnglesZYX() - testing.expectThat(alpha2, testing.isNotNan(), 'Alpha rotation in ZYX convention is nan') - testing.expectThat(beta2, testing.isNotNan(), 'Beta rotation in ZYX convention is nan') - testing.expectThat(gamma2, testing.isNotNan(), 'Gamma rotation in ZYX convention is nan') + testing.expectThat(alpha2, matchers.isNotNan(), 'Alpha rotation in ZYX convention is nan') + testing.expectThat(beta2, matchers.isNotNan(), 'Beta rotation in ZYX convention is nan') + testing.expectThat(gamma2, matchers.isNotNan(), 'Gamma rotation in ZYX convention is nan') end end) diff --git a/scripts/data/integration_tests/testing_util/matchers.lua b/scripts/data/integration_tests/testing_util/matchers.lua new file mode 100644 index 0000000000..efb9e11587 --- /dev/null +++ b/scripts/data/integration_tests/testing_util/matchers.lua @@ -0,0 +1,79 @@ +local module = {} + +--- +-- Matcher verifying that distance between given value and expected is not greater than maxDistance. +-- @function elementsAreArray +-- @param expected#vector. +-- @usage +-- expectThat(util.vector2(0, 0), closeToVector(util.vector2(0, 1), 1)) +function module.closeToVector(expected, maxDistance) + return function(actual) + local distance = (expected - actual):length() + if distance <= maxDistance then + return '' + end + return string.format('%s is too far from expected %s: %s > %s', actual, expected, distance, maxDistance) + end +end + +--- +-- Matcher verifying that given value is an array each element of which matches elements of expected. +-- @function elementsAreArray +-- @param expected#array of values or matcher functions. +-- @usage +-- local t = {42, 13} +-- local matcher = function(actual) +-- if actual ~= 42 then +-- return string.format('%s is not 42', actual) +-- end +-- return '' +-- end +-- expectThat({42, 13}, elementsAreArray({matcher, 13})) +function module.elementsAreArray(expected) + local expected_matchers = {} + for i, v in ipairs(expected) do + if type(v) == 'function' then + expected_matchers[i] = v + else + expected_matchers[i] = function (other) + if expected[i].__eq(expected[i], other) then + return '' + end + return string.format('%s element %s does no match expected: %s', i, other, expected[i]) + end + end + end + return function(actual) + if #actual < #expected_matchers then + return string.format('number of elements is less than expected: %s < %s', #actual, #expected_matchers) + end + local message = '' + for i, v in ipairs(actual) do + if i > #expected_matchers then + message = string.format('%s\n%s element is out of expected range: %s', message, i, #expected_matchers) + break + end + local match_message = expected_matchers[i](v) + if match_message ~= '' then + message = string.format('%s\n%s', message, match_message) + end + end + return message + end +end + +--- +-- Matcher verifying that given number is not a nan. +-- @function isNotNan +-- @usage +-- expectThat(value, isNotNan()) +function module.isNotNan() + return function(actual) + if actual ~= actual then + return 'actual value is nan, expected to be not nan' + end + return '' + end +end + +return module diff --git a/scripts/data/integration_tests/testing_util/testing_util.lua b/scripts/data/integration_tests/testing_util/testing_util.lua index 5a69cb89ed..c894fa96f4 100644 --- a/scripts/data/integration_tests/testing_util/testing_util.lua +++ b/scripts/data/integration_tests/testing_util/testing_util.lua @@ -158,76 +158,6 @@ function M.expectNotEqual(v1, v2, msg) end end -function M.closeToVector(expected, maxDistance) - return function(actual) - local distance = (expected - actual):length() - if distance <= maxDistance then - return '' - end - return string.format('%s is too far from expected %s: %s > %s', actual, expected, distance, maxDistance) - end -end - ---- --- Matcher verifying that given value is an array each element of which matches elements of expected. --- @function elementsAreArray --- @param expected#array of values or matcher functions. --- @usage --- local t = {42, 13} --- local matcher = function(actual) --- if actual ~= 42 then --- return string.format('%s is not 42', actual) --- end --- return '' --- end --- expectThat({42, 13}, elementsAreArray({matcher, 13})) -function M.elementsAreArray(expected) - local expected_matchers = {} - for i, v in ipairs(expected) do - if type(v) == 'function' then - expected_matchers[i] = v - else - expected_matchers[i] = function (other) - if expected[i].__eq(expected[i], other) then - return '' - end - return string.format('%s element %s does no match expected: %s', i, other, expected[i]) - end - end - end - return function(actual) - if #actual < #expected_matchers then - return string.format('number of elements is less than expected: %s < %s', #actual, #expected_matchers) - end - local message = '' - for i, v in ipairs(actual) do - if i > #expected_matchers then - message = string.format('%s\n%s element is out of expected range: %s', message, i, #expected_matchers) - break - end - local match_message = expected_matchers[i](v) - if match_message ~= '' then - message = string.format('%s\n%s', message, match_message) - end - end - return message - end -end - ---- --- Matcher verifying that given number is not a nan. --- @function isNotNan --- @usage --- expectThat(value, isNotNan()) -function M.isNotNan(expected) - return function(actual) - if actual ~= actual then - return 'actual value is nan, expected to be not nan' - end - return '' - end -end - --- -- Verifies that given value matches provided matcher. -- @function expectThat diff --git a/scripts/data/morrowind_tests/player.lua b/scripts/data/morrowind_tests/player.lua index 226d9754f0..fcb1126c1b 100644 --- a/scripts/data/morrowind_tests/player.lua +++ b/scripts/data/morrowind_tests/player.lua @@ -5,6 +5,7 @@ local testing = require('testing_util') local util = require('openmw.util') local types = require('openmw.types') local nearby = require('openmw.nearby') +local matchers = require('matchers') types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Fighting, false) types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Jumping, false) @@ -45,33 +46,33 @@ testing.registerLocalTest('Guard in Imperial Prison Ship should find path (#7241 testing.expectLessOrEqual((util.vector2(path[#path].x, path[#path].y) - util.vector2(dst.x, dst.y)):length(), 1, 'Last path point x, y') testing.expectLessOrEqual(path[#path].z - dst.z, 20, 'Last path point z') if agentBounds.shapeType == nearby.COLLISION_SHAPE_TYPE.Aabb then - testing.expectThat(path, testing.elementsAreArray({ - testing.closeToVector(util.vector3(34.29737091064453125, 806.3817138671875, 112.76610565185546875), 1e-1), - testing.closeToVector(util.vector3(15, 1102, 112.2945709228515625), 1e-1), - testing.closeToVector(util.vector3(-112, 1110, 112.2945709228515625), 1e-1), - testing.closeToVector(util.vector3(-118, 1393, 112.2945709228515625), 1e-1), - testing.closeToVector(util.vector3(-67.99993896484375, 1421.2000732421875, 112.2945709228515625), 1e-1), - testing.closeToVector(util.vector3(-33.999935150146484375, 1414.4000244140625, 112.2945709228515625), 1e-1), - testing.closeToVector(util.vector3(-6.79993534088134765625, 1380.4000244140625, 85.094573974609375), 1e-1), - testing.closeToVector(util.vector3(79, 724, -104.83390045166015625), 1e-1), - testing.closeToVector(util.vector3(84, 290.000030517578125, -104.83390045166015625), 1e-1), - testing.closeToVector(util.vector3(83.552001953125, 42.26399993896484375, -104.58989715576171875), 1e-1), - testing.closeToVector(util.vector3(89, -105, -98.72841644287109375), 1e-1), - testing.closeToVector(util.vector3(90, -90, -99.7056884765625), 1e-1), + testing.expectThat(path, matchers.elementsAreArray({ + matchers.closeToVector(util.vector3(34.29737091064453125, 806.3817138671875, 112.76610565185546875), 1e-1), + matchers.closeToVector(util.vector3(15, 1102, 112.2945709228515625), 1e-1), + matchers.closeToVector(util.vector3(-112, 1110, 112.2945709228515625), 1e-1), + matchers.closeToVector(util.vector3(-118, 1393, 112.2945709228515625), 1e-1), + matchers.closeToVector(util.vector3(-67.99993896484375, 1421.2000732421875, 112.2945709228515625), 1e-1), + matchers.closeToVector(util.vector3(-33.999935150146484375, 1414.4000244140625, 112.2945709228515625), 1e-1), + matchers.closeToVector(util.vector3(-6.79993534088134765625, 1380.4000244140625, 85.094573974609375), 1e-1), + matchers.closeToVector(util.vector3(79, 724, -104.83390045166015625), 1e-1), + matchers.closeToVector(util.vector3(84, 290.000030517578125, -104.83390045166015625), 1e-1), + matchers.closeToVector(util.vector3(83.552001953125, 42.26399993896484375, -104.58989715576171875), 1e-1), + matchers.closeToVector(util.vector3(89, -105, -98.72841644287109375), 1e-1), + matchers.closeToVector(util.vector3(90, -90, -99.7056884765625), 1e-1), })) elseif agentBounds.shapeType == nearby.COLLISION_SHAPE_TYPE.Cylinder then - testing.expectThat(path, testing.elementsAreArray({ - testing.closeToVector(util.vector3(34.29737091064453125, 806.3817138671875, 112.76610565185546875), 1e-1), - testing.closeToVector(util.vector3(-13.5999355316162109375, 1060.800048828125, 112.2945709228515625), 1e-1), - testing.closeToVector(util.vector3(-27.1999359130859375, 1081.2000732421875, 112.2945709228515625), 1e-1), - testing.closeToVector(util.vector3(-81.59993743896484375, 1128.800048828125, 112.2945709228515625), 1e-1), - testing.closeToVector(util.vector3(-101.99993896484375, 1156.0001220703125, 112.2945709228515625), 1e-1), - testing.closeToVector(util.vector3(-118, 1393, 112.2945709228515625), 1e-1), - testing.closeToVector(util.vector3(7, 1470, 114.73973846435546875), 1e-1), - testing.closeToVector(util.vector3(79, 724, -104.83390045166015625), 1e-1), - testing.closeToVector(util.vector3(84, 290.000030517578125, -104.83390045166015625), 1e-1), - testing.closeToVector(util.vector3(95, 27, -104.83390045166015625), 1e-1), - testing.closeToVector(util.vector3(90, -90, -104.83390045166015625), 1e-1), + testing.expectThat(path, matchers.elementsAreArray({ + matchers.closeToVector(util.vector3(34.29737091064453125, 806.3817138671875, 112.76610565185546875), 1e-1), + matchers.closeToVector(util.vector3(-13.5999355316162109375, 1060.800048828125, 112.2945709228515625), 1e-1), + matchers.closeToVector(util.vector3(-27.1999359130859375, 1081.2000732421875, 112.2945709228515625), 1e-1), + matchers.closeToVector(util.vector3(-81.59993743896484375, 1128.800048828125, 112.2945709228515625), 1e-1), + matchers.closeToVector(util.vector3(-101.99993896484375, 1156.0001220703125, 112.2945709228515625), 1e-1), + matchers.closeToVector(util.vector3(-118, 1393, 112.2945709228515625), 1e-1), + matchers.closeToVector(util.vector3(7, 1470, 114.73973846435546875), 1e-1), + matchers.closeToVector(util.vector3(79, 724, -104.83390045166015625), 1e-1), + matchers.closeToVector(util.vector3(84, 290.000030517578125, -104.83390045166015625), 1e-1), + matchers.closeToVector(util.vector3(95, 27, -104.83390045166015625), 1e-1), + matchers.closeToVector(util.vector3(90, -90, -104.83390045166015625), 1e-1), })) end end) From 536325e0ba54738453aada9666f129ab0c674de2 Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 28 Feb 2025 23:51:58 +0100 Subject: [PATCH 097/154] Add test for saving and loading the game --- .../integration_tests/test_lua_api/menu.lua | 44 +++++- .../testing_util/matchers.lua | 139 ++++++++++++++++++ 2 files changed, 178 insertions(+), 5 deletions(-) diff --git a/scripts/data/integration_tests/test_lua_api/menu.lua b/scripts/data/integration_tests/test_lua_api/menu.lua index 37f7cb826e..8316761765 100644 --- a/scripts/data/integration_tests/test_lua_api/menu.lua +++ b/scripts/data/integration_tests/test_lua_api/menu.lua @@ -1,12 +1,46 @@ local testing = require('testing_util') +local matchers = require('matchers') local menu = require('openmw.menu') +testing.registerMenuTest('save and load', function() + menu.newGame() + coroutine.yield() + menu.saveGame('save and load') + coroutine.yield() + + local directorySaves = {} + directorySaves['save_and_load.omwsave'] = { + playerName = '', + playerLevel = 1, + timePlayed = 0, + description = 'save and load', + contentFiles = { + 'builtin.omwscripts', + 'template.omwgame', + 'landracer.omwaddon', + 'the_hub.omwaddon', + 'test_lua_api.omwscripts', + }, + creationTime = matchers.isAny(), + } + local expectedAllSaves = {} + expectedAllSaves[' - 1'] = directorySaves + + testing.expectThat(menu.getAllSaves(), matchers.equalTo(expectedAllSaves)) + + menu.loadGame(' - 1', 'save_and_load.omwsave') + coroutine.yield() + + menu.deleteGame(' - 1', 'save_and_load.omwsave') + testing.expectThat(menu.getAllSaves(), matchers.equalTo({})) +end) + local function registerGlobalTest(name, description) - testing.registerMenuTest(description or name, function() - menu.newGame() - coroutine.yield() - testing.runGlobalTest(name) - end) + testing.registerMenuTest(description or name, function() + menu.newGame() + coroutine.yield() + testing.runGlobalTest(name) + end) end registerGlobalTest('timers') diff --git a/scripts/data/integration_tests/testing_util/matchers.lua b/scripts/data/integration_tests/testing_util/matchers.lua index efb9e11587..c7643af206 100644 --- a/scripts/data/integration_tests/testing_util/matchers.lua +++ b/scripts/data/integration_tests/testing_util/matchers.lua @@ -76,4 +76,143 @@ function module.isNotNan() end end +--- +-- Matcher accepting any value. +-- @function isAny +-- @usage +-- expectThat(value, isAny()) +function module.isAny() + return function(actual) + return '' + end +end + +local function serializeArray(a) + local result = nil + for _, v in ipairs(a) do + if result == nil then + result = string.format('{%s', serialize(v)) + else + result = string.format('%s, %s', result, serialize(v)) + end + end + if result == nil then + return '{}' + end + return string.format('%s}', result) +end + +local function serializeTable(t) + local result = nil + for k, v in pairs(t) do + if result == nil then + result = string.format('{%q = %s', k, serialize(v)) + else + result = string.format('%s, %q = %s', result, k, serialize(v)) + end + end + if result == nil then + return '{}' + end + return string.format('%s}', result) +end + +local function isArray(t) + local i = 1 + for _ in pairs(t) do + if t[i] == nil then + return false + end + i = i + 1 + end + return true +end + +function serialize(v) + local t = type(v) + if t == 'string' then + return string.format('%q', v) + elseif t == 'table' then + if isArray(v) then + return serializeArray(v) + end + return serializeTable(v) + end + return string.format('%s', v) +end + +local function compareScalars(v1, v2) + if v1 == v2 then + return '' + end + if type(v1) == 'string' then + return string.format('%q ~= %q', v1, v2) + end + return string.format('%s ~= %s', v1, v2) +end + +local function collectKeys(t) + local result = {} + for key in pairs(t) do + table.insert(result, key) + end + table.sort(result) + return result +end + +local function compareTables(t1, t2) + local keys1 = collectKeys(t1) + local keys2 = collectKeys(t2) + if #keys1 ~= #keys2 then + return string.format('table size mismatch: %d ~= %d', #keys1, #keys2) + end + for i = 1, #keys1 do + local key1 = keys1[i] + local key2 = keys2[i] + if key1 ~= key2 then + return string.format('table keys mismatch: %q ~= %q', key1, key2) + end + local d = compare(t1[key1], t2[key2]) + if d ~= '' then + return string.format('table values mismatch at key %s: %s', serialize(key1), d) + end + end + return '' +end + +function compare(v1, v2) + local type1 = type(v1) + local type2 = type(v2) + if type2 == 'function' then + return v2(v1) + end + if type1 ~= type2 then + return string.format('types mismatch: %s ~= %s', type1, type2) + end + if type1 == 'nil' then + return '' + elseif type1 == 'table' then + return compareTables(v1, v2) + elseif type1 == 'nil' or type1 == 'boolean' or type1 == 'number' or type1 == 'string' then + return compareScalars(v1, v2) + end + error('unsupported type: %s', type1) +end + +--- +-- Matcher verifying that given value is equal to expected. Accepts nil, boolean, number, string and table or matcher +-- function. +-- @function equalTo +-- @usage +-- expectThat({a = {42, 'foo', {b = true}}}, equalTo({a = {42, 'foo', {b = true}}})) +function module.equalTo(expected) + return function(actual) + local diff = compare(actual, expected) + if diff == '' then + return '' + end + return string.format('%s; actual: %s; expected: %s', diff, serialize(actual, ''), serialize(expected, '')) + end +end + return module From 2ebdc43bbe19514029ab080e1e80e6d80285a37b Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 7 Mar 2025 22:24:39 +0100 Subject: [PATCH 098/154] Add test for load while teleporting To reproduce #8311. Load game while landracer is scheduled to teleport from different cell. --- .../integration_tests/test_lua_api/global.lua | 18 ++++++++++++++++++ .../integration_tests/test_lua_api/menu.lua | 17 +++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/scripts/data/integration_tests/test_lua_api/global.lua b/scripts/data/integration_tests/test_lua_api/global.lua index cc8240554a..225660b858 100644 --- a/scripts/data/integration_tests/test_lua_api/global.lua +++ b/scripts/data/integration_tests/test_lua_api/global.lua @@ -326,6 +326,24 @@ testing.registerGlobalTest('player weapon attack', function() testing.runLocalTest(player, 'player weapon attack') end) +testing.registerGlobalTest('load while teleporting - init player', function() + local player = world.players[1] + player:teleport('Museum of Wonders', util.vector3(0, -1500, 111), util.transform.rotateZ(math.rad(180))) +end) + +testing.registerGlobalTest('load while teleporting - teleport', function() + local player = world.players[1] + local landracer = world.createObject('landracer') + landracer:teleport(player.cell, player.position + util.vector3(0, 500, 0)) + coroutine.yield() + + local door = world.getObjectByFormId(core.getFormId('the_hub.omwaddon', 26)) + door:activateBy(player) + coroutine.yield() + + landracer:teleport(player.cell, player.position) +end) + return { engineHandlers = { onUpdate = testing.updateGlobal, diff --git a/scripts/data/integration_tests/test_lua_api/menu.lua b/scripts/data/integration_tests/test_lua_api/menu.lua index 8316761765..8c6895a5d8 100644 --- a/scripts/data/integration_tests/test_lua_api/menu.lua +++ b/scripts/data/integration_tests/test_lua_api/menu.lua @@ -35,6 +35,23 @@ testing.registerMenuTest('save and load', function() testing.expectThat(menu.getAllSaves(), matchers.equalTo({})) end) +testing.registerMenuTest('load while teleporting', function() + menu.newGame() + coroutine.yield() + + testing.runGlobalTest('load while teleporting - init player') + + menu.saveGame('load while teleporting') + coroutine.yield() + + testing.runGlobalTest('load while teleporting - teleport') + + menu.loadGame(' - 1', 'load_while_teleporting.omwsave') + coroutine.yield() + + menu.deleteGame(' - 1', 'load_while_teleporting.omwsave') +end) + local function registerGlobalTest(name, description) testing.registerMenuTest(description or name, function() menu.newGame() From caef91d261c4cdcdbe4f2922b4e2afe68364b30c Mon Sep 17 00:00:00 2001 From: Dave Corley Date: Thu, 27 Mar 2025 14:55:05 -0700 Subject: [PATCH 099/154] FIX: Remove outdated instructions for ubuntu installation --- docs/source/manuals/installation/install-openmw.rst | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/source/manuals/installation/install-openmw.rst b/docs/source/manuals/installation/install-openmw.rst index 50efe7542d..9c1f79ae4f 100644 --- a/docs/source/manuals/installation/install-openmw.rst +++ b/docs/source/manuals/installation/install-openmw.rst @@ -29,12 +29,7 @@ Add it and install OpenMW:: $ sudo add-apt-repository ppa:openmw/openmw $ sudo apt-get update - $ sudo apt-get install openmw openmw-launcher - -.. note:: - OpenMW-CS must be installed separately by typing:: - - $ sudo apt-get install openmw-cs + $ sudo apt-get install openmw The Arch Linux Way ================== From b6be7cdd56f81417a548338c3cf3dc3398aa4cf7 Mon Sep 17 00:00:00 2001 From: Dave Corley Date: Fri, 28 Mar 2025 22:27:30 +0000 Subject: [PATCH 100/154] CLEANUP: Use apt instead of apt-get --- docs/source/manuals/installation/install-openmw.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/manuals/installation/install-openmw.rst b/docs/source/manuals/installation/install-openmw.rst index 9c1f79ae4f..2ef72abfd5 100644 --- a/docs/source/manuals/installation/install-openmw.rst +++ b/docs/source/manuals/installation/install-openmw.rst @@ -28,8 +28,8 @@ A `Launchpad PPA `_ is available. Add it and install OpenMW:: $ sudo add-apt-repository ppa:openmw/openmw - $ sudo apt-get update - $ sudo apt-get install openmw + $ sudo apt update + $ sudo apt install openmw The Arch Linux Way ================== From a61ce111a5033165402e20cbde3f19f59a1d0070 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 31 Mar 2025 00:28:06 +0200 Subject: [PATCH 101/154] Remove declaration without definition --- apps/openmw/mwmechanics/aiwander.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index aed7214f4d..f4d5585fe7 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -182,8 +182,6 @@ namespace MWMechanics /// lookup table for converting idleSelect value to groupName static const std::string sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1]; - - static int OffsetToPreventOvercrowding(); }; } From 86426aa87b159c595ed39b09730299eafdc384a8 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 31 Mar 2025 17:11:09 +0200 Subject: [PATCH 102/154] Open the data directory file picker at the last opened location --- apps/launcher/datafilespage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 9bb54fdb8e..093c64c99b 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -765,7 +765,7 @@ void Launcher::DataFilesPage::addSubdirectories(bool append) return; QString rootPath = QFileDialog::getExistingDirectory( - this, tr("Select Directory"), QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::Option::ReadOnly); + this, tr("Select Directory"), {}, QFileDialog::ShowDirsOnly | QFileDialog::Option::ReadOnly); if (rootPath.isEmpty()) return; From 9a6807f862dc4aa8bd4a1ec771ec3025bfc0942e Mon Sep 17 00:00:00 2001 From: elsid Date: Tue, 1 Apr 2025 23:19:18 +0200 Subject: [PATCH 103/154] Remove cmake_minimum_required for osx install script --- CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1805ea6fea..23e158fa78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -860,7 +860,6 @@ if (OPENMW_OSX_DEPLOYMENT AND APPLE) set(BU_CHMOD_BUNDLE_ITEMS ON) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH}) include(BundleUtilities) - cmake_minimum_required(VERSION 3.1) " COMPONENT Runtime) set(ABSOLUTE_PLUGINS "") From 065a3886325cba7877aa4532939236b43e855d2e Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sat, 5 Apr 2025 00:27:43 +0300 Subject: [PATCH 104/154] Allow enchantments to be missing on equipped items --- apps/openmw/mwmechanics/activespells.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 3cee16f29f..9e64996e46 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -289,8 +289,9 @@ namespace MWMechanics const ESM::RefId& enchantmentId = slot->getClass().getEnchantment(*slot); if (enchantmentId.empty()) continue; - const ESM::Enchantment* enchantment = world->getStore().get().find(enchantmentId); - if (enchantment->mData.mType != ESM::Enchantment::ConstantEffect) + const ESM::Enchantment* enchantment + = world->getStore().get().search(enchantmentId); + if (enchantment == nullptr || enchantment->mData.mType != ESM::Enchantment::ConstantEffect) continue; if (std::find_if(mSpells.begin(), mSpells.end(), [&](const ActiveSpellParams& params) { From 15f4368fe6b25caa59e39e0b075c1148989c2db0 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sat, 5 Apr 2025 10:19:50 +0200 Subject: [PATCH 105/154] Account for creatures not having a model in more places --- CHANGELOG.md | 1 + apps/openmw/mwrender/actoranimation.cpp | 2 +- apps/openmw/mwrender/animation.cpp | 4 ++-- apps/openmw/mwrender/creatureanimation.cpp | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae65cbcc36..9f1b76ccc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -229,6 +229,7 @@ Bug #8299: Crash while smoothing landscape Bug #8364: Crash when clicking scrollbar without handle (divide by zero) Bug #8378: Korean bitmap fonts are unusable + Bug #8439: Creatures without models can crash the game Feature #1415: Infinite fall failsafe Feature #2566: Handle NAM9 records for manual cell references Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index f4bafdb48b..05c722f486 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -161,7 +161,7 @@ namespace MWRender bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const { - if (Settings::game().mShieldSheathing) + if (Settings::game().mShieldSheathing && mObjectRoot) { const MWWorld::Class& cls = mPtr.getClass(); MWMechanics::CreatureStats& stats = cls.getCreatureStats(mPtr); diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 0a2ef7bef8..f07a325f7c 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -1695,7 +1695,7 @@ namespace MWRender mGlowUpdater->setColor(color); mGlowUpdater->setDuration(glowDuration); } - else + else if (mObjectRoot) mGlowUpdater = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, color, glowDuration); } } @@ -1869,7 +1869,7 @@ namespace MWRender void Animation::setAlpha(float alpha) { - if (alpha == mAlpha) + if (alpha == mAlpha || !mObjectRoot) return; mAlpha = alpha; diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index f84e58e4bc..fb839cfa3a 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -259,7 +259,8 @@ namespace MWRender void CreatureWeaponAnimation::addControllers() { Animation::addControllers(); - WeaponAnimation::addControllers(mNodeMap, mActiveControllers, mObjectRoot.get()); + if (mObjectRoot) + WeaponAnimation::addControllers(mNodeMap, mActiveControllers, mObjectRoot.get()); } osg::Vec3f CreatureWeaponAnimation::runAnimation(float duration) From d609bd1ab1a5e42ae79a1e6f3044c464b0e820fd Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 30 Mar 2025 03:35:47 +0200 Subject: [PATCH 106/154] Fix clang-tidy header filter --- .clang-tidy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-tidy b/.clang-tidy index d630063315..567cc0b689 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -15,4 +15,4 @@ WarningsAsErrors: > -clang-analyzer-optin*, -clang-analyzer-cplusplus.NewDeleteLeaks, -clang-analyzer-core.CallAndMessage -HeaderFilterRegex: '^(apps|components)' +HeaderFilterRegex: '(apps|components)/' From da388c93eb5f1d0fdf17b3b5654ab6d429397d6b Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 30 Mar 2025 01:48:41 +0100 Subject: [PATCH 107/154] Remove boost-* clang-tidy checks There are only: * https://clang.llvm.org/extra/clang-tidy/checks/boost/use-ranges.html * https://clang.llvm.org/extra/clang-tidy/checks/boost/use-to-string.html None of them makes sense in this project. --- .clang-tidy | 2 -- 1 file changed, 2 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 567cc0b689..f4dc9cabc5 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,6 +1,5 @@ Checks: > -*, - boost-*, portability-*, clang-analyzer-*, -clang-analyzer-optin*, @@ -9,7 +8,6 @@ Checks: > -modernize-avoid-bind WarningsAsErrors: > -*, - boost-*, portability-*, clang-analyzer-*, -clang-analyzer-optin*, From 7c45a564a1e9791b418d2eec24e93d530028adfc Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 30 Mar 2025 12:32:27 +0200 Subject: [PATCH 108/154] Fix clang-analyzer-deadcode.DeadStores --- .../detournavigator/navmeshtilescache.cpp | 2 +- apps/benchmarks/esm/benchrefid.cpp | 20 ++++++++-------- apps/benchmarks/settings/access.cpp | 24 +++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/benchmarks/detournavigator/navmeshtilescache.cpp b/apps/benchmarks/detournavigator/navmeshtilescache.cpp index 26873d9a03..eabd757796 100644 --- a/apps/benchmarks/detournavigator/navmeshtilescache.cpp +++ b/apps/benchmarks/detournavigator/navmeshtilescache.cpp @@ -179,7 +179,7 @@ namespace generateKeys(std::back_inserter(keys), keys.size() * (100 - hitPercentage) / 100, random); std::size_t n = 0; - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { const auto& key = keys[n++ % keys.size()]; auto result = cache.get(key.mAgentBounds, key.mTilePosition, key.mRecastMesh); diff --git a/apps/benchmarks/esm/benchrefid.cpp b/apps/benchmarks/esm/benchrefid.cpp index b12f494ab9..3f38177ca7 100644 --- a/apps/benchmarks/esm/benchrefid.cpp +++ b/apps/benchmarks/esm/benchrefid.cpp @@ -104,7 +104,7 @@ namespace std::minstd_rand random; std::vector refIds = generateStringRefIds(state.range(0), random); std::size_t i = 0; - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(refIds[i].serialize()); if (++i >= refIds.size()) @@ -118,7 +118,7 @@ namespace std::vector serializedRefIds = generateSerializedStringRefIds(state.range(0), random, [](ESM::RefId v) { return v.serialize(); }); std::size_t i = 0; - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(ESM::RefId::deserialize(serializedRefIds[i])); if (++i >= serializedRefIds.size()) @@ -131,7 +131,7 @@ namespace std::minstd_rand random; std::vector refIds = generateStringRefIds(state.range(0), random); std::size_t i = 0; - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(refIds[i].serializeText()); if (++i >= refIds.size()) @@ -145,7 +145,7 @@ namespace std::vector serializedRefIds = generateSerializedStringRefIds(state.range(0), random, [](ESM::RefId v) { return v.serializeText(); }); std::size_t i = 0; - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(ESM::RefId::deserializeText(serializedRefIds[i])); if (++i >= serializedRefIds.size()) @@ -158,7 +158,7 @@ namespace std::minstd_rand random; std::vector refIds = generateGeneratedRefIds(random); std::size_t i = 0; - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(refIds[i].serializeText()); if (++i >= refIds.size()) @@ -172,7 +172,7 @@ namespace std::vector serializedRefIds = generateSerializedGeneratedRefIds(random, [](ESM::RefId v) { return v.serializeText(); }); std::size_t i = 0; - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(ESM::RefId::deserializeText(serializedRefIds[i])); if (++i >= serializedRefIds.size()) @@ -185,7 +185,7 @@ namespace std::minstd_rand random; std::vector refIds = generateIndexRefIds(random); std::size_t i = 0; - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(refIds[i].serializeText()); if (++i >= refIds.size()) @@ -199,7 +199,7 @@ namespace std::vector serializedRefIds = generateSerializedIndexRefIds(random, [](ESM::RefId v) { return v.serializeText(); }); std::size_t i = 0; - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(ESM::RefId::deserializeText(serializedRefIds[i])); if (++i >= serializedRefIds.size()) @@ -212,7 +212,7 @@ namespace std::minstd_rand random; std::vector refIds = generateESM3ExteriorCellRefIds(random); std::size_t i = 0; - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(refIds[i].serializeText()); if (++i >= refIds.size()) @@ -226,7 +226,7 @@ namespace std::vector serializedRefIds = generateSerializedESM3ExteriorCellRefIds(random, [](ESM::RefId v) { return v.serializeText(); }); std::size_t i = 0; - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(ESM::RefId::deserializeText(serializedRefIds[i])); if (++i >= serializedRefIds.size()) diff --git a/apps/benchmarks/settings/access.cpp b/apps/benchmarks/settings/access.cpp index 7660d0d55e..9e6999dd1f 100644 --- a/apps/benchmarks/settings/access.cpp +++ b/apps/benchmarks/settings/access.cpp @@ -9,7 +9,7 @@ namespace { void settingsManager(benchmark::State& state) { - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(Settings::Manager::getFloat("sky blending start", "Fog")); } @@ -17,7 +17,7 @@ namespace void settingsManager2(benchmark::State& state) { - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(Settings::Manager::getFloat("near clip", "Camera")); benchmark::DoNotOptimize(Settings::Manager::getBool("transparent postpass", "Post Processing")); @@ -26,7 +26,7 @@ namespace void settingsManager3(benchmark::State& state) { - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(Settings::Manager::getFloat("near clip", "Camera")); benchmark::DoNotOptimize(Settings::Manager::getBool("transparent postpass", "Post Processing")); @@ -36,7 +36,7 @@ namespace void localStatic(benchmark::State& state) { - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { static float v = Settings::Manager::getFloat("sky blending start", "Fog"); benchmark::DoNotOptimize(v); @@ -45,7 +45,7 @@ namespace void localStatic2(benchmark::State& state) { - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { static float v1 = Settings::Manager::getFloat("near clip", "Camera"); static bool v2 = Settings::Manager::getBool("transparent postpass", "Post Processing"); @@ -56,7 +56,7 @@ namespace void localStatic3(benchmark::State& state) { - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { static float v1 = Settings::Manager::getFloat("near clip", "Camera"); static bool v2 = Settings::Manager::getBool("transparent postpass", "Post Processing"); @@ -69,7 +69,7 @@ namespace void settingsStorage(benchmark::State& state) { - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { float v = Settings::fog().mSkyBlendingStart.get(); benchmark::DoNotOptimize(v); @@ -78,7 +78,7 @@ namespace void settingsStorage2(benchmark::State& state) { - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { bool v1 = Settings::postProcessing().mTransparentPostpass.get(); float v2 = Settings::camera().mNearClip.get(); @@ -89,7 +89,7 @@ namespace void settingsStorage3(benchmark::State& state) { - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { bool v1 = Settings::postProcessing().mTransparentPostpass.get(); float v2 = Settings::camera().mNearClip.get(); @@ -102,7 +102,7 @@ namespace void settingsStorageGet(benchmark::State& state) { - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(Settings::get("Fog", "sky blending start")); } @@ -110,7 +110,7 @@ namespace void settingsStorageGet2(benchmark::State& state) { - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(Settings::get("Post Processing", "transparent postpass")); benchmark::DoNotOptimize(Settings::get("Camera", "near clip")); @@ -119,7 +119,7 @@ namespace void settingsStorageGet3(benchmark::State& state) { - for (auto _ : state) + for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(Settings::get("Post Processing", "transparent postpass")); benchmark::DoNotOptimize(Settings::get("Camera", "near clip")); From e098770ba26322d37f9f00a53da53deaedc164f2 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 30 Mar 2025 18:13:31 +0200 Subject: [PATCH 109/154] Use custom clang-tidy config for extern/ --- extern/.clang-tidy | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 extern/.clang-tidy diff --git a/extern/.clang-tidy b/extern/.clang-tidy new file mode 100644 index 0000000000..bc766e0a9e --- /dev/null +++ b/extern/.clang-tidy @@ -0,0 +1,3 @@ +Checks: >- + -clang-analyzer-core.NullDereference, + -clang-analyzer-cplusplus.NewDelete From c34b0f90d7ef543c4b4efb66384852605b395236 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 30 Mar 2025 19:55:41 +0200 Subject: [PATCH 110/154] Avoid clang-tidy checks duplication --- .clang-tidy | 8 +------- CI/before_script.linux.sh | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index f4dc9cabc5..026d78fa12 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -6,11 +6,5 @@ Checks: > -clang-analyzer-cplusplus.NewDeleteLeaks, -clang-analyzer-core.CallAndMessage, -modernize-avoid-bind -WarningsAsErrors: > - -*, - portability-*, - clang-analyzer-*, - -clang-analyzer-optin*, - -clang-analyzer-cplusplus.NewDeleteLeaks, - -clang-analyzer-core.CallAndMessage +WarningsAsErrors: '*' HeaderFilterRegex: '(apps|components)/' diff --git a/CI/before_script.linux.sh b/CI/before_script.linux.sh index 2589c2807e..c6fd306e25 100755 --- a/CI/before_script.linux.sh +++ b/CI/before_script.linux.sh @@ -38,7 +38,7 @@ fi if [[ $CI_CLANG_TIDY ]]; then CMAKE_CONF_OPTS+=( - -DCMAKE_CXX_CLANG_TIDY="clang-tidy;--warnings-as-errors=*" + -DCMAKE_CXX_CLANG_TIDY=clang-tidy -DBUILD_COMPONENTS_TESTS=ON -DBUILD_OPENMW_TESTS=ON -DBUILD_OPENCS_TESTS=ON From 621a0a15a395af89c99440adeebb9590432aef16 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 30 Mar 2025 23:17:42 +0200 Subject: [PATCH 111/154] Disable clang-analyzer-cplusplus.NewDelete clang-tidy check --- .clang-tidy | 1 + 1 file changed, 1 insertion(+) diff --git a/.clang-tidy b/.clang-tidy index 026d78fa12..231a26f6e6 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -4,6 +4,7 @@ Checks: > clang-analyzer-*, -clang-analyzer-optin*, -clang-analyzer-cplusplus.NewDeleteLeaks, + -clang-analyzer-cplusplus.NewDelete, -clang-analyzer-core.CallAndMessage, -modernize-avoid-bind WarningsAsErrors: '*' From 3af2091b28984f0886f4284167ac423c935ea51e Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 30 Mar 2025 23:18:09 +0200 Subject: [PATCH 112/154] Use prefix with dot for clang-analyzer-optin. checks --- .clang-tidy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-tidy b/.clang-tidy index 231a26f6e6..855e550ac5 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -2,7 +2,7 @@ Checks: > -*, portability-*, clang-analyzer-*, - -clang-analyzer-optin*, + -clang-analyzer-optin.*, -clang-analyzer-cplusplus.NewDeleteLeaks, -clang-analyzer-cplusplus.NewDelete, -clang-analyzer-core.CallAndMessage, From 7254bb74a49b58a630f4682dcca813025cd63d00 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 30 Mar 2025 23:23:34 +0200 Subject: [PATCH 113/154] Enable modernize-avoid-bind clang-tidy check --- .clang-tidy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-tidy b/.clang-tidy index 855e550ac5..e70de76c7f 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -6,6 +6,6 @@ Checks: > -clang-analyzer-cplusplus.NewDeleteLeaks, -clang-analyzer-cplusplus.NewDelete, -clang-analyzer-core.CallAndMessage, - -modernize-avoid-bind + modernize-avoid-bind WarningsAsErrors: '*' HeaderFilterRegex: '(apps|components)/' From 12377465499100699f6148d6378992d4c39db226 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Sun, 6 Apr 2025 01:31:05 +0100 Subject: [PATCH 114/154] Be more careful when we tell Qt that data has changed Unchecking files only changes whether they're checked, and doesn't completely rearrange the table and change the number of elements it has, so we only need to change the check state, not the whole layout. It's way faster to just query all the data once after setting a content list than it is to query the data for all files between the old and new location of a file when we change any file's location in the load order. --- components/contentselector/model/contentmodel.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index fe26d37b97..d7222f92f3 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -725,7 +725,6 @@ void ContentSelectorModel::ContentModel::setContentList(const QStringList& fileL if (filePosition < previousPosition) { mFiles.move(filePosition, previousPosition); - emit dataChanged(index(filePosition, 0, QModelIndex()), index(previousPosition, 0, QModelIndex())); } else { @@ -734,6 +733,7 @@ void ContentSelectorModel::ContentModel::setContentList(const QStringList& fileL } } checkForLoadOrderErrors(); + emit dataChanged(index(0, 0), index(rowCount(), columnCount())); } void ContentSelectorModel::ContentModel::checkForLoadOrderErrors() @@ -900,7 +900,6 @@ ContentSelectorModel::ContentFileList ContentSelectorModel::ContentModel::checke void ContentSelectorModel::ContentModel::uncheckAll() { - emit layoutAboutToBeChanged(); mCheckedFiles.clear(); - emit layoutChanged(); + emit dataChanged(index(0, 0), index(rowCount(), columnCount()), { Qt::CheckStateRole, Qt::UserRole + 1 }); } From 7bad2864d9354cc611390f88dc0c96376e81f581 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Sun, 6 Apr 2025 02:40:42 +0100 Subject: [PATCH 115/154] Reuse QIcon This saves more than 15% of launcher startup time on my machine (after the prior improvements - it's way less without those) --- apps/launcher/datafilespage.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 9bb54fdb8e..36532a7d84 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -351,6 +351,8 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) if (!resourcesVfs.isEmpty()) directories.insert(0, { resourcesVfs }); + QIcon containsDataIcon(":/images/openmw-plugin.png"); + std::unordered_set visitedDirectories; for (const Config::SettingValue& currentDir : directories) { @@ -402,7 +404,7 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) // Add a "data file" icon if the directory contains a content file if (mSelector->containsDataFiles(currentDir.value)) { - item->setIcon(QIcon(":/images/openmw-plugin.png")); + item->setIcon(containsDataIcon); tooltip << tr("Contains content file(s)"); } From 973282e471154b7821ffbfaa6ad62d8a19668722 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Sun, 6 Apr 2025 02:45:28 +0100 Subject: [PATCH 116/154] Optimise ContentSelectorModel::ContentModel::item This saves about 5% of remaining launcher startup time Not using fileProperty avoids loads of QVariant conversions. --- components/contentselector/model/contentmodel.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index d7222f92f3..4ec7324e5d 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -78,14 +78,10 @@ ContentSelectorModel::EsmFile* ContentSelectorModel::ContentModel::item(int row) } const ContentSelectorModel::EsmFile* ContentSelectorModel::ContentModel::item(const QString& name) const { - EsmFile::FileProperty fp = EsmFile::FileProperty_FileName; - - if (name.contains('/')) - fp = EsmFile::FileProperty_FilePath; - + bool path = name.contains('/'); for (const EsmFile* file : mFiles) { - if (name.compare(file->fileProperty(fp).toString(), Qt::CaseInsensitive) == 0) + if (name.compare(path ? file->filePath() : file->fileName(), Qt::CaseInsensitive) == 0) return file; } return nullptr; From ed62f9b12bd3f5fe3971d798279f87fcca659bf6 Mon Sep 17 00:00:00 2001 From: Chronolegionnaire Date: Sun, 6 Apr 2025 06:27:40 +0000 Subject: [PATCH 117/154] Lua api demands a boolean for victim aware but crimes.lua looks for a number. Which makes scripts that call the crime interface unable to provide a value other than nil for victim aware. --- files/data/scripts/omw/crimes.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/data/scripts/omw/crimes.lua b/files/data/scripts/omw/crimes.lua index 58e043964b..827dd56bc0 100644 --- a/files/data/scripts/omw/crimes.lua +++ b/files/data/scripts/omw/crimes.lua @@ -42,7 +42,7 @@ return { "faction id passed to commitCrime must be a string or nil") assert(type(options.arg) == "number" or options.arg == nil, "arg value passed to commitCrime must be a number or nil") - assert(type(options.victimAware) == "number" or options.victimAware == nil, + assert(type(options.victimAware) == "boolean" or options.victimAware == nil, "victimAware value passed to commitCrime must be a boolean or nil") assert(options.type ~= nil, "crime type passed to commitCrime cannot be nil") From 962ef91e25fc0845fb6276961e42ed8268167892 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 6 Apr 2025 11:01:26 +0200 Subject: [PATCH 118/154] Allow skinned plants to be harvested --- components/nifosg/nifloader.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index d817ed2c9f..07eb342221 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -391,6 +391,7 @@ namespace NifOsg osg::ref_ptr skel = new SceneUtil::Skeleton; skel->setStateSet(created->getStateSet()); skel->setName(created->getName()); + skel->setUserDataContainer(created->getUserDataContainer()); for (unsigned int i = 0; i < created->getNumChildren(); ++i) skel->addChild(created->getChild(i)); created->removeChildren(0, created->getNumChildren()); From d826962eaaee0201dd136874846685ce7eff1b38 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 6 Apr 2025 11:02:31 +0200 Subject: [PATCH 119/154] Don't assume unresolved containers contain no visible items --- apps/openmw/mwclass/container.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index 8327904ecd..c8b1f05972 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -237,7 +237,12 @@ namespace MWClass bool Container::hasToolTip(const MWWorld::ConstPtr& ptr) const { if (const MWWorld::CustomData* data = ptr.getRefData().getCustomData()) - return !canBeHarvested(ptr) || data->asContainerCustomData().mStore.hasVisibleItems(); + { + if (!canBeHarvested(ptr)) + return true; + const MWWorld::ContainerStore& store = data->asContainerCustomData().mStore; + return !store.isResolved() || store.hasVisibleItems(); + } return true; } From 1667b11564a754cb1477c475e4562ab5236a01b1 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 6 Apr 2025 19:42:04 +0200 Subject: [PATCH 120/154] Pause menu video playback when OpenMW is minimized --- CHANGELOG.md | 1 + apps/openmw/mwbase/windowmanager.hpp | 2 +- apps/openmw/mwgui/mainmenu.cpp | 21 ++++++++++++++++++--- apps/openmw/mwgui/windowmanagerimp.cpp | 2 +- apps/openmw/mwgui/windowmanagerimp.hpp | 2 +- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f1b76ccc8..c74ff74397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -230,6 +230,7 @@ Bug #8364: Crash when clicking scrollbar without handle (divide by zero) Bug #8378: Korean bitmap fonts are unusable Bug #8439: Creatures without models can crash the game + Bug #8441: Freeze when using video main menu replacers Feature #1415: Infinite fall failsafe Feature #2566: Handle NAM9 records for manual cell references Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index df334bbfe8..8164501b4b 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -363,7 +363,7 @@ namespace MWBase void windowVisibilityChange(bool visible) override = 0; void windowResized(int x, int y) override = 0; void windowClosed() override = 0; - virtual bool isWindowVisible() = 0; + virtual bool isWindowVisible() const = 0; virtual void watchActor(const MWWorld::Ptr& ptr) = 0; virtual MWWorld::Ptr getWatchedActor() const = 0; diff --git a/apps/openmw/mwgui/mainmenu.cpp b/apps/openmw/mwgui/mainmenu.cpp index da747dd7a2..1b3619bd9f 100644 --- a/apps/openmw/mwgui/mainmenu.cpp +++ b/apps/openmw/mwgui/mainmenu.cpp @@ -29,11 +29,26 @@ namespace MWGui { Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(MWBase::Environment::get().getFrameRateLimit()); + const MWBase::WindowManager& windowManager = *MWBase::Environment::get().getWindowManager(); + bool paused = false; while (mRunning) { - // If finished playing, start again - if (!mVideo->update()) - mVideo->playVideo("video\\menu_background.bik"); + if (windowManager.isWindowVisible()) + { + if (paused) + { + mVideo->resume(); + paused = false; + } + // If finished playing, start again + if (!mVideo->update()) + mVideo->playVideo("video\\menu_background.bik"); + } + else if (!paused) + { + paused = true; + mVideo->pause(); + } frameRateLimiter.limit(); } } diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 322b38b7e6..565fb43127 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -1218,7 +1218,7 @@ namespace MWGui // TODO: check if any windows are now off-screen and move them back if so } - bool WindowManager::isWindowVisible() + bool WindowManager::isWindowVisible() const { return mWindowVisible; } diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 409a31a514..03902e21c4 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -290,7 +290,7 @@ namespace MWGui void windowVisibilityChange(bool visible) override; void windowResized(int x, int y) override; void windowClosed() override; - bool isWindowVisible() override; + bool isWindowVisible() const override; void watchActor(const MWWorld::Ptr& ptr) override; MWWorld::Ptr getWatchedActor() const override; From 8419116cae091e921ad7b1b403654cb365a98181 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 6 Apr 2025 04:14:39 +0300 Subject: [PATCH 121/154] Fix crash if ripple pipeline shaders are unavailable --- apps/openmw/mwrender/ripples.cpp | 58 +++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/apps/openmw/mwrender/ripples.cpp b/apps/openmw/mwrender/ripples.cpp index 309fc7b305..ab982d0c55 100644 --- a/apps/openmw/mwrender/ripples.cpp +++ b/apps/openmw/mwrender/ripples.cpp @@ -49,18 +49,6 @@ namespace MWRender && exts.glslLanguageVersion >= minimumGLVersionRequiredForCompute; #endif - static bool pipelineLogged = false; - - if (!pipelineLogged) - { - if (mUseCompute) - Log(Debug::Info) << "Initialized compute shader pipeline for water ripples"; - else - Log(Debug::Info) << "Initialized fallback fragment shader pipeline for water ripples"; - - pipelineLogged = true; - } - for (size_t i = 0; i < mState.size(); ++i) { osg::ref_ptr stateset = new osg::StateSet; @@ -98,6 +86,18 @@ namespace MWRender else setupFragmentPipeline(); + if (mProgramBlobber != nullptr) + { + static bool pipelineLogged = [&] { + if (mUseCompute) + Log(Debug::Info) << "Initialized compute shader pipeline for water ripples"; + else + Log(Debug::Info) << "Initialized fallback fragment shader pipeline for water ripples"; + return true; + }(); + (void)pipelineLogged; + } + setCullCallback(new osg::NodeCallback); setUpdateCallback(new osg::NodeCallback); } @@ -109,21 +109,36 @@ namespace MWRender Shader::ShaderManager::DefineMap defineMap = { { "rippleMapSize", std::to_string(sRTTSize) + ".0" } }; osg::ref_ptr vertex = shaderManager.getShader("fullscreen_tri.vert", {}, osg::Shader::VERTEX); + osg::ref_ptr blobber + = shaderManager.getShader("ripples_blobber.frag", defineMap, osg::Shader::FRAGMENT); + osg::ref_ptr simulate + = shaderManager.getShader("ripples_simulate.frag", defineMap, osg::Shader::FRAGMENT); + if (vertex == nullptr || blobber == nullptr || simulate == nullptr) + { + Log(Debug::Error) << "Failed to load shaders required for fragment shader ripple pipeline"; + return; + } - mProgramBlobber = shaderManager.getProgram( - vertex, shaderManager.getShader("ripples_blobber.frag", defineMap, osg::Shader::FRAGMENT)); - mProgramSimulation = shaderManager.getProgram( - std::move(vertex), shaderManager.getShader("ripples_simulate.frag", defineMap, osg::Shader::FRAGMENT)); + mProgramBlobber = shaderManager.getProgram(vertex, std::move(blobber)); + mProgramSimulation = shaderManager.getProgram(std::move(vertex), std::move(simulate)); } void RipplesSurface::setupComputePipeline() { auto& shaderManager = mResourceSystem->getSceneManager()->getShaderManager(); - mProgramBlobber = shaderManager.getProgram( - nullptr, shaderManager.getShader("core/ripples_blobber.comp", {}, osg::Shader::COMPUTE)); - mProgramSimulation = shaderManager.getProgram( - nullptr, shaderManager.getShader("core/ripples_simulate.comp", {}, osg::Shader::COMPUTE)); + osg::ref_ptr blobber + = shaderManager.getShader("core/ripples_blobber.comp", {}, osg::Shader::COMPUTE); + osg::ref_ptr simulate + = shaderManager.getShader("core/ripples_simulate.comp", {}, osg::Shader::COMPUTE); + if (blobber == nullptr || simulate == nullptr) + { + Log(Debug::Error) << "Failed to load shaders required for compute shader ripple pipeline"; + return; + } + + mProgramBlobber = shaderManager.getProgram(nullptr, std::move(blobber)); + mProgramSimulation = shaderManager.getProgram(nullptr, std::move(simulate)); } void RipplesSurface::updateState(const osg::FrameStamp& frameStamp, State& state) @@ -191,6 +206,9 @@ namespace MWRender void RipplesSurface::drawImplementation(osg::RenderInfo& renderInfo) const { + if (mProgramBlobber == nullptr || mProgramSimulation == nullptr) + return; + osg::State& state = *renderInfo.getState(); const std::size_t currentFrame = state.getFrameStamp()->getFrameNumber() % 2; const State& frameState = mState[currentFrame]; From e779f115efc5fd88ae3cbaca479c93ea9c998749 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Mon, 7 Apr 2025 16:11:27 +0100 Subject: [PATCH 122/154] Exclude directories from containsDataFiles Also include capo's microoptimisation even though it doesn't make things any faster. --- components/contentselector/model/contentmodel.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index 4ec7324e5d..9c161cf0ff 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -548,15 +549,13 @@ void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newf bool ContentSelectorModel::ContentModel::containsDataFiles(const QString& path) { - QDir dir(path); QStringList filters; filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; - dir.setNameFilters(filters); - - return dir.entryList().count() != 0; + QDirIterator it(path, filters, QDir::Files | QDir::NoDotAndDotDot); + return it.hasNext(); } void ContentSelectorModel::ContentModel::clearFiles() From d6b61f1f54b70b8e30b11e09c82c3181688d2dc8 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Tue, 8 Apr 2025 00:34:45 +0100 Subject: [PATCH 123/154] Sprinkle some const& QStringView required more fighting as loads of call sites take a const& --- components/contentselector/model/esmfile.hpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/contentselector/model/esmfile.hpp b/components/contentselector/model/esmfile.hpp index 28b4bd2822..d040dbd04d 100644 --- a/components/contentselector/model/esmfile.hpp +++ b/components/contentselector/model/esmfile.hpp @@ -48,18 +48,18 @@ namespace ContentSelectorModel void addGameFile(const QString& name) { mGameFiles.append(name); } QVariant fileProperty(const FileProperty prop) const; - QString fileName() const { return mFileName; } - QString author() const { return mAuthor; } + const QString& fileName() const { return mFileName; } + const QString& author() const { return mAuthor; } QDateTime modified() const { return mModified; } - QString formatVersion() const { return mVersion; } - QString filePath() const { return mPath; } + const QString& formatVersion() const { return mVersion; } + const QString& filePath() const { return mPath; } bool builtIn() const { return mBuiltIn; } bool fromAnotherConfigFile() const { return mFromAnotherConfigFile; } bool isMissing() const { return mPath.isEmpty(); } /// @note Contains file names, not paths. const QStringList& gameFiles() const { return mGameFiles; } - QString description() const { return mDescription; } + const QString& description() const { return mDescription; } QString toolTip() const { if (isMissing()) From 894ea4ba626914ee56f5763f1ec5b0ab02a27d54 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Tue, 8 Apr 2025 01:19:24 +0100 Subject: [PATCH 124/154] Don't precompute load order errors after every change It's much slower than doing it on demand as it only takes a microsecond, but for a really big load order, there are hundreds of thousands of intermediate calls before everything's set up and we can draw the GUI. --- .../contentselector/model/contentmodel.cpp | 32 ++++--------------- .../contentselector/model/contentmodel.hpp | 4 --- .../contentselector/view/contentselector.cpp | 2 -- 3 files changed, 7 insertions(+), 31 deletions(-) diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index 9c161cf0ff..5f0d41d38c 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -307,7 +307,6 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex& index, const { setCheckState(file->filePath(), success); emit dataChanged(index, index); - checkForLoadOrderErrors(); } else return success; @@ -422,7 +421,6 @@ bool ContentSelectorModel::ContentModel::dropMimeData( dataChanged(index(minRow, 0), index(maxRow, 0)); // at this point we know that drag and drop has finished. - checkForLoadOrderErrors(); return true; } @@ -702,12 +700,13 @@ void ContentSelectorModel::ContentModel::setNonUserContent(const QStringList& fi bool ContentSelectorModel::ContentModel::isLoadOrderError(const EsmFile* file) const { - return mPluginsWithLoadOrderError.contains(file->filePath()); + int index = indexFromItem(file).row(); + auto errors = checkForLoadOrderErrors(file, index); + return !errors.empty(); } void ContentSelectorModel::ContentModel::setContentList(const QStringList& fileList) { - mPluginsWithLoadOrderError.clear(); int previousPosition = -1; for (const QString& filepath : fileList) { @@ -727,27 +726,9 @@ void ContentSelectorModel::ContentModel::setContentList(const QStringList& fileL } } } - checkForLoadOrderErrors(); emit dataChanged(index(0, 0), index(rowCount(), columnCount())); } -void ContentSelectorModel::ContentModel::checkForLoadOrderErrors() -{ - for (int row = 0; row < mFiles.count(); ++row) - { - EsmFile* file = mFiles.at(row); - bool isRowInError = checkForLoadOrderErrors(file, row).count() != 0; - if (isRowInError) - { - mPluginsWithLoadOrderError.insert(file->filePath()); - } - else - { - mPluginsWithLoadOrderError.remove(file->filePath()); - } - } -} - QList ContentSelectorModel::ContentModel::checkForLoadOrderErrors( const EsmFile* file, int row) const { @@ -786,11 +767,12 @@ QList ContentSelectorModel::ContentModel:: QString ContentSelectorModel::ContentModel::toolTip(const EsmFile* file) const { - if (isLoadOrderError(file)) + int index = indexFromItem(file).row(); + auto errors = checkForLoadOrderErrors(file, index); + if (!errors.empty()) { QString text(""); - int index = indexFromItem(item(file->filePath())).row(); - for (const LoadOrderError& error : checkForLoadOrderErrors(file, index)) + for (const LoadOrderError& error : errors) { assert(error.errorCode() != LoadOrderError::ErrorCode::ErrorCode_None); diff --git a/components/contentselector/model/contentmodel.hpp b/components/contentselector/model/contentmodel.hpp index 467a9c032a..3eba939125 100644 --- a/components/contentselector/model/contentmodel.hpp +++ b/components/contentselector/model/contentmodel.hpp @@ -69,9 +69,6 @@ namespace ContentSelectorModel void refreshModel(); - /// Checks all plug-ins for load order errors and updates mPluginsWithLoadOrderError with plug-ins with issues - void checkForLoadOrderErrors(); - private: void addFile(EsmFile* file); @@ -89,7 +86,6 @@ namespace ContentSelectorModel QStringList mNonUserContent; std::set mCheckedFiles; QHash mNewFiles; - QSet mPluginsWithLoadOrderError; QString mEncoding; QIcon mWarningIcon; QIcon mErrorIcon; diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index bce136b335..0be6e7c023 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -211,7 +211,6 @@ void ContentSelectorView::ContentSelector::addFiles(const QString& path, bool ne ui->gameFileView->setCurrentIndex(0); mContentModel->uncheckAll(); - mContentModel->checkForLoadOrderErrors(); } void ContentSelectorView::ContentSelector::sortFiles() @@ -254,7 +253,6 @@ void ContentSelectorView::ContentSelector::slotCurrentGameFileIndexChanged(int i oldIndex = index; setGameFileSelected(index, true); - mContentModel->checkForLoadOrderErrors(); } emit signalCurrentGamefileIndexChanged(index); From 096759435a40f94f5098db9a379d20d12704efd4 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Wed, 9 Apr 2025 01:36:52 +0100 Subject: [PATCH 125/154] Add progress bars where the launcher can be limited by IO I tested this with a USB3 external hard drive. These two places were the only ones where we're IO-bound and block the main thread, so they're the only ones that need progress bars. If trying to replicate this test, then it's important to unplug the hard drive between each repeat. Apparently Windows is excellent at disk caching these days as it takes a minute and a half to start the launcher with Total Overhaul on this drive when it's just been plugged in, but less time than the first launch after a reboot on an NVME drive once the cache has been warmed up. --- apps/launcher/datafilespage.cpp | 7 +++++++ components/config/gamesettings.cpp | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 36532a7d84..16ece6d34c 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -353,9 +354,15 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) QIcon containsDataIcon(":/images/openmw-plugin.png"); + QProgressDialog progressBar("Adding data directories", {}, 0, directories.count(), this); + progressBar.setWindowModality(Qt::WindowModal); + progressBar.setValue(0); + std::unordered_set visitedDirectories; for (const Config::SettingValue& currentDir : directories) { + progressBar.setValue(progressBar.value() + 1); + if (!visitedDirectories.insert(currentDir.value).second) continue; diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index f318cec4a4..36373f8f35 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -1,6 +1,7 @@ #include "gamesettings.hpp" #include +#include #include #include @@ -37,8 +38,13 @@ void Config::GameSettings::validatePaths() mDataDirs.clear(); + QProgressDialog progressBar("Validating paths", {}, 0, paths.count() + 1); + progressBar.setWindowModality(Qt::WindowModal); + progressBar.setValue(0); + for (const auto& dataDir : paths) { + progressBar.setValue(progressBar.value() + 1); if (QDir(dataDir.value).exists()) { SettingValue copy = dataDir; @@ -50,6 +56,8 @@ void Config::GameSettings::validatePaths() // Do the same for data-local const QString& local = mSettings.value(QString("data-local")).value; + progressBar.setValue(progressBar.value() + 1); + if (!local.isEmpty() && QDir(local).exists()) { mDataLocal = QDir(local).canonicalPath(); From 15162a734d7c9a5a3aecb7fbda41041c394f78a5 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Thu, 10 Apr 2025 16:16:19 +0100 Subject: [PATCH 126/154] Avoid IO in resolveParentFileIndices In the olden days, we passed it a vector of open ESMReader instances, as they knew the filenames and sizes, so were a convenient source of this knowledge. When the ReadersCache was introduced as a pool of readers to limit the maximum number of simultaneously open file handles (to avoid going over the OS' limit) it was a poor substitute. * We iterate over all the earlier readers in order in a double loop, which is the worst case scenario for an LRU pool as once we're past the size limit, we're guaranteed maximum thrashing - the least recently used item is the most likely to be used next, so the worst to evict. * We didn't want to read any ESM files, just know whether they'd been read and what their sizes were, so didn't want to open a file handle, which the ReadersCache forced us to do. Obviously, opening lots of file handles isn't fast, and as this was an operation done for each content file which iterated over the file's masters and within that loop iterated over every loaded file, that's O(n^3) complexity in the worst case, and for things like delta plugin merged plugins, they hit the worst case in long load orders. This resolves the freeze reported as https://gitlab.com/OpenMW/openmw/-/issues/8425, but there may be other freezes on launch. --- components/esm3/esmreader.cpp | 6 ++-- components/esm3/readerscache.cpp | 54 ++++++++++++++++++++++++++++++++ components/esm3/readerscache.hpp | 5 +++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/components/esm3/esmreader.cpp b/components/esm3/esmreader.cpp index 4f69b8edef..adb0d1c103 100644 --- a/components/esm3/esmreader.cpp +++ b/components/esm3/esmreader.cpp @@ -73,10 +73,10 @@ namespace ESM int index = getIndex(); for (int i = 0; i < getIndex(); i++) { - const ESM::ReadersCache::BusyItem reader = readers.get(static_cast(i)); - if (reader->getFileSize() == 0) + if (readers.getFileSize(static_cast(i)) == 0) continue; // Content file in non-ESM format - const auto fnamecandidate = Files::pathToUnicodeString(reader->getName().filename()); + const auto fnamecandidate + = Files::pathToUnicodeString(readers.getName(static_cast(i)).filename()); if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) { index = i; diff --git a/components/esm3/readerscache.cpp b/components/esm3/readerscache.cpp index 732a2a22ba..a80236240e 100644 --- a/components/esm3/readerscache.cpp +++ b/components/esm3/readerscache.cpp @@ -47,6 +47,7 @@ namespace ESM { it->mReader.open(*it->mName); it->mName.reset(); + it->mFileSize.reset(); } mBusyItems.splice(mBusyItems.end(), mClosedItems, it); break; @@ -57,6 +58,58 @@ namespace ESM return BusyItem(*this, it); } + const std::filesystem::path& ReadersCache::getName(std::size_t index) const + { + const auto indexIt = mIndex.find(index); + if (indexIt == mIndex.end()) + throw std::logic_error("ESMReader at index " + std::to_string(index) + " has not been created yet"); + else + { + switch (indexIt->second->mState) + { + case State::Busy: + case State::Free: + return indexIt->second->mReader.getName(); + case State::Closed: + if (indexIt->second->mName) + return *indexIt->second->mName; + else + throw std::logic_error( + "ESMReader at index " + std::to_string(index) + " has forgotten its filename"); + default: + throw std::logic_error("ESMReader at index " + std::to_string(index) + " in unknown state"); + } + } + } + + std::size_t ReadersCache::getFileSize(std::size_t index) + { + const auto indexIt = mIndex.find(index); + if (indexIt == mIndex.end()) + return 0; + else + { + switch (indexIt->second->mState) + { + case State::Busy: + case State::Free: + if (!indexIt->second->mReader.getName().empty()) + return indexIt->second->mReader.getFileSize(); + else + throw std::logic_error( + "ESMReader at index " + std::to_string(index) + " has not been opened yet"); + case State::Closed: + if (indexIt->second->mFileSize) + return *indexIt->second->mFileSize; + else + throw std::logic_error( + "ESMReader at index " + std::to_string(index) + " has forgotten its file size"); + default: + throw std::logic_error("ESMReader at index " + std::to_string(index) + " in unknown state"); + } + } + } + void ReadersCache::closeExtraReaders() { while (!mFreeItems.empty() && mBusyItems.size() + mFreeItems.size() + 1 > mCapacity) @@ -65,6 +118,7 @@ namespace ESM if (it->mReader.isOpen()) { it->mName = it->mReader.getName(); + it->mFileSize = it->mReader.getFileSize(); it->mReader.close(); } mClosedItems.splice(mClosedItems.end(), mFreeItems, it); diff --git a/components/esm3/readerscache.hpp b/components/esm3/readerscache.hpp index 80ca5d1ef3..3f6ac01874 100644 --- a/components/esm3/readerscache.hpp +++ b/components/esm3/readerscache.hpp @@ -26,6 +26,7 @@ namespace ESM State mState = State::Busy; ESMReader mReader; std::optional mName; + std::optional mFileSize; Item() = default; }; @@ -55,6 +56,10 @@ namespace ESM BusyItem get(std::size_t index); + const std::filesystem::path& getName(std::size_t index) const; + + std::size_t getFileSize(std::size_t index); + void clear(); private: From 37dc1a6a76499cf334908573b818b374bf88738d Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Thu, 10 Apr 2025 16:51:23 +0100 Subject: [PATCH 127/154] Remove redundant elses --- components/esm3/readerscache.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/components/esm3/readerscache.cpp b/components/esm3/readerscache.cpp index a80236240e..6a8098865d 100644 --- a/components/esm3/readerscache.cpp +++ b/components/esm3/readerscache.cpp @@ -73,9 +73,8 @@ namespace ESM case State::Closed: if (indexIt->second->mName) return *indexIt->second->mName; - else - throw std::logic_error( - "ESMReader at index " + std::to_string(index) + " has forgotten its filename"); + throw std::logic_error( + "ESMReader at index " + std::to_string(index) + " has forgotten its filename"); default: throw std::logic_error("ESMReader at index " + std::to_string(index) + " in unknown state"); } @@ -95,15 +94,12 @@ namespace ESM case State::Free: if (!indexIt->second->mReader.getName().empty()) return indexIt->second->mReader.getFileSize(); - else - throw std::logic_error( - "ESMReader at index " + std::to_string(index) + " has not been opened yet"); + throw std::logic_error("ESMReader at index " + std::to_string(index) + " has not been opened yet"); case State::Closed: if (indexIt->second->mFileSize) return *indexIt->second->mFileSize; - else - throw std::logic_error( - "ESMReader at index " + std::to_string(index) + " has forgotten its file size"); + throw std::logic_error( + "ESMReader at index " + std::to_string(index) + " has forgotten its file size"); default: throw std::logic_error("ESMReader at index " + std::to_string(index) + " in unknown state"); } From 586467540b1d9bc907f892a04407dc0030ce7c71 Mon Sep 17 00:00:00 2001 From: Dave Corley Date: Thu, 3 Apr 2025 03:01:17 -0700 Subject: [PATCH 128/154] FIX: Clarify that `ignore` field of raycast options types are a list and not a single object --- files/lua_api/openmw/nearby.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/files/lua_api/openmw/nearby.lua b/files/lua_api/openmw/nearby.lua index ea1b8738bc..9b320db821 100644 --- a/files/lua_api/openmw/nearby.lua +++ b/files/lua_api/openmw/nearby.lua @@ -59,7 +59,7 @@ -- @field [parent=#nearby] #COLLISION_TYPE COLLISION_TYPE --- --- Result of raycasing +-- Result of raycasting -- @type RayCastingResult -- @field [parent=#RayCastingResult] #boolean hit Is there a collision? (true/false) -- @field [parent=#RayCastingResult] openmw.util#Vector3 hitPos Position of the collision point (nil if no collision) @@ -69,7 +69,7 @@ --- -- A table of parameters for @{#nearby.castRay} -- @type CastRayOptions --- @field openmw.core#GameObject ignore An object to ignore (specify here the source of the ray) +-- @field openmw.core#ObjectList ignore An array of objects to ignore (specify here the source of the ray, or other objects which should not collide) -- @field #number collisionType Object types to work with (see @{openmw.nearby#COLLISION_TYPE}) -- @field #number radius The radius of the ray (zero by default). If not zero then castRay actually casts a sphere with given radius. -- NOTE: currently `ignore` is not supported if `radius>0`. @@ -92,7 +92,7 @@ --- -- A table of parameters for @{#nearby.castRenderingRay} and @{#nearby.asyncCastRenderingRay} -- @type CastRenderingRayOptions --- @field #table ignore A list of @{openmw.core#GameObject} to ignore while doing the ray cast +-- @field openmw.core#ObjectList ignore A list of @{openmw.core#GameObject} to ignore while doing the ray cast --- -- Cast ray from one point to another and find the first visual intersection with anything in the scene. From 1d1ae1c906679a40b68c80a630103311d66a636a Mon Sep 17 00:00:00 2001 From: Dave Corley Date: Thu, 10 Apr 2025 10:13:42 -0700 Subject: [PATCH 129/154] CLEANUP: But it can also be a single object --- files/lua_api/openmw/nearby.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/files/lua_api/openmw/nearby.lua b/files/lua_api/openmw/nearby.lua index 9b320db821..2528ed8f3b 100644 --- a/files/lua_api/openmw/nearby.lua +++ b/files/lua_api/openmw/nearby.lua @@ -69,7 +69,7 @@ --- -- A table of parameters for @{#nearby.castRay} -- @type CastRayOptions --- @field openmw.core#ObjectList ignore An array of objects to ignore (specify here the source of the ray, or other objects which should not collide) +-- @field #any ignore An @{openmw.core#GameObject} or @{openmw.core#ObjectList} to ignore (specify here the source of the ray, or other objects which should not collide) -- @field #number collisionType Object types to work with (see @{openmw.nearby#COLLISION_TYPE}) -- @field #number radius The radius of the ray (zero by default). If not zero then castRay actually casts a sphere with given radius. -- NOTE: currently `ignore` is not supported if `radius>0`. @@ -92,7 +92,7 @@ --- -- A table of parameters for @{#nearby.castRenderingRay} and @{#nearby.asyncCastRenderingRay} -- @type CastRenderingRayOptions --- @field openmw.core#ObjectList ignore A list of @{openmw.core#GameObject} to ignore while doing the ray cast +-- @field #any ignore A @{openmw.core#GameObject} or @{openmw.core#ObjectList} to ignore while doing the ray cast --- -- Cast ray from one point to another and find the first visual intersection with anything in the scene. From 48572e4c9633b340f90b42f4ed8f0d01158ff975 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Thu, 10 Apr 2025 18:32:52 +0100 Subject: [PATCH 130/154] Even more elses --- components/esm3/readerscache.cpp | 52 ++++++++++++++------------------ 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/components/esm3/readerscache.cpp b/components/esm3/readerscache.cpp index 6a8098865d..34ceff5ffd 100644 --- a/components/esm3/readerscache.cpp +++ b/components/esm3/readerscache.cpp @@ -63,21 +63,17 @@ namespace ESM const auto indexIt = mIndex.find(index); if (indexIt == mIndex.end()) throw std::logic_error("ESMReader at index " + std::to_string(index) + " has not been created yet"); - else + switch (indexIt->second->mState) { - switch (indexIt->second->mState) - { - case State::Busy: - case State::Free: - return indexIt->second->mReader.getName(); - case State::Closed: - if (indexIt->second->mName) - return *indexIt->second->mName; - throw std::logic_error( - "ESMReader at index " + std::to_string(index) + " has forgotten its filename"); - default: - throw std::logic_error("ESMReader at index " + std::to_string(index) + " in unknown state"); - } + case State::Busy: + case State::Free: + return indexIt->second->mReader.getName(); + case State::Closed: + if (indexIt->second->mName) + return *indexIt->second->mName; + throw std::logic_error("ESMReader at index " + std::to_string(index) + " has forgotten its filename"); + default: + throw std::logic_error("ESMReader at index " + std::to_string(index) + " in unknown state"); } } @@ -86,23 +82,19 @@ namespace ESM const auto indexIt = mIndex.find(index); if (indexIt == mIndex.end()) return 0; - else + switch (indexIt->second->mState) { - switch (indexIt->second->mState) - { - case State::Busy: - case State::Free: - if (!indexIt->second->mReader.getName().empty()) - return indexIt->second->mReader.getFileSize(); - throw std::logic_error("ESMReader at index " + std::to_string(index) + " has not been opened yet"); - case State::Closed: - if (indexIt->second->mFileSize) - return *indexIt->second->mFileSize; - throw std::logic_error( - "ESMReader at index " + std::to_string(index) + " has forgotten its file size"); - default: - throw std::logic_error("ESMReader at index " + std::to_string(index) + " in unknown state"); - } + case State::Busy: + case State::Free: + if (!indexIt->second->mReader.getName().empty()) + return indexIt->second->mReader.getFileSize(); + throw std::logic_error("ESMReader at index " + std::to_string(index) + " has not been opened yet"); + case State::Closed: + if (indexIt->second->mFileSize) + return *indexIt->second->mFileSize; + throw std::logic_error("ESMReader at index " + std::to_string(index) + " has forgotten its file size"); + default: + throw std::logic_error("ESMReader at index " + std::to_string(index) + " in unknown state"); } } From e512a8e74fca6e1092356d61591a33c379b051f9 Mon Sep 17 00:00:00 2001 From: Dave Corley Date: Thu, 10 Apr 2025 11:42:24 -0700 Subject: [PATCH 131/154] FIX: Add a name to options table in castRenderingRay --- files/lua_api/openmw/nearby.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/lua_api/openmw/nearby.lua b/files/lua_api/openmw/nearby.lua index 2528ed8f3b..f2a3dbbc37 100644 --- a/files/lua_api/openmw/nearby.lua +++ b/files/lua_api/openmw/nearby.lua @@ -102,7 +102,7 @@ -- @function [parent=#nearby] castRenderingRay -- @param openmw.util#Vector3 from Start point of the ray. -- @param openmw.util#Vector3 to End point of the ray. --- @param #CastRenderingRayOptions +-- @param #CastRenderingRayOptions options An optional table with additional optional arguments -- @return #RayCastingResult --- From b68935e9173ad997fe6490046796cff839ba72ec Mon Sep 17 00:00:00 2001 From: Dave Corley Date: Thu, 10 Apr 2025 11:44:34 -0700 Subject: [PATCH 132/154] FIX: Model param of addVfx should not be a header --- files/lua_api/openmw/animation.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/lua_api/openmw/animation.lua b/files/lua_api/openmw/animation.lua index 46f3ff20fc..e0a30ac6e8 100644 --- a/files/lua_api/openmw/animation.lua +++ b/files/lua_api/openmw/animation.lua @@ -221,7 +221,7 @@ -- Can be used only in local scripts on self. Can also be evoked by sending an AddVfx event to the target actor. -- @function [parent=#animation] addVfx -- @param openmw.core#GameObject actor --- @param #string model #string model path (normally taken from a record such as @{openmw.types#StaticRecord.model} or similar) +-- @param #string model path (normally taken from a record such as @{openmw.types#StaticRecord.model} or similar) -- @param #table options optional table of parameters. Can contain: -- -- * `loop` - boolean, if true the effect will loop until removed (default: 0). From 22172f3b0e0419c5fbb4b893a9713f7ed6007e37 Mon Sep 17 00:00:00 2001 From: Dave Corley Date: Thu, 10 Apr 2025 11:48:57 -0700 Subject: [PATCH 133/154] FIX: useAmbientLighting arg of addVfx options is a bool --- files/lua_api/openmw/animation.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/lua_api/openmw/animation.lua b/files/lua_api/openmw/animation.lua index e0a30ac6e8..b5bf50d446 100644 --- a/files/lua_api/openmw/animation.lua +++ b/files/lua_api/openmw/animation.lua @@ -228,7 +228,7 @@ -- * `boneName` - name of the bone to attach the vfx to. (default: "") -- * `particleTextureOverride` - name of the particle texture to use. (default: "") -- * `vfxId` - a string ID that can be used to remove the effect later, using #removeVfx, and to avoid duplicate effects. The default value of "" can have duplicates. To avoid interaction with the engine, use unique identifiers unrelated to magic effect IDs. The engine uses this identifier to add and remove magic effects based on what effects are active on the actor. If this is set equal to the @{openmw.core#MagicEffectId} identifier of the magic effect being added, for example core.magic.EFFECT_TYPE.FireDamage, then the engine will remove it once the fire damage effect on the actor reaches 0. (Default: ""). --- * `useAmbientLighting` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: 1) +-- * `useAmbientLighting` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: true) -- -- @usage local mgef = core.magic.effects.records[myEffectName] -- anim.addVfx(self, 'VFX_Hands', {boneName = 'Bip01 L Hand', particleTextureOverride = mgef.particle, loop = mgef.continuousVfx, vfxId = mgef.id..'_myuniquenamehere'}) From 31fcc5e126ca2d8dd73098dcef0a232b64df05ab Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Fri, 11 Apr 2025 17:30:56 +0100 Subject: [PATCH 134/154] Add test for new ReadersCache functions --- apps/components_tests/esm3/readerscache.cpp | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/apps/components_tests/esm3/readerscache.cpp b/apps/components_tests/esm3/readerscache.cpp index f222a29bf8..1cb9a85fb6 100644 --- a/apps/components_tests/esm3/readerscache.cpp +++ b/apps/components_tests/esm3/readerscache.cpp @@ -88,4 +88,31 @@ namespace EXPECT_EQ(reader->getFileOffset(), sInitialOffset); } } + + TEST_F(ESM3ReadersCacheWithContentFile, CachedSizeAndName) + { + ESM::ReadersCache readers(2); + { + readers.get(0)->openRaw(std::make_unique("123"), "closed0.omwaddon"); + readers.get(1)->openRaw(std::make_unique("12345"), "closed1.omwaddon"); + readers.get(2)->openRaw(std::make_unique("1234567"), "free.omwaddon"); + } + auto busy = readers.get(3); + busy->openRaw(std::make_unique("123456789"), "busy.omwaddon"); + + EXPECT_EQ(readers.getFileSize(0), 3); + EXPECT_EQ(readers.getName(0), "closed0.omwaddon"); + + EXPECT_EQ(readers.getFileSize(1), 5); + EXPECT_EQ(readers.getName(1), "closed1.omwaddon"); + + EXPECT_EQ(readers.getFileSize(2), 7); + EXPECT_EQ(readers.getName(2), "free.omwaddon"); + + EXPECT_EQ(readers.getFileSize(3), 9); + EXPECT_EQ(readers.getName(3), "busy.omwaddon"); + + // not-yet-seen indices give zero for their size + EXPECT_EQ(readers.getFileSize(4), 0); + } } From 396cd1c7275125678e2b34e478861366ae86ea7f Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Fri, 11 Apr 2025 17:33:19 +0100 Subject: [PATCH 135/154] Fix Windows Debug build This was a regression from https://gitlab.com/OpenMW/openmw/-/merge_requests/4596 Also move more things into the anonymous namespace because there's not really a reason not to and I had to rearrange things anyway. --- components/debug/debugging.cpp | 194 ++++++++++++++++----------------- 1 file changed, 97 insertions(+), 97 deletions(-) diff --git a/components/debug/debugging.cpp b/components/debug/debugging.cpp index 6006e1abe5..6936dfe008 100644 --- a/components/debug/debugging.cpp +++ b/components/debug/debugging.cpp @@ -106,94 +106,94 @@ namespace Debug logListener = std::move(listener); } - class DebugOutputBase : public boost::iostreams::sink - { - public: - virtual std::streamsize write(const char* str, std::streamsize size) - { - if (size <= 0) - return size; - std::string_view msg{ str, static_cast(size) }; - - // Skip debug level marker - Level level = All; - if (Log::sWriteLevel) - { - level = getLevelMarker(msg[0]); - msg = msg.substr(1); - } - - char prefix[32]; - std::size_t prefixSize; - { - prefix[0] = '['; - const auto now = std::chrono::system_clock::now(); - const auto time = std::chrono::system_clock::to_time_t(now); - tm time_info{}; -#ifdef _WIN32 - (void)localtime_s(&time_info, &time); -#else - (void)localtime_r(&time, &time_info); -#endif - prefixSize = std::strftime(prefix + 1, sizeof(prefix) - 1, "%T", &time_info) + 1; - char levelLetter = " EWIVD*"[int(level)]; - const auto ms = std::chrono::duration_cast(now.time_since_epoch()).count(); - prefixSize += snprintf(prefix + prefixSize, sizeof(prefix) - prefixSize, ".%03u %c] ", - static_cast(ms % 1000), levelLetter); - } - - while (!msg.empty()) - { - if (msg[0] == 0) - break; - size_t lineSize = 1; - while (lineSize < msg.size() && msg[lineSize - 1] != '\n') - lineSize++; - writeImpl(prefix, prefixSize, level); - writeImpl(msg.data(), lineSize, level); - if (logListener) - logListener(level, std::string_view(prefix, prefixSize), std::string_view(msg.data(), lineSize)); - msg = msg.substr(lineSize); - } - - return size; - } - - virtual ~DebugOutputBase() = default; - - protected: - static Level getLevelMarker(char marker) - { - if (0 <= marker && static_cast(marker) < static_cast(All)) - return static_cast(marker); - return All; - } - - virtual std::streamsize writeImpl(const char* str, std::streamsize size, Level debugLevel) - { - return size; - } - }; - -#if defined _WIN32 && defined _DEBUG - class DebugOutput : public DebugOutputBase - { - public: - std::streamsize writeImpl(const char* str, std::streamsize size, Level debugLevel) - { - // Make a copy for null termination - std::string tmp(str, static_cast(size)); - // Write string to Visual Studio Debug output - OutputDebugString(tmp.c_str()); - return size; - } - - virtual ~DebugOutput() = default; - }; -#else - namespace { + class DebugOutputBase : public boost::iostreams::sink + { + public: + virtual std::streamsize write(const char* str, std::streamsize size) + { + if (size <= 0) + return size; + std::string_view msg{ str, static_cast(size) }; + + // Skip debug level marker + Level level = All; + if (Log::sWriteLevel) + { + level = getLevelMarker(msg[0]); + msg = msg.substr(1); + } + + char prefix[32]; + std::size_t prefixSize; + { + prefix[0] = '['; + const auto now = std::chrono::system_clock::now(); + const auto time = std::chrono::system_clock::to_time_t(now); + tm time_info{}; +#ifdef _WIN32 + (void)localtime_s(&time_info, &time); +#else + (void)localtime_r(&time, &time_info); +#endif + prefixSize = std::strftime(prefix + 1, sizeof(prefix) - 1, "%T", &time_info) + 1; + char levelLetter = " EWIVD*"[int(level)]; + const auto ms = std::chrono::duration_cast(now.time_since_epoch()).count(); + prefixSize += snprintf(prefix + prefixSize, sizeof(prefix) - prefixSize, ".%03u %c] ", + static_cast(ms % 1000), levelLetter); + } + + while (!msg.empty()) + { + if (msg[0] == 0) + break; + size_t lineSize = 1; + while (lineSize < msg.size() && msg[lineSize - 1] != '\n') + lineSize++; + writeImpl(prefix, prefixSize, level); + writeImpl(msg.data(), lineSize, level); + if (logListener) + logListener(level, std::string_view(prefix, prefixSize), std::string_view(msg.data(), lineSize)); + msg = msg.substr(lineSize); + } + + return size; + } + + virtual ~DebugOutputBase() = default; + + protected: + static Level getLevelMarker(char marker) + { + if (0 <= marker && static_cast(marker) < static_cast(All)) + return static_cast(marker); + return All; + } + + virtual std::streamsize writeImpl(const char* str, std::streamsize size, Level debugLevel) + { + return size; + } + }; + +#if defined _WIN32 && defined _DEBUG + class DebugOutput : public DebugOutputBase + { + public: + std::streamsize writeImpl(const char* str, std::streamsize size, Level debugLevel) + { + // Make a copy for null termination + std::string tmp(str, static_cast(size)); + // Write string to Visual Studio Debug output + OutputDebugString(tmp.c_str()); + return size; + } + + virtual ~DebugOutput() = default; + }; +#else + struct Record { std::string mValue; @@ -324,6 +324,7 @@ namespace Debug First mFirst; Second mSecond; }; +#endif Level toLevel(std::string_view value) { @@ -340,22 +341,21 @@ namespace Debug return Verbose; } - } -#endif - static std::unique_ptr rawStdout = nullptr; - static std::unique_ptr rawStderr = nullptr; - static std::unique_ptr rawStderrMutex = nullptr; - static std::ofstream logfile; + static std::unique_ptr rawStdout = nullptr; + static std::unique_ptr rawStderr = nullptr; + static std::unique_ptr rawStderrMutex = nullptr; + static std::ofstream logfile; #if defined(_WIN32) && defined(_DEBUG) - static boost::iostreams::stream_buffer sb; + static boost::iostreams::stream_buffer sb; #else - static boost::iostreams::stream_buffer> standardOut; - static boost::iostreams::stream_buffer> standardErr; - static boost::iostreams::stream_buffer> bufferedOut; - static boost::iostreams::stream_buffer> bufferedErr; + static boost::iostreams::stream_buffer> standardOut; + static boost::iostreams::stream_buffer> standardErr; + static boost::iostreams::stream_buffer> bufferedOut; + static boost::iostreams::stream_buffer> bufferedErr; #endif + } std::ostream& getRawStdout() { From d74a0edb82cba81ca546f5a67c0e6a5dfafa66da Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Fri, 11 Apr 2025 17:37:55 +0100 Subject: [PATCH 136/154] Format --- components/debug/debugging.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/components/debug/debugging.cpp b/components/debug/debugging.cpp index 6936dfe008..103efdbc88 100644 --- a/components/debug/debugging.cpp +++ b/components/debug/debugging.cpp @@ -139,7 +139,8 @@ namespace Debug #endif prefixSize = std::strftime(prefix + 1, sizeof(prefix) - 1, "%T", &time_info) + 1; char levelLetter = " EWIVD*"[int(level)]; - const auto ms = std::chrono::duration_cast(now.time_since_epoch()).count(); + const auto ms + = std::chrono::duration_cast(now.time_since_epoch()).count(); prefixSize += snprintf(prefix + prefixSize, sizeof(prefix) - prefixSize, ".%03u %c] ", static_cast(ms % 1000), levelLetter); } @@ -154,7 +155,8 @@ namespace Debug writeImpl(prefix, prefixSize, level); writeImpl(msg.data(), lineSize, level); if (logListener) - logListener(level, std::string_view(prefix, prefixSize), std::string_view(msg.data(), lineSize)); + logListener( + level, std::string_view(prefix, prefixSize), std::string_view(msg.data(), lineSize)); msg = msg.substr(lineSize); } @@ -171,10 +173,7 @@ namespace Debug return All; } - virtual std::streamsize writeImpl(const char* str, std::streamsize size, Level debugLevel) - { - return size; - } + virtual std::streamsize writeImpl(const char* str, std::streamsize size, Level debugLevel) { return size; } }; #if defined _WIN32 && defined _DEBUG From a5a6f33578f521661574dc548316796af407e85b Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Fri, 11 Apr 2025 17:41:40 +0100 Subject: [PATCH 137/154] Manual reformatting that wasn't done automatically on my machine --- components/debug/debugging.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/debug/debugging.cpp b/components/debug/debugging.cpp index 103efdbc88..2ba7b2072e 100644 --- a/components/debug/debugging.cpp +++ b/components/debug/debugging.cpp @@ -173,7 +173,10 @@ namespace Debug return All; } - virtual std::streamsize writeImpl(const char* str, std::streamsize size, Level debugLevel) { return size; } + virtual std::streamsize writeImpl(const char* str, std::streamsize size, Level debugLevel) + { + return size; + } }; #if defined _WIN32 && defined _DEBUG From deb070389f82d9e9f30d24615d88ebd7db3a348d Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Sat, 12 Apr 2025 18:38:55 +0100 Subject: [PATCH 138/154] Improve Windows crash/freeze catcher UX * Change crash log to crash dump in messages. * Make the freeze catcher popup disappear more quickly when OpenMW thaws - we got a few freeze dumps from after a thaw. * Improve freeze catcher message - hopefully fewer users think it's a false positive they're expected to put up with and we get future reports sooner. --- .../crashcatcher/windows_crashcatcher.cpp | 2 +- .../crashcatcher/windows_crashcatcher.hpp | 1 + .../crashcatcher/windows_crashmonitor.cpp | 17 +++++++++-------- .../crashcatcher/windows_crashmonitor.hpp | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/components/crashcatcher/windows_crashcatcher.cpp b/components/crashcatcher/windows_crashcatcher.cpp index 23dfac549c..d9ef00ef77 100644 --- a/components/crashcatcher/windows_crashcatcher.cpp +++ b/components/crashcatcher/windows_crashcatcher.cpp @@ -237,7 +237,7 @@ namespace Crash // must remain until monitor has finished waitMonitor(); - std::string message = "OpenMW has encountered a fatal error.\nCrash log saved to '" + std::string message = "OpenMW has encountered a fatal error.\nCrash dump saved to '" + Misc::StringUtils::u8StringToString(getCrashDumpPath(*mShm).u8string()) + "'.\nPlease report this to https://gitlab.com/OpenMW/openmw/issues !"; SDL_ShowSimpleMessageBox(0, "Fatal Error", message.c_str(), nullptr); diff --git a/components/crashcatcher/windows_crashcatcher.hpp b/components/crashcatcher/windows_crashcatcher.hpp index 89678c9ada..bcf1ed688d 100644 --- a/components/crashcatcher/windows_crashcatcher.hpp +++ b/components/crashcatcher/windows_crashcatcher.hpp @@ -21,6 +21,7 @@ namespace Crash // the main openmw process in task manager. static constexpr const int CrashCatcherTimeout = 2500; + static constexpr const int CrashCatcherThawTimeout = 250; struct CrashSHM; diff --git a/components/crashcatcher/windows_crashmonitor.cpp b/components/crashcatcher/windows_crashmonitor.cpp index 3708283efa..8398528ec9 100644 --- a/components/crashcatcher/windows_crashmonitor.cpp +++ b/components/crashcatcher/windows_crashmonitor.cpp @@ -87,9 +87,10 @@ namespace Crash SetEvent(mSignalAppEvent); } - bool CrashMonitor::waitApp() const + bool CrashMonitor::waitApp(bool thawMode) const { - return WaitForSingleObject(mSignalMonitorEvent, CrashCatcherTimeout) == WAIT_OBJECT_0; + return WaitForSingleObject(mSignalMonitorEvent, thawMode ? CrashCatcherThawTimeout : CrashCatcherTimeout) + == WAIT_OBJECT_0; } bool CrashMonitor::isAppAlive() const @@ -185,7 +186,7 @@ namespace Crash frozen = false; } - if (!mFreezeAbort && waitApp()) + if (!mFreezeAbort && waitApp(frozen)) { shmLock(); @@ -215,7 +216,7 @@ namespace Crash { handleCrash(true); TerminateProcess(mAppProcessHandle, 0xDEAD); - std::string message = "OpenMW appears to have frozen.\nCrash log saved to '" + std::string message = "OpenMW has frozen.\nCrash dump saved to '" + Misc::StringUtils::u8StringToString(getFreezeDumpPath(*mShm).u8string()) + "'.\nPlease report this to https://gitlab.com/OpenMW/openmw/issues !"; SDL_ShowSimpleMessageBox(0, "Fatal Error", message.c_str(), nullptr); @@ -289,10 +290,10 @@ namespace Crash { std::thread messageBoxThread([&]() { SDL_MessageBoxButtonData button = { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 0, "Abort" }; - SDL_MessageBoxData messageBoxData = { SDL_MESSAGEBOX_ERROR, nullptr, "OpenMW appears to have frozen", - "OpenMW appears to have frozen. Press Abort to terminate it and generate a crash dump.\nIf OpenMW " - "hasn't actually frozen, this message box will disappear a within a few seconds of it becoming " - "responsive.", + SDL_MessageBoxData messageBoxData = { SDL_MESSAGEBOX_ERROR, nullptr, "OpenMW has frozen", + "OpenMW has frozen. This should never happen. Press Abort to terminate it and generate a crash dump to " + "help diagnose the problem.\nOpenMW may unfreeze if you wait, and this message box will disappear " + "after it becomes responsive.", 1, &button, nullptr }; int buttonId; diff --git a/components/crashcatcher/windows_crashmonitor.hpp b/components/crashcatcher/windows_crashmonitor.hpp index 16e173169e..25ee710fd3 100644 --- a/components/crashcatcher/windows_crashmonitor.hpp +++ b/components/crashcatcher/windows_crashmonitor.hpp @@ -41,7 +41,7 @@ namespace Crash void signalApp() const; - bool waitApp() const; + bool waitApp(bool thawMode) const; bool isAppAlive() const; From 84f471ce5c6f29a50c0df1d30c3283dbd4304922 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 30 Mar 2025 20:34:35 +0200 Subject: [PATCH 139/154] Enable identifier naming clang-tidy check --- .clang-tidy | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.clang-tidy b/.clang-tidy index e70de76c7f..92500ad04d 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -6,6 +6,10 @@ Checks: > -clang-analyzer-cplusplus.NewDeleteLeaks, -clang-analyzer-cplusplus.NewDelete, -clang-analyzer-core.CallAndMessage, - modernize-avoid-bind + modernize-avoid-bind, + readability-identifier-naming WarningsAsErrors: '*' HeaderFilterRegex: '(apps|components)/' +CheckOptions: +- key: readability-identifier-naming.ConceptCase + value: CamelCase From 6aed2d82843ccfc95d3729d72419e09219150245 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 13 Apr 2025 14:05:07 +0300 Subject: [PATCH 140/154] Bump Crimes interface version --- files/data/scripts/omw/crimes.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/data/scripts/omw/crimes.lua b/files/data/scripts/omw/crimes.lua index 827dd56bc0..8539471973 100644 --- a/files/data/scripts/omw/crimes.lua +++ b/files/data/scripts/omw/crimes.lua @@ -24,7 +24,7 @@ return { interface = { --- Interface version -- @field [parent=#Crimes] #number version - version = 1, + version = 2, --- -- Commits a crime as if done through an in-game action. Can only be used in global context. From 0d5e9ef85ff69e98b8e69336f7d4937e1adc1a87 Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 18 Apr 2025 12:26:23 +0200 Subject: [PATCH 141/154] Remove redundant using namespace Fallback C++ has ADL to find overloads. using namespace does nothing in this case. --- apps/bulletobjecttool/main.cpp | 5 ++--- apps/navmeshtool/main.cpp | 2 -- apps/opencs/editor.cpp | 9 +++++---- apps/openmw/main.cpp | 4 +--- components/fallback/validate.hpp | 2 -- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/apps/bulletobjecttool/main.cpp b/apps/bulletobjecttool/main.cpp index 190eb3364d..5cb275a53d 100644 --- a/apps/bulletobjecttool/main.cpp +++ b/apps/bulletobjecttool/main.cpp @@ -53,8 +53,6 @@ namespace bpo::options_description makeOptionsDescription() { - using Fallback::FallbackMap; - bpo::options_description result; auto addOption = result.add_options(); addOption("help", "print help message"); @@ -87,7 +85,8 @@ namespace "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" "\n\twin1252 - Western European (Latin) alphabet, used by default"); - addOption("fallback", bpo::value()->default_value(FallbackMap(), "")->multitoken()->composing(), + addOption("fallback", + bpo::value()->default_value(Fallback::FallbackMap(), "")->multitoken()->composing(), "fallback values"); Files::ConfigurationManager::addCommonOptions(result); diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp index a05babfbe8..e148e60d54 100644 --- a/apps/navmeshtool/main.cpp +++ b/apps/navmeshtool/main.cpp @@ -62,8 +62,6 @@ namespace NavMeshTool bpo::options_description makeOptionsDescription() { - using Fallback::FallbackMap; - bpo::options_description result; auto addOption = result.add_options(); addOption("help", "print help message"); diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 4cab88e5f2..8781f54154 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -38,8 +38,6 @@ #include "view/doc/viewmanager.hpp" -using namespace Fallback; - CS::Editor::Editor(int argc, char** argv) : mConfigVariables(readConfiguration()) , mSettingsState(mCfgMgr) @@ -124,7 +122,10 @@ boost::program_options::variables_map CS::Editor::readConfiguration() ->default_value(std::vector(), "fallback-archive") ->multitoken()); addOption("fallback", - boost::program_options::value()->default_value(FallbackMap(), "")->multitoken()->composing(), + boost::program_options::value() + ->default_value(Fallback::FallbackMap(), "") + ->multitoken() + ->composing(), "fallback values"); Files::ConfigurationManager::addCommonOptions(desc); @@ -141,7 +142,7 @@ std::pair> CS::Editor::readConfig { boost::program_options::variables_map& variables = mConfigVariables; - Fallback::Map::init(variables["fallback"].as().mMap); + Fallback::Map::init(variables["fallback"].as().mMap); mEncodingName = variables["encoding"].as(); mDocumentManager.setEncoding(ToUTF8::calculateEncoding(mEncodingName)); diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index ac2baad8b1..812a6ffd85 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -28,8 +28,6 @@ extern "C" __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 0x #include #endif -using namespace Fallback; - /** * \brief Parses application command line and calls \ref Cfg::ConfigurationManager * to parse configuration files. @@ -152,7 +150,7 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati engine.setSaveGameFile(variables["load-savegame"].as().u8string()); // other settings - Fallback::Map::init(variables["fallback"].as().mMap); + Fallback::Map::init(variables["fallback"].as().mMap); engine.setSoundUsage(!variables["no-sound"].as()); engine.setActivationDistanceOverride(variables["activate-dist"].as()); engine.enableFontExport(variables["export-fonts"].as()); diff --git a/components/fallback/validate.hpp b/components/fallback/validate.hpp index b48dff50d1..9540c85654 100644 --- a/components/fallback/validate.hpp +++ b/components/fallback/validate.hpp @@ -24,8 +24,6 @@ namespace Fallback }; // Parses and validates a fallback map from boost program_options. - // Note: for boost to pick up the validate function, you need to pull in the namespace e.g. - // by using namespace Fallback; void validate(boost::any& v, std::vector const& tokens, FallbackMap*, int); } From 972995d1240d0dbef586210987b52328e7f45e75 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 14 Apr 2025 22:43:16 +0200 Subject: [PATCH 142/154] Fix typo in namespace name --- components/stereo/frustum.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/stereo/frustum.hpp b/components/stereo/frustum.hpp index 35e3adf95a..cf619529b4 100644 --- a/components/stereo/frustum.hpp +++ b/components/stereo/frustum.hpp @@ -24,7 +24,7 @@ namespace osgViewer class Viewer; } -namespace usgUtil +namespace osgUtil { class CullVisitor; } From f80283422f0774bdf8f6f4abcedd5678eb59172c Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 18 Apr 2025 13:32:11 +0200 Subject: [PATCH 143/154] Use unique_ptr to handle lua state lifetime --- components/CMakeLists.txt | 2 +- components/esm/luascripts.cpp | 13 ++++++++----- components/lua/luastate.cpp | 35 +++++++++++++++++----------------- components/lua/luastate.hpp | 22 +++------------------ components/lua/luastateptr.hpp | 18 +++++++++++++++++ 5 files changed, 48 insertions(+), 42 deletions(-) create mode 100644 components/lua/luastateptr.hpp diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index b6734e6fc6..7659de0ffd 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -59,7 +59,7 @@ list(APPEND COMPONENT_FILES "${OpenMW_BINARY_DIR}/${OSG_PLUGIN_CHECKER_CPP_FILE} add_component_dir (lua luastate scriptscontainer asyncpackage utilpackage serialization configuration l10n storage utf8 - shapes/box inputactions yamlloader scripttracker + shapes/box inputactions yamlloader scripttracker luastateptr ) add_component_dir (l10n diff --git a/components/esm/luascripts.cpp b/components/esm/luascripts.cpp index 81cc867aff..f724331548 100644 --- a/components/esm/luascripts.cpp +++ b/components/esm/luascripts.cpp @@ -1,8 +1,9 @@ #include "luascripts.hpp" -#include "components/esm3/esmreader.hpp" -#include "components/esm3/esmwriter.hpp" +#include +#include +#include #include // List of all records, that are related to Lua. @@ -102,13 +103,16 @@ void ESM::LuaScriptsCfg::adjustRefNums(const ESMReader& esm) throw std::runtime_error("Incorrect contentFile index"); }; - lua_State* L = luaL_newstate(); + LuaUtil::LuaStatePtr state(luaL_newstate()); + if (state == nullptr) + throw std::runtime_error("Failed to create Lua runtime"); + LuaUtil::BasicSerializer serializer(adjustRefNumFn); auto adjustLuaData = [&](std::string& data) { if (data.empty()) return; - sol::object luaData = LuaUtil::deserialize(L, data, &serializer); + sol::object luaData = LuaUtil::deserialize(state.get(), data, &serializer); data = LuaUtil::serialize(luaData, &serializer); }; @@ -123,7 +127,6 @@ void ESM::LuaScriptsCfg::adjustRefNums(const ESMReader& esm) refCfg.mRefnumContentFile = adjustRefNumFn(refCfg.mRefnumContentFile); } } - lua_close(L); } void ESM::LuaScriptsCfg::save(ESMWriter& esm) const diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp index de876f2c5c..f959263153 100644 --- a/components/lua/luastate.cpp +++ b/components/lua/luastate.cpp @@ -11,6 +11,7 @@ #include #include +#include "luastateptr.hpp" #include "scriptscontainer.hpp" #include "utf8.hpp" @@ -151,37 +152,37 @@ namespace LuaUtil return newPtr; } - lua_State* LuaState::createLuaRuntime(LuaState* luaState) + LuaStatePtr LuaState::createLuaRuntime(LuaState* luaState) { if (sProfilerEnabled) { Log(Debug::Info) << "Initializing LuaUtil::LuaState with profiler"; - lua_State* L = lua_newstate(&trackingAllocator, luaState); - if (L) - return L; - else - { - sProfilerEnabled = false; - Log(Debug::Error) - << "Failed to initialize LuaUtil::LuaState with custom allocator; disabling Lua profiler"; - } + LuaStatePtr state(lua_newstate(&trackingAllocator, luaState)); + if (state != nullptr) + return state; + sProfilerEnabled = false; + Log(Debug::Error) << "Failed to initialize LuaUtil::LuaState with custom allocator; disabling Lua profiler"; } Log(Debug::Info) << "Initializing LuaUtil::LuaState without profiler"; - lua_State* L = luaL_newstate(); - if (!L) - throw std::runtime_error("Can't create Lua runtime"); - return L; + LuaStatePtr state(luaL_newstate()); + if (state == nullptr) + throw std::runtime_error("Failed to create Lua runtime"); + return state; } LuaState::LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf, const LuaStateSettings& settings) : mSettings(settings) - , mLuaHolder(createLuaRuntime(this)) - , mSol(mLuaHolder.get()) + , mLuaState([&] { + LuaStatePtr state = createLuaRuntime(this); + sol::set_default_state(state.get()); + return state; + }()) + , mSol(mLuaState.get()) , mConf(conf) , mVFS(vfs) { if (sProfilerEnabled) - lua_sethook(mLuaHolder.get(), &countHook, LUA_MASKCOUNT, countHookStep); + lua_sethook(mLuaState.get(), &countHook, LUA_MASKCOUNT, countHookStep); protectedCall([&](LuaView& view) { auto& sol = view.sol(); diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp index cf8e62690a..d842478cb1 100644 --- a/components/lua/luastate.hpp +++ b/components/lua/luastate.hpp @@ -10,6 +10,7 @@ #include #include "configuration.hpp" +#include "luastateptr.hpp" namespace VFS { @@ -188,7 +189,7 @@ namespace LuaUtil static void countHook(lua_State* L, lua_Debug* ar); static void* trackingAllocator(void* ud, void* ptr, size_t osize, size_t nsize); - lua_State* createLuaRuntime(LuaState* luaState); + static LuaStatePtr createLuaRuntime(LuaState* luaState); struct AllocOwner { @@ -206,25 +207,8 @@ namespace LuaUtil uint64_t mSmallAllocMemoryUsage = 0; std::vector mMemoryUsage; - class LuaStateHolder - { - public: - LuaStateHolder(lua_State* L) - : L(L) - { - sol::set_default_state(L); - } - ~LuaStateHolder() { lua_close(L); } - LuaStateHolder(const LuaStateHolder&) = delete; - LuaStateHolder(LuaStateHolder&&) = delete; - lua_State* get() { return L; } - - private: - lua_State* L; - }; - // Must be declared before mSol and all sol-related objects. Then on exit it will be destructed the last. - LuaStateHolder mLuaHolder; + LuaStatePtr mLuaState; sol::state_view mSol; const ScriptsConfiguration* mConf; diff --git a/components/lua/luastateptr.hpp b/components/lua/luastateptr.hpp new file mode 100644 index 0000000000..09733b0754 --- /dev/null +++ b/components/lua/luastateptr.hpp @@ -0,0 +1,18 @@ +#ifndef OPENMW_COMPONENTS_LUA_LUASTATEPTR_H +#define OPENMW_COMPONENTS_LUA_LUASTATEPTR_H + +#include + +#include + +namespace LuaUtil +{ + struct CloseLuaState + { + void operator()(lua_State* state) noexcept { lua_close(state); } + }; + + using LuaStatePtr = std::unique_ptr; +} + +#endif From 042c4b2b9dbd9990ad0969a91d1f175bbc2551df Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 18 Apr 2025 14:37:42 +0200 Subject: [PATCH 144/154] Use static_assert for compile time check --- apps/openmw/mwworld/containerstore.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/openmw/mwworld/containerstore.hpp b/apps/openmw/mwworld/containerstore.hpp index fb2722dde8..6bfbd78493 100644 --- a/apps/openmw/mwworld/containerstore.hpp +++ b/apps/openmw/mwworld/containerstore.hpp @@ -406,8 +406,7 @@ namespace MWWorld template ContainerStoreIteratorBase(const ContainerStoreIteratorBase& other) { - char CANNOT_CONVERT_CONST_ITERATOR_TO_ITERATOR[IsConvertible::value ? 1 : -1]; - ((void)CANNOT_CONVERT_CONST_ITERATOR_TO_ITERATOR); + static_assert(IsConvertible::value); copy(other); } From 6f89d38b78a58c244a75b8d4bce70796759d8f52 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 20 Apr 2025 02:28:34 +0200 Subject: [PATCH 145/154] Replace includes by forward declaration --- components/stereo/frustum.hpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/components/stereo/frustum.hpp b/components/stereo/frustum.hpp index cf619529b4..56740c125a 100644 --- a/components/stereo/frustum.hpp +++ b/components/stereo/frustum.hpp @@ -2,10 +2,7 @@ #define STEREO_FRUSTUM_H #include -#include #include -#include -#include #include #include @@ -13,6 +10,7 @@ namespace osg { + class Camera; class FrameBufferObject; class Texture2D; class Texture2DMultisample; From 626d7b22820c6958b226e41b26354b1177306393 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 21 Apr 2025 16:36:19 +0200 Subject: [PATCH 146/154] Add missing TargetPolygonNotFound enum value to lua bindings --- CMakeLists.txt | 2 +- apps/openmw/mwlua/nearbybindings.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 23e158fa78..94b52e0156 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 49) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 71) +set(OPENMW_LUA_API_REVISION 72) set(OPENMW_POSTPROCESSING_API_REVISION 2) set(OPENMW_VERSION_COMMITHASH "") diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp index df317ffeba..a6d762499a 100644 --- a/apps/openmw/mwlua/nearbybindings.cpp +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -213,6 +213,7 @@ namespace MWLua { "NavMeshNotFound", DetourNavigator::Status::NavMeshNotFound }, { "StartPolygonNotFound", DetourNavigator::Status::StartPolygonNotFound }, { "EndPolygonNotFound", DetourNavigator::Status::EndPolygonNotFound }, + { "TargetPolygonNotFound", DetourNavigator::Status::TargetPolygonNotFound }, { "MoveAlongSurfaceFailed", DetourNavigator::Status::MoveAlongSurfaceFailed }, { "FindPathOverPolygonsFailed", DetourNavigator::Status::FindPathOverPolygonsFailed }, { "InitNavMeshQueryFailed", DetourNavigator::Status::InitNavMeshQueryFailed }, From 928bbed09b2c2774ddaac89411967288e4f11476 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Mon, 21 Apr 2025 22:52:24 +0100 Subject: [PATCH 147/154] Increment cache keys missed in !4450 It changed the filenames for deps, so we've got two copies of the deps in the cache, and now we're running out of disk space. --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c910a0c4ed..e02c42fa31 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -651,7 +651,7 @@ macOS14_Xcode15_arm64: - Get-Volume - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: - key: ninja-2022-v11 + key: ninja-2022-v12 paths: - ccache - deps @@ -798,7 +798,7 @@ macOS14_Xcode15_arm64: - Get-Volume - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: - key: msbuild-2022-v11 + key: msbuild-2022-v12 paths: - deps - MSVC2022_64/deps/Qt From 19725473d730f44970ac2dcfe08f7b2ec02b5c85 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Mon, 21 Apr 2025 23:28:09 +0100 Subject: [PATCH 148/154] Detect failures in multiline PowerShell commands GitLab inserts a check for failure after each command in our `script`. This is documented here https://docs.gitlab.com/runner/shells/#powershell However, it doesn't detect failures if we run commands back to back. This adds the checks GitLab would have added for us if we were able to make it do that. --- .gitlab-ci.yml | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e02c42fa31..ad17f34af7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -632,12 +632,16 @@ macOS14_Xcode15_arm64: - | if (Get-ChildItem -Recurse *.pdb) { 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt + if(!$?) { Exit $LASTEXITCODE } if (Test-Path env:AWS_ACCESS_KEY_ID) { aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory} + if(!$?) { Exit $LASTEXITCODE } } Push-Location .. ..\CI\Store-Symbols.ps1 -SkipCompress + if(!$?) { Exit $LASTEXITCODE } 7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt + if(!$?) { Exit $LASTEXITCODE } Pop-Location Get-ChildItem -Recurse *.pdb | Remove-Item } @@ -645,8 +649,15 @@ macOS14_Xcode15_arm64: - | if (Test-Path env:AWS_ACCESS_KEY_ID) { aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory} + if(!$?) { Exit $LASTEXITCODE } + } + - | + if ($executables) { + foreach ($exe in $executables.Split(',')) { + & .\$exe + if(!$?) { Exit $LASTEXITCODE } + } } - - if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } } after_script: - Get-Volume - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log @@ -779,12 +790,16 @@ macOS14_Xcode15_arm64: - | if (Get-ChildItem -Recurse *.pdb) { 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt + if(!$?) { Exit $LASTEXITCODE } if (Test-Path env:AWS_ACCESS_KEY_ID) { aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory} + if(!$?) { Exit $LASTEXITCODE } } Push-Location .. ..\CI\Store-Symbols.ps1 -SkipCompress + if(!$?) { Exit $LASTEXITCODE } 7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt + if(!$?) { Exit $LASTEXITCODE } Pop-Location Get-ChildItem -Recurse *.pdb | Remove-Item } @@ -792,8 +807,15 @@ macOS14_Xcode15_arm64: - | if (Test-Path env:AWS_ACCESS_KEY_ID) { aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory} + if(!$?) { Exit $LASTEXITCODE } + } + - | + if ($executables) { + foreach ($exe in $executables.Split(',')) { + & .\$exe + if(!$?) { Exit $LASTEXITCODE } + } } - - if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } } after_script: - Get-Volume - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log From 8ee0c9e7be8b8866dc87c90c2bad309eaf8d041f Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 20 Apr 2025 02:16:24 +0200 Subject: [PATCH 149/154] Retry apt-get update and add-apt-repository --- CI/install_debian_deps.sh | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/CI/install_debian_deps.sh b/CI/install_debian_deps.sh index d29f16f55f..3ba66133ca 100755 --- a/CI/install_debian_deps.sh +++ b/CI/install_debian_deps.sh @@ -95,7 +95,7 @@ declare -rA GROUPED_DEPS=( [libasan6]="libasan6" [android]="binutils build-essential cmake ccache curl unzip git pkg-config" - + [openmw-clang-format]=" clang-format-14 git-core @@ -126,10 +126,24 @@ export APT_CACHE_DIR="${PWD}/apt-cache" export DEBIAN_FRONTEND=noninteractive set -x mkdir -pv "$APT_CACHE_DIR" -apt-get update -yqq + +while true; do + apt-get update -yqq && break +done + apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends software-properties-common gnupg >/dev/null -add-apt-repository -y ppa:openmw/openmw -add-apt-repository -y ppa:openmw/openmw-daily -add-apt-repository -y ppa:openmw/staging + +while true; do + add-apt-repository -y ppa:openmw/openmw && break +done + +while true; do + add-apt-repository -y ppa:openmw/openmw-daily && break +done + +while true; do + add-apt-repository -y ppa:openmw/staging && break +done + apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends "${deps[@]}" >/dev/null apt list --installed From 5ef2cf23b3f17bf0e3db9ff398fe8f08c5c2cc7e Mon Sep 17 00:00:00 2001 From: Dave Corley Date: Tue, 22 Apr 2025 01:50:41 -0700 Subject: [PATCH 150/154] CLEANUP: Loop param, also, is a bool --- files/lua_api/openmw/animation.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/lua_api/openmw/animation.lua b/files/lua_api/openmw/animation.lua index b5bf50d446..d15241afff 100644 --- a/files/lua_api/openmw/animation.lua +++ b/files/lua_api/openmw/animation.lua @@ -224,7 +224,7 @@ -- @param #string model path (normally taken from a record such as @{openmw.types#StaticRecord.model} or similar) -- @param #table options optional table of parameters. Can contain: -- --- * `loop` - boolean, if true the effect will loop until removed (default: 0). +-- * `loop` - boolean, if true the effect will loop until removed (default: false). -- * `boneName` - name of the bone to attach the vfx to. (default: "") -- * `particleTextureOverride` - name of the particle texture to use. (default: "") -- * `vfxId` - a string ID that can be used to remove the effect later, using #removeVfx, and to avoid duplicate effects. The default value of "" can have duplicates. To avoid interaction with the engine, use unique identifiers unrelated to magic effect IDs. The engine uses this identifier to add and remove magic effects based on what effects are active on the actor. If this is set equal to the @{openmw.core#MagicEffectId} identifier of the magic effect being added, for example core.magic.EFFECT_TYPE.FireDamage, then the engine will remove it once the fire damage effect on the actor reaches 0. (Default: ""). From 1948ab21f730a631b97e50606972054111e559b7 Mon Sep 17 00:00:00 2001 From: Sam Kaufman Date: Sun, 20 Apr 2025 20:39:28 -0700 Subject: [PATCH 151/154] Set SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH. This fixes bugs #8225 and #8462. --- CHANGELOG.md | 1 + apps/openmw/engine.cpp | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c74ff74397..8656f792df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -231,6 +231,7 @@ Bug #8378: Korean bitmap fonts are unusable Bug #8439: Creatures without models can crash the game Bug #8441: Freeze when using video main menu replacers + Bug #8462: Crashes when resizing the window on macOS Feature #1415: Infinite fall failsafe Feature #2566: Handle NAM9 records for manual cell references Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 16b888d945..7dc372f269 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -379,6 +379,9 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) , mCfgMgr(configurationManager) , mGlMaxTextureImageUnits(0) { +#if SDL_VERSION_ATLEAST(2, 24, 0) + SDL_SetHint(SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH, "1"); +#endif SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0"); // We use only gamepads Uint32 flags From f487a6332b0de2bf9effdd15b162254b5665ba44 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 12 Apr 2025 18:00:20 +0200 Subject: [PATCH 152/154] Use string_view for sIdleSelectToGroupName --- apps/openmw/mwbase/mechanicsmanager.hpp | 2 +- apps/openmw/mwmechanics/actors.cpp | 2 +- apps/openmw/mwmechanics/actors.hpp | 2 +- apps/openmw/mwmechanics/aiwander.cpp | 22 +++++++++---------- apps/openmw/mwmechanics/aiwander.hpp | 3 ++- .../mwmechanics/mechanicsmanagerimp.cpp | 3 ++- .../mwmechanics/mechanicsmanagerimp.hpp | 2 +- 7 files changed, 19 insertions(+), 17 deletions(-) diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index 4883fa2000..23d79c1a6b 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -200,7 +200,7 @@ namespace MWBase ///< Skip the animation for the given MW-reference for one frame. Calls to this function for /// references that are currently not in the scene should be ignored. - virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) = 0; + virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, std::string_view groupName) = 0; virtual bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const = 0; diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index bb3273981d..1e62cc4a21 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -2028,7 +2028,7 @@ namespace MWMechanics iter->second->getCharacterController().skipAnim(); } - bool Actors::checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) const + bool Actors::checkAnimationPlaying(const MWWorld::Ptr& ptr, std::string_view groupName) const { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index b575ec2827..3e34ed0d67 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -119,7 +119,7 @@ namespace MWMechanics std::string_view startKey, std::string_view stopKey, bool forceLoop); void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable); void skipAnimation(const MWWorld::Ptr& ptr) const; - bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) const; + bool checkAnimationPlaying(const MWWorld::Ptr& ptr, std::string_view groupName) const; bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const; void persistAnimationStates() const; void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted); diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 3c299c1490..42667a406a 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -40,15 +40,15 @@ namespace MWMechanics static const std::size_t MAX_IDLE_SIZE = 8; - const std::string AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = { - std::string("idle2"), - std::string("idle3"), - std::string("idle4"), - std::string("idle5"), - std::string("idle6"), - std::string("idle7"), - std::string("idle8"), - std::string("idle9"), + const std::string_view AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = { + "idle2", + "idle3", + "idle4", + "idle5", + "idle6", + "idle7", + "idle8", + "idle9", }; namespace @@ -680,7 +680,7 @@ namespace MWMechanics { if ((GroupIndex_MinIdle <= idleSelect) && (idleSelect <= GroupIndex_MaxIdle)) { - const std::string& groupName = sIdleSelectToGroupName[idleSelect - GroupIndex_MinIdle]; + const std::string_view groupName = sIdleSelectToGroupName[idleSelect - GroupIndex_MinIdle]; return MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, groupName, 0, 1); } else @@ -695,7 +695,7 @@ namespace MWMechanics { if ((GroupIndex_MinIdle <= idleSelect) && (idleSelect <= GroupIndex_MaxIdle)) { - const std::string& groupName = sIdleSelectToGroupName[idleSelect - GroupIndex_MinIdle]; + const std::string_view groupName = sIdleSelectToGroupName[idleSelect - GroupIndex_MinIdle]; return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, groupName); } else diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index f4d5585fe7..01a02096a4 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -3,6 +3,7 @@ #include "typedaipackage.hpp" +#include #include #include "aitemporarybase.hpp" @@ -181,7 +182,7 @@ namespace MWMechanics const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage); /// lookup table for converting idleSelect value to groupName - static const std::string sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1]; + static const std::string_view sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1]; }; } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 46f6440ae6..47c49a8861 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -778,7 +778,8 @@ namespace MWMechanics else mObjects.skipAnimation(ptr); } - bool MechanicsManager::checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) + + bool MechanicsManager::checkAnimationPlaying(const MWWorld::Ptr& ptr, std::string_view groupName) { if (ptr.getClass().isActor()) return mActors.checkAnimationPlaying(ptr, groupName); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 4b0126cd34..93af89863b 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -146,7 +146,7 @@ namespace MWMechanics std::string_view startKey, std::string_view stopKey, bool forceLoop) override; void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) override; void skipAnimation(const MWWorld::Ptr& ptr) override; - bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) override; + bool checkAnimationPlaying(const MWWorld::Ptr& ptr, std::string_view groupName) override; bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const override; void persistAnimationStates() override; void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) override; From fc4cc3255d4265011a15a4cce73b12f56673784d Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 20 Apr 2025 16:48:23 +0200 Subject: [PATCH 153/154] Do not build navmeshtool translation units twice --- apps/navmeshtool/CMakeLists.txt | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/navmeshtool/CMakeLists.txt b/apps/navmeshtool/CMakeLists.txt index 090fc00f36..f056c93685 100644 --- a/apps/navmeshtool/CMakeLists.txt +++ b/apps/navmeshtool/CMakeLists.txt @@ -1,20 +1,16 @@ -set(NAVMESHTOOL +set(NAVMESHTOOL_LIB worldspacedata.cpp navmesh.cpp - main.cpp ) -source_group(apps\\navmeshtool FILES ${NAVMESHTOOL}) -add_library(openmw-navmeshtool-lib STATIC - ${NAVMESHTOOL} -) +source_group(apps\\navmeshtool FILES ${NAVMESHTOOL_LIB} main.cpp) + +add_library(openmw-navmeshtool-lib STATIC ${NAVMESHTOOL_LIB}) if (ANDROID) - add_library(openmw-navmeshtool SHARED - main.cpp - ) + add_library(openmw-navmeshtool SHARED main.cpp) else() - openmw_add_executable(openmw-navmeshtool ${NAVMESHTOOL}) + openmw_add_executable(openmw-navmeshtool main.cpp) endif() target_link_libraries(openmw-navmeshtool openmw-navmeshtool-lib) From 52281a5e32003d81ced2563eb2aa1c8ca8a0c5fc Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 26 Apr 2025 11:48:15 +0200 Subject: [PATCH 154/154] Fix path for junit reports --- .gitlab-ci.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ad17f34af7..7b6bb7d92e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -228,7 +228,7 @@ Ubuntu_GCC_tests: name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} when: always reports: - junit: build/*_tests.xml + junit: build/*-tests.xml .Ubuntu_GCC_tests_Debug: extends: Ubuntu_GCC @@ -244,7 +244,7 @@ Ubuntu_GCC_tests: name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} when: always reports: - junit: build/*_tests.xml + junit: build/*-tests.xml Ubuntu_GCC_tests_asan: extends: Ubuntu_GCC @@ -262,7 +262,7 @@ Ubuntu_GCC_tests_asan: name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} when: always reports: - junit: build/*_tests.xml + junit: build/*-tests.xml Ubuntu_GCC_tests_ubsan: extends: Ubuntu_GCC @@ -279,7 +279,7 @@ Ubuntu_GCC_tests_ubsan: name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} when: always reports: - junit: build/*_tests.xml + junit: build/*-tests.xml .Ubuntu_GCC_tests_tsan: extends: Ubuntu_GCC @@ -297,7 +297,7 @@ Ubuntu_GCC_tests_ubsan: name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} when: always reports: - junit: build/*_tests.xml + junit: build/*-tests.xml Ubuntu_GCC_tests_coverage: extends: .Ubuntu_GCC_tests_Debug @@ -316,7 +316,7 @@ Ubuntu_GCC_tests_coverage: coverage_report: coverage_format: cobertura path: coverage.xml - junit: build/*_tests.xml + junit: build/*-tests.xml .Ubuntu_Static_Deps: extends: Ubuntu_Clang @@ -357,7 +357,7 @@ Ubuntu_GCC_tests_coverage: name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} when: always reports: - junit: build/*_tests.xml + junit: build/*-tests.xml Ubuntu_Clang: extends: .Ubuntu @@ -440,7 +440,7 @@ Ubuntu_Clang_Tidy_other: name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} when: always reports: - junit: build/*_tests.xml + junit: build/*-tests.xml Ubuntu_Clang_tests_Debug: extends: Ubuntu_Clang @@ -455,7 +455,7 @@ Ubuntu_Clang_tests_Debug: name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} when: always reports: - junit: build/*_tests.xml + junit: build/*-tests.xml .Ubuntu_integration_tests_base: extends: .Ubuntu_Image