diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 44655466ea..63f5bfb45e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -142,7 +142,7 @@ Ubuntu_GCC: variables: CC: gcc CXX: g++ - CCACHE_SIZE: 4G + CCACHE_SIZE: 3G # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. timeout: 2h @@ -193,7 +193,7 @@ Ubuntu_GCC_Debug: variables: CC: gcc CXX: g++ - CCACHE_SIZE: 4G + CCACHE_SIZE: 3G CMAKE_BUILD_TYPE: Debug CMAKE_CXX_FLAGS_DEBUG: -O0 # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. diff --git a/AUTHORS.md b/AUTHORS.md index 9791171b9c..e2903febe4 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -245,6 +245,7 @@ Programmers xyzz Yohaulticetl Yuri Krupenin + Yury Stepovikov zelurker Documentation diff --git a/CHANGELOG.md b/CHANGELOG.md index 30ad2bae6c..75e6f05dd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,15 @@ Bug #4382: Sound output device does not change when it should Bug #4508: Can't stack enchantment buffs from different instances of the same self-cast generic magic apparel Bug #4610: Casting a Bound Weapon spell cancels the casting animation by equipping the weapon prematurely + Bug #4683: Disposition decrease when player commits crime is not implemented properly Bug #4742: Actors with wander never stop walking after Loopgroup Walkforward + Bug #4743: PlayGroup doesn't play non-looping animations correctly Bug #4754: Stack of ammunition cannot be equipped partially Bug #4816: GetWeaponDrawn returns 1 before weapon is attached + Bug #4822: Non-weapon equipment and body parts can't inherit time from parent animation Bug #5057: Weapon swing sound plays at same pitch whether it hits or misses Bug #5062: Root bone rotations for NPC animation don't work the same as for creature animation + Bug #5066: Quirks with starting and stopping scripted animations Bug #5129: Stuttering animation on Centurion Archer Bug #5280: Unskinned shapes in skinned equipment are rendered in the wrong place Bug #5371: Keyframe animation tracks are used for any file that begins with an X @@ -28,12 +32,15 @@ Bug #6190: Unintuitive sun specularity time of day dependence Bug #6222: global map cell size can crash openmw if set to too high a value Bug #6313: Followers with high Fight can turn hostile + Bug #6402: The sound of a thunderstorm does not stop playing after entering the premises Bug #6427: Enemy health bar disappears before damaging effect ends Bug #6550: Cloned body parts don't inherit texture effects Bug #6645: Enemy block sounds align with animation instead of blocked hits Bug #6657: Distant terrain tiles become black when using FWIW mod Bug #6661: Saved games that have no preview screenshot cause issues or crashes Bug #6716: mwscript comparison operator handling is too restrictive + Bug #6754: Beast to Non-beast transformation mod is not working on OpenMW + Bug #6758: Main menu background video can be stopped by opening the options menu Bug #6807: Ultimate Galleon is not working properly Bug #6893: Lua: Inconsistent behavior with actors affected by Disable and SetDelete commands Bug #6894: Added item combines with equipped stack instead of creating a new unequipped stack @@ -58,6 +65,7 @@ Bug #7084: Resurrecting an actor doesn't take into account base record changes Bug #7088: Deleting last save game of last character doesn't clear character name/details Bug #7092: BSA archives from higher priority directories don't take priority + Bug #7103: Multiple paths pointing to the same plugin but with different cases lead to automatically removed config entries Bug #7122: Teleportation to underwater should cancel active water walking effect Bug #7131: MyGUI log spam when post processing HUD is open Bug #7134: Saves with an invalid last generated RefNum can be loaded @@ -67,38 +75,63 @@ Bug #7229: Error marker loading failure is not handled Bug #7243: Supporting loading external files from VFS from esm files Bug #7284: "Your weapon has no effect." message doesn't always show when the player character attempts to attack + Bug #7292: Weather settings for disabling or enabling snow and rain ripples don't work Bug #7298: Water ripples from projectiles sometimes are not spawned Bug #7307: Alchemy "Magic Effect" search string does not match on tool tip for effects related to attributes + Bug #7309: Sunlight scattering is visible in inappropriate situations Bug #7322: Shadows don't cover groundcover depending on the view angle and perspective with compute scene bounds = primitives + Bug #7354: Disabling post processing in-game causes a crash + Bug #7364: Post processing is not reflected in savegame previews Bug #7380: NiZBufferProperty issue Bug #7413: Generated wilderness cells don't spawn fish Bug #7415: Unbreakable lock discrepancies + Bug #7416: Modpccrimelevel is different from vanilla Bug #7428: AutoCalc flag is not used to calculate enchantment costs Bug #7450: Evading obstacles does not work for actors missing certain animations Bug #7459: Icons get stacked on the cursor when picking up multiple items simultaneously Bug #7472: Crash when enchanting last projectiles + Bug #7475: Equipping a constant effect item doesn't update the magic menu Bug #7502: Data directories dialog (0.48.0) forces adding subdirectory instead of intended directory Bug #7505: Distant terrain does not support sample size greater than cell size Bug #7553: Faction reaction loading is incorrect Bug #7557: Terrain::ChunkManager::createChunk is called twice for the same position, lod on initial loading Bug #7573: Drain Fatigue can't bring fatigue below zero by default + Bug #7585: Difference in interior lighting between OpenMW with legacy lighting method enabled and vanilla Morrowind Bug #7603: Scripts menu size is not updated properly Bug #7604: Goblins Grunt becomes idle once injured Bug #7609: ForceGreeting should not open dialogue for werewolves Bug #7611: Beast races' idle animations slide after turning or jumping in place + Bug #7617: The death prompt asks the player if they wanted to load the character's last created save + Bug #7619: Long map notes may get cut off Bug #7630: Charm can be cast on creatures Bug #7631: Cannot trade with/talk to Creeper or Mudcrab Merchant when they're fleeing Bug #7636: Animations bug out when switching between 1st and 3rd person, while playing a scripted animation Bug #7637: Actors can sometimes move while playing scripted animations Bug #7639: NPCs don't use hand-to-hand if their other melee skills were damaged during combat + Bug #7641: loopgroup loops the animation one time too many for actors Bug #7642: Items in repair and recharge menus aren't sorted alphabetically + Bug #7643: Can't enchant items with constant effect on self magic effects for non-player character + Bug #7646: Follower voices pain sounds when attacked with magic Bug #7647: NPC walk cycle bugs after greeting player Bug #7654: Tooltips for enchantments with invalid effects cause crashes Bug #7660: Some inconsistencies regarding Invisibility breaking + Bug #7661: Player followers should stop attacking newly recruited actors Bug #7665: Alchemy menu is missing the ability to deselect and choose different qualities of an apparatus Bug #7675: Successful lock spell doesn't produce a sound + Bug #7676: Incorrect magic effect order in alchemy Bug #7679: Scene luminance value flashes when toggling shaders + Bug #7685: Corky sometimes doesn't follow Llovyn Andus + Bug #7712: Casting doesn't support spells and enchantments with no effects + Bug #7723: Assaulting vampires and werewolves shouldn't be a crime + Bug #7724: Guards don't help vs werewolves + Bug #7733: Launcher shows incorrect data paths when there's two plugins with the same name + Bug #7742: Governing attribute training limit should use the modified attribute + Bug #7758: Water walking is not taken into account to compute path cost on the water + Bug #7761: Rain and ambient loop sounds are mutually exclusive + Bug #7770: Sword of the Perithia: Script execution failure + Feature #2566: Handle NAM9 records for manual cell references Feature #3537: Shader-based water ripples + Feature #5173: Support for NiFogProperty Feature #5492: Let rain and snow collide with statics Feature #6149: Dehardcode Lua API_REVISION Feature #6152: Playing music via lua scripts @@ -127,14 +160,17 @@ Feature #7477: NegativeLight Magic Effect flag Feature #7499: OpenMW-CS: Generate record filters by drag & dropping cell content to the filters field Feature #7546: Start the game on Fredas + Feature #7554: Controller binding for tab for menu navigation Feature #7568: Uninterruptable scripted music Feature #7608: Make the missing dependencies warning when loading a savegame more helpful Feature #7618: Show the player character's health in the save details Feature #7625: Add some missing console error outputs Feature #7634: Support NiParticleBomb Feature #7652: Sort inactive post processing shaders list properly + Feature #7698: Implement sAbsorb, sDamage, sDrain, sFortify and sRestore Feature #7709: Improve resolution selection in Launcher Task #5896: Do not use deprecated MyGUI properties + Task #6624: Drop support for saves made prior to 0.45 Task #7113: Move from std::atoi to std::from_char Task #7117: Replace boost::scoped_array with std::vector Task #7151: Do not use std::strerror to get errno error message diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index 40210428d0..dd54030dfb 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -1,31 +1,26 @@ #!/bin/sh -ex export HOMEBREW_NO_EMOJI=1 +export HOMEBREW_NO_INSTALL_CLEANUP=1 +export HOMEBREW_AUTOREMOVE=1 -brew uninstall --ignore-dependencies python@3.8 || true -brew uninstall --ignore-dependencies python@3.9 || true -brew uninstall --ignore-dependencies qt@6 || true -brew uninstall --ignore-dependencies jpeg || true +# workaround for gitlab's pre-installed brew +# purge large and unnecessary packages that get in our way and have caused issues +brew uninstall ruby php openjdk node postgresql maven curl || true brew tap --repair brew update --quiet # Some of these tools can come from places other than brew, so check before installing -brew reinstall xquartz fontconfig freetype harfbuzz brotli - -# Fix: can't open file: @loader_path/libbrotlicommon.1.dylib (No such file or directory) -BREW_LIB_PATH="$(brew --prefix)/lib" -install_name_tool -change "@loader_path/libbrotlicommon.1.dylib" "${BREW_LIB_PATH}/libbrotlicommon.1.dylib" ${BREW_LIB_PATH}/libbrotlidec.1.dylib -install_name_tool -change "@loader_path/libbrotlicommon.1.dylib" "${BREW_LIB_PATH}/libbrotlicommon.1.dylib" ${BREW_LIB_PATH}/libbrotlienc.1.dylib +brew install curl xquartz gd fontconfig freetype harfbuzz brotli command -v ccache >/dev/null 2>&1 || brew install ccache command -v cmake >/dev/null 2>&1 || brew install cmake command -v qmake >/dev/null 2>&1 || brew install qt@5 export PATH="/opt/homebrew/opt/qt@5/bin:$PATH" - # Install deps -brew install icu4c yaml-cpp sqlite +brew install openal-soft icu4c yaml-cpp sqlite ccache --version cmake --version diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index cdac794a7e..e11ceb499d 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -902,7 +902,6 @@ printf "Qt ${QT_VER}... " fi cd $QT_SDK - add_cmake_opts -DQT_QMAKE_EXECUTABLE="${QT_SDK}/bin/qmake.exe" for CONFIGURATION in ${CONFIGURATIONS[@]}; do if [ $CONFIGURATION == "Debug" ]; then DLLSUFFIX="d" diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh index c956f27514..cab67b6e4d 100755 --- a/CI/before_script.osx.sh +++ b/CI/before_script.osx.sh @@ -10,12 +10,13 @@ DEPENDENCIES_ROOT="/tmp/openmw-deps" QT_PATH=$(brew --prefix qt@5) ICU_PATH=$(brew --prefix icu4c) +OPENAL_PATH=$(brew --prefix openal-soft) CCACHE_EXECUTABLE=$(brew --prefix ccache)/bin/ccache mkdir build cd build cmake \ --D CMAKE_PREFIX_PATH="$DEPENDENCIES_ROOT;$QT_PATH" \ +-D CMAKE_PREFIX_PATH="$DEPENDENCIES_ROOT;$QT_PATH;$OPENAL_PATH" \ -D CMAKE_C_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" \ -D CMAKE_CXX_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" \ -D CMAKE_CXX_FLAGS="-stdlib=libc++" \ diff --git a/CI/file_name_exceptions.txt b/CI/file_name_exceptions.txt index 5035d73f27..c3bcee8661 100644 --- a/CI/file_name_exceptions.txt +++ b/CI/file_name_exceptions.txt @@ -19,6 +19,7 @@ apps/openmw_test_suite/lua/test_serialization.cpp apps/openmw_test_suite/lua/test_storage.cpp apps/openmw_test_suite/lua/test_ui_content.cpp apps/openmw_test_suite/lua/test_utilpackage.cpp +apps/openmw_test_suite/lua/test_inputactions.cpp apps/openmw_test_suite/misc/test_endianness.cpp apps/openmw_test_suite/misc/test_resourcehelpers.cpp apps/openmw_test_suite/misc/test_stringops.cpp diff --git a/CI/teal_ci.sh b/CI/teal_ci.sh index 5ea312e88c..8117e93443 100755 --- a/CI/teal_ci.sh +++ b/CI/teal_ci.sh @@ -1,4 +1,4 @@ -set -e +#!/bin/bash -e docs/source/install_luadocumentor_in_docker.sh PATH=$PATH:~/luarocks/bin diff --git a/CMakeLists.txt b/CMakeLists.txt index 34df0216da..28109bd01b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ IF(NOT CMAKE_BUILD_TYPE) ENDIF() if (APPLE) + set(CMAKE_FIND_FRAMEWORK LAST) # prefer dylibs over frameworks set(APP_BUNDLE_NAME "${CMAKE_PROJECT_NAME}.app") set(APP_BUNDLE_DIR "${OpenMW_BINARY_DIR}/${APP_BUNDLE_NAME}") diff --git a/apps/bsatool/bsatool.cpp b/apps/bsatool/bsatool.cpp index de755e7d1d..28711df929 100644 --- a/apps/bsatool/bsatool.cpp +++ b/apps/bsatool/bsatool.cpp @@ -194,7 +194,8 @@ int extract(std::unique_ptr& bsa, Arguments& info) // Get a stream for the file to extract for (auto it = bsa->getList().rbegin(); it != bsa->getList().rend(); ++it) { - if (Misc::StringUtils::ciEqual(Misc::StringUtils::stringToU8String(it->name()), archivePath)) + auto streamPath = Misc::StringUtils::stringToU8String(it->name()); + if (Misc::StringUtils::ciEqual(streamPath, archivePath) || Misc::StringUtils::ciEqual(streamPath, extractPath)) { stream = bsa->getFile(&*it); break; diff --git a/apps/bulletobjecttool/main.cpp b/apps/bulletobjecttool/main.cpp index c522d69d70..7d87899f4a 100644 --- a/apps/bulletobjecttool/main.cpp +++ b/apps/bulletobjecttool/main.cpp @@ -145,12 +145,12 @@ namespace config.filterOutNonExistingPaths(dataDirs); - const auto resDir = variables["resources"].as(); + const auto& resDir = variables["resources"].as(); Log(Debug::Info) << Version::getOpenmwVersionDescription(); dataDirs.insert(dataDirs.begin(), resDir / "vfs"); - const auto fileCollections = Files::Collections(dataDirs); - const auto archives = variables["fallback-archive"].as(); - const auto contentFiles = variables["content"].as(); + const Files::Collections fileCollections(dataDirs); + const auto& archives = variables["fallback-archive"].as(); + const auto& contentFiles = variables["content"].as(); Fallback::Map::init(variables["fallback"].as().mMap); diff --git a/apps/esmtool/esmtool.cpp b/apps/esmtool/esmtool.cpp index 092be66e97..13f222ed72 100644 --- a/apps/esmtool/esmtool.cpp +++ b/apps/esmtool/esmtool.cpp @@ -156,7 +156,7 @@ Allowed options)"); return false; }*/ - const auto inputFiles = variables["input-file"].as(); + const auto& inputFiles = variables["input-file"].as(); info.filename = inputFiles[0].u8string(); // This call to u8string is redundant, but required to build on // MSVC 14.26 due to implementation bugs. if (inputFiles.size() > 1) @@ -265,7 +265,7 @@ namespace std::cout << " Faction rank: " << ref.mFactionRank << '\n'; std::cout << " Enchantment charge: " << ref.mEnchantmentCharge << '\n'; std::cout << " Uses/health: " << ref.mChargeInt << '\n'; - std::cout << " Gold value: " << ref.mGoldValue << '\n'; + std::cout << " Count: " << ref.mCount << '\n'; std::cout << " Blocked: " << static_cast(ref.mReferenceBlocked) << '\n'; std::cout << " Deleted: " << deleted << '\n'; if (!ref.mKey.empty()) @@ -341,7 +341,7 @@ namespace { std::cout << "Author: " << esm.getAuthor() << '\n' << "Description: " << esm.getDesc() << '\n' - << "File format version: " << esm.getFVer() << '\n'; + << "File format version: " << esm.esmVersionF() << '\n'; std::vector masterData = esm.getGameFiles(); if (!masterData.empty()) { @@ -508,7 +508,7 @@ namespace ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(info.encoding)); esm.setEncoder(&encoder); esm.setHeader(data.mHeader); - esm.setVersion(ESM::VER_13); + esm.setVersion(ESM::VER_130); esm.setRecordCount(recordCount); std::fstream save(info.outname, std::fstream::out | std::fstream::binary); diff --git a/apps/esmtool/record.cpp b/apps/esmtool/record.cpp index 96c418c0c4..044fbf9f93 100644 --- a/apps/esmtool/record.cpp +++ b/apps/esmtool/record.cpp @@ -1084,14 +1084,8 @@ namespace EsmTool std::cout << " Rank: " << (int)mData.mNpdt.mRank << std::endl; std::cout << " Attributes:" << std::endl; - std::cout << " Strength: " << (int)mData.mNpdt.mStrength << std::endl; - std::cout << " Intelligence: " << (int)mData.mNpdt.mIntelligence << std::endl; - std::cout << " Willpower: " << (int)mData.mNpdt.mWillpower << std::endl; - std::cout << " Agility: " << (int)mData.mNpdt.mAgility << std::endl; - std::cout << " Speed: " << (int)mData.mNpdt.mSpeed << std::endl; - std::cout << " Endurance: " << (int)mData.mNpdt.mEndurance << std::endl; - std::cout << " Personality: " << (int)mData.mNpdt.mPersonality << std::endl; - std::cout << " Luck: " << (int)mData.mNpdt.mLuck << std::endl; + for (size_t i = 0; i != mData.mNpdt.mAttributes.size(); i++) + std::cout << " " << attributeLabel(i) << ": " << int(mData.mNpdt.mAttributes[i]) << std::endl; std::cout << " Skills:" << std::endl; for (size_t i = 0; i != mData.mNpdt.mSkills.size(); i++) @@ -1169,19 +1163,23 @@ namespace EsmTool std::cout << " Description: " << mData.mDescription << std::endl; std::cout << " Flags: " << raceFlags(mData.mData.mFlags) << std::endl; - for (int i = 0; i < 2; ++i) + std::cout << " Male:" << std::endl; + for (int j = 0; j < ESM::Attribute::Length; ++j) { - bool male = i == 0; - - std::cout << (male ? " Male:" : " Female:") << std::endl; - - for (int j = 0; j < ESM::Attribute::Length; ++j) - std::cout << " " << ESM::Attribute::indexToRefId(j) << ": " - << mData.mData.mAttributeValues[j].getValue(male) << std::endl; - - std::cout << " Height: " << mData.mData.mHeight.getValue(male) << std::endl; - std::cout << " Weight: " << mData.mData.mWeight.getValue(male) << std::endl; + ESM::RefId id = ESM::Attribute::indexToRefId(j); + std::cout << " " << id << ": " << mData.mData.getAttribute(id, true) << std::endl; } + std::cout << " Height: " << mData.mData.mMaleHeight << std::endl; + std::cout << " Weight: " << mData.mData.mMaleWeight << std::endl; + + std::cout << " Female:" << std::endl; + for (int j = 0; j < ESM::Attribute::Length; ++j) + { + ESM::RefId id = ESM::Attribute::indexToRefId(j); + std::cout << " " << id << ": " << mData.mData.getAttribute(id, false) << std::endl; + } + std::cout << " Height: " << mData.mData.mFemaleHeight << std::endl; + std::cout << " Weight: " << mData.mData.mFemaleWeight << std::endl; for (const auto& bonus : mData.mData.mBonus) // Not all races have 7 skills. diff --git a/apps/essimporter/converter.cpp b/apps/essimporter/converter.cpp index 4751fd9497..07146fc388 100644 --- a/apps/essimporter/converter.cpp +++ b/apps/essimporter/converter.cpp @@ -1,6 +1,7 @@ #include "converter.hpp" #include +#include #include #include @@ -33,7 +34,7 @@ namespace objstate.mPosition = cellref.mPos; objstate.mRef.mRefNum = cellref.mRefNum; if (cellref.mDeleted) - objstate.mCount = 0; + objstate.mRef.mCount = 0; convertSCRI(cellref.mActorData.mSCRI, objstate.mLocals); objstate.mHasLocals = !objstate.mLocals.mVariables.empty(); @@ -90,14 +91,14 @@ namespace ESSImport struct MAPH { - unsigned int size; - unsigned int value; + uint32_t size; + uint32_t value; }; void ConvertFMAP::read(ESM::ESMReader& esm) { MAPH maph; - esm.getHNTSized<8>(maph, "MAPH"); + esm.getHNT("MAPH", maph.size, maph.value); std::vector data; esm.getSubNameIs("MAPD"); esm.getSubHeader(); @@ -278,7 +279,7 @@ namespace ESSImport while (esm.isNextSub("MPCD")) { float notepos[3]; - esm.getHTSized<3 * sizeof(float)>(notepos); + esm.getHT(notepos); // Markers seem to be arranged in a 32*32 grid, notepos has grid-indices. // This seems to be the reason markers can't be placed everywhere in interior cells, diff --git a/apps/essimporter/convertinventory.cpp b/apps/essimporter/convertinventory.cpp index 69a2ea4120..7025b0ae43 100644 --- a/apps/essimporter/convertinventory.cpp +++ b/apps/essimporter/convertinventory.cpp @@ -16,7 +16,7 @@ namespace ESSImport objstate.blank(); objstate.mRef = item; objstate.mRef.mRefID = ESM::RefId::stringRefId(item.mId); - objstate.mCount = item.mCount; + objstate.mRef.mCount = item.mCount; state.mItems.push_back(objstate); if (item.mRelativeEquipmentSlot != -1) // Note we should really write the absolute slot here, which we do not know about diff --git a/apps/essimporter/importacdt.hpp b/apps/essimporter/importacdt.hpp index 785e988200..65519c6a6c 100644 --- a/apps/essimporter/importacdt.hpp +++ b/apps/essimporter/importacdt.hpp @@ -1,6 +1,7 @@ #ifndef OPENMW_ESSIMPORT_ACDT_H #define OPENMW_ESSIMPORT_ACDT_H +#include #include #include "importscri.hpp" @@ -25,14 +26,12 @@ namespace ESSImport }; /// Actor data, shared by (at least) REFR and CellRef -#pragma pack(push) -#pragma pack(1) struct ACDT { // Note, not stored at *all*: // - Level changes are lost on reload, except for the player (there it's in the NPC record). unsigned char mUnknown[12]; - unsigned int mFlags; + uint32_t mFlags; float mBreathMeter; // Seconds left before drowning unsigned char mUnknown2[20]; float mDynamic[3][2]; @@ -41,7 +40,7 @@ namespace ESSImport float mMagicEffects[27]; // Effect attributes: // https://wiki.openmw.org/index.php?title=Research:Magic#Effect_attributes unsigned char mUnknown4[4]; - unsigned int mGoldPool; + uint32_t mGoldPool; unsigned char mCountDown; // seen the same value as in ACSC.mCorpseClearCountdown, maybe // this one is for respawning? unsigned char mUnknown5[3]; @@ -60,7 +59,6 @@ namespace ESSImport unsigned char mUnknown[3]; float mTime; }; -#pragma pack(pop) struct ActorData { diff --git a/apps/essimporter/importcellref.cpp b/apps/essimporter/importcellref.cpp index a900440a96..56e888d3f6 100644 --- a/apps/essimporter/importcellref.cpp +++ b/apps/essimporter/importcellref.cpp @@ -1,6 +1,7 @@ #include "importcellref.hpp" #include +#include namespace ESSImport { @@ -44,19 +45,14 @@ namespace ESSImport bool isDeleted = false; ESM::CellRef::loadData(esm, isDeleted); - mActorData.mHasACDT = false; - if (esm.isNextSub("ACDT")) - { - mActorData.mHasACDT = true; - esm.getHTSized<264>(mActorData.mACDT); - } + mActorData.mHasACDT + = esm.getHNOT("ACDT", mActorData.mACDT.mUnknown, mActorData.mACDT.mFlags, mActorData.mACDT.mBreathMeter, + mActorData.mACDT.mUnknown2, mActorData.mACDT.mDynamic, mActorData.mACDT.mUnknown3, + mActorData.mACDT.mAttributes, mActorData.mACDT.mMagicEffects, mActorData.mACDT.mUnknown4, + mActorData.mACDT.mGoldPool, mActorData.mACDT.mCountDown, mActorData.mACDT.mUnknown5); - mActorData.mHasACSC = false; - if (esm.isNextSub("ACSC")) - { - mActorData.mHasACSC = true; - esm.getHTSized<112>(mActorData.mACSC); - } + mActorData.mHasACSC = esm.getHNOT("ACSC", mActorData.mACSC.mUnknown1, mActorData.mACSC.mFlags, + mActorData.mACSC.mUnknown2, mActorData.mACSC.mCorpseClearCountdown, mActorData.mACSC.mUnknown3); if (esm.isNextSub("ACSL")) esm.skipHSubSize(112); @@ -122,23 +118,17 @@ namespace ESSImport } // FIXME: not all actors have this, add flag - if (esm.isNextSub("CHRD")) // npc only - esm.getHExact(mActorData.mSkills, 27 * 2 * sizeof(int)); + esm.getHNOT("CHRD", mActorData.mSkills); // npc only - if (esm.isNextSub("CRED")) // creature only - esm.getHExact(mActorData.mCombatStats, 3 * 2 * sizeof(int)); + esm.getHNOT("CRED", mActorData.mCombatStats); // creature only mActorData.mSCRI.load(esm); if (esm.isNextSub("ND3D")) esm.skipHSub(); - mActorData.mHasANIS = false; - if (esm.isNextSub("ANIS")) - { - mActorData.mHasANIS = true; - esm.getHTSized<8>(mActorData.mANIS); - } + mActorData.mHasANIS + = esm.getHNOT("ANIS", mActorData.mANIS.mGroupIndex, mActorData.mANIS.mUnknown, mActorData.mANIS.mTime); if (esm.isNextSub("LVCR")) { @@ -155,13 +145,13 @@ namespace ESSImport // DATA should occur for all references, except levelled creature spawners // I've seen DATA *twice* on a creature record, and with the exact same content too! weird // alarmvoi0000.ess - esm.getHNOTSized<24>(mPos, "DATA"); - esm.getHNOTSized<24>(mPos, "DATA"); + for (int i = 0; i < 2; ++i) + esm.getHNOT("DATA", mPos.pos, mPos.rot); mDeleted = 0; if (esm.isNextSub("DELE")) { - unsigned int deleted; + uint32_t deleted; esm.getHT(deleted); mDeleted = ((deleted >> 24) & 0x2) != 0; // the other 3 bytes seem to be uninitialized garbage } diff --git a/apps/essimporter/importcntc.cpp b/apps/essimporter/importcntc.cpp index 41f4e50101..34c99babef 100644 --- a/apps/essimporter/importcntc.cpp +++ b/apps/essimporter/importcntc.cpp @@ -1,6 +1,7 @@ #include "importcntc.hpp" #include +#include namespace ESSImport { diff --git a/apps/essimporter/importcntc.hpp b/apps/essimporter/importcntc.hpp index 1bc7d94bd5..6ee843805e 100644 --- a/apps/essimporter/importcntc.hpp +++ b/apps/essimporter/importcntc.hpp @@ -14,7 +14,7 @@ namespace ESSImport /// Changed container contents struct CNTC { - int mIndex; + int32_t mIndex; Inventory mInventory; diff --git a/apps/essimporter/importcrec.hpp b/apps/essimporter/importcrec.hpp index 77933eafe8..5217f4edc4 100644 --- a/apps/essimporter/importcrec.hpp +++ b/apps/essimporter/importcrec.hpp @@ -3,6 +3,7 @@ #include "importinventory.hpp" #include +#include namespace ESM { @@ -15,7 +16,7 @@ namespace ESSImport /// Creature changes struct CREC { - int mIndex; + int32_t mIndex; Inventory mInventory; ESM::AIPackageList mAiPackages; diff --git a/apps/essimporter/importdial.cpp b/apps/essimporter/importdial.cpp index 6c45f9d059..43905738a1 100644 --- a/apps/essimporter/importdial.cpp +++ b/apps/essimporter/importdial.cpp @@ -8,11 +8,11 @@ namespace ESSImport void DIAL::load(ESM::ESMReader& esm) { // See ESM::Dialogue::Type enum, not sure why we would need this here though - int type = 0; + int32_t type = 0; esm.getHNOT(type, "DATA"); // Deleted dialogue in a savefile. No clue what this means... - int deleted = 0; + int32_t deleted = 0; esm.getHNOT(deleted, "DELE"); mIndex = 0; diff --git a/apps/essimporter/importdial.hpp b/apps/essimporter/importdial.hpp index 9a1e882332..b8b6fd536a 100644 --- a/apps/essimporter/importdial.hpp +++ b/apps/essimporter/importdial.hpp @@ -1,5 +1,8 @@ #ifndef OPENMW_ESSIMPORT_IMPORTDIAL_H #define OPENMW_ESSIMPORT_IMPORTDIAL_H + +#include + namespace ESM { class ESMReader; @@ -10,7 +13,7 @@ namespace ESSImport struct DIAL { - int mIndex; // Journal index + int32_t mIndex; // Journal index void load(ESM::ESMReader& esm); }; diff --git a/apps/essimporter/importgame.cpp b/apps/essimporter/importgame.cpp index 5295d2a1e8..8161a20031 100644 --- a/apps/essimporter/importgame.cpp +++ b/apps/essimporter/importgame.cpp @@ -9,17 +9,17 @@ namespace ESSImport { esm.getSubNameIs("GMDT"); esm.getSubHeader(); - if (esm.getSubSize() == 92) - { - esm.getExact(&mGMDT, 92); - mGMDT.mSecundaPhase = 0; - } - else if (esm.getSubSize() == 96) - { - esm.getTSized<96>(mGMDT); - } - else - esm.fail("unexpected subrecord size for GAME.GMDT"); + bool hasSecundaPhase = esm.getSubSize() == 96; + esm.getT(mGMDT.mCellName); + esm.getT(mGMDT.mFogColour); + esm.getT(mGMDT.mFogDensity); + esm.getT(mGMDT.mCurrentWeather); + esm.getT(mGMDT.mNextWeather); + esm.getT(mGMDT.mWeatherTransition); + esm.getT(mGMDT.mTimeOfNextTransition); + esm.getT(mGMDT.mMasserPhase); + if (hasSecundaPhase) + esm.getT(mGMDT.mSecundaPhase); mGMDT.mWeatherTransition &= (0x000000ff); mGMDT.mSecundaPhase &= (0x000000ff); diff --git a/apps/essimporter/importgame.hpp b/apps/essimporter/importgame.hpp index 8b26b9d8bd..276060ae4c 100644 --- a/apps/essimporter/importgame.hpp +++ b/apps/essimporter/importgame.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_ESSIMPORT_GAME_H #define OPENMW_ESSIMPORT_GAME_H +#include + namespace ESM { class ESMReader; @@ -15,12 +17,12 @@ namespace ESSImport struct GMDT { char mCellName[64]{}; - int mFogColour{ 0 }; + int32_t mFogColour{ 0 }; float mFogDensity{ 0.f }; - int mCurrentWeather{ 0 }, mNextWeather{ 0 }; - int mWeatherTransition{ 0 }; // 0-100 transition between weathers, top 3 bytes may be garbage + int32_t mCurrentWeather{ 0 }, mNextWeather{ 0 }; + int32_t mWeatherTransition{ 0 }; // 0-100 transition between weathers, top 3 bytes may be garbage float mTimeOfNextTransition{ 0.f }; // weather changes when gamehour == timeOfNextTransition - int mMasserPhase{ 0 }, mSecundaPhase{ 0 }; // top 3 bytes may be garbage + int32_t mMasserPhase{ 0 }, mSecundaPhase{ 0 }; // top 3 bytes may be garbage }; GMDT mGMDT; diff --git a/apps/essimporter/importinventory.cpp b/apps/essimporter/importinventory.cpp index 9d71c04f2a..f1db301bd0 100644 --- a/apps/essimporter/importinventory.cpp +++ b/apps/essimporter/importinventory.cpp @@ -12,7 +12,7 @@ namespace ESSImport while (esm.isNextSub("NPCO")) { ContItem contItem; - esm.getHTSized<36>(contItem); + esm.getHT(contItem.mCount, contItem.mItem.mData); InventoryItem item; item.mId = contItem.mItem.toString(); @@ -28,7 +28,7 @@ namespace ESSImport bool newStack = esm.isNextSub("XIDX"); if (newStack) { - unsigned int idx; + uint32_t idx; esm.getHT(idx); separateStacks = true; item.mCount = 1; @@ -40,7 +40,7 @@ namespace ESSImport bool isDeleted = false; item.ESM::CellRef::loadData(esm, isDeleted); - int charge = -1; + int32_t charge = -1; esm.getHNOT(charge, "XHLT"); item.mChargeInt = charge; @@ -60,7 +60,7 @@ namespace ESSImport // this is currently not handled properly. esm.getSubHeader(); - int itemIndex; // index of the item in the NPCO list + int32_t itemIndex; // index of the item in the NPCO list esm.getT(itemIndex); if (itemIndex < 0 || itemIndex >= int(mItems.size())) @@ -68,7 +68,7 @@ namespace ESSImport // appears to be a relative index for only the *possible* slots this item can be equipped in, // i.e. 0 most of the time - int slotIndex; + int32_t slotIndex; esm.getT(slotIndex); mItems[itemIndex].mRelativeEquipmentSlot = slotIndex; diff --git a/apps/essimporter/importinventory.hpp b/apps/essimporter/importinventory.hpp index 7a11b3f0a0..7261e64f68 100644 --- a/apps/essimporter/importinventory.hpp +++ b/apps/essimporter/importinventory.hpp @@ -1,6 +1,7 @@ #ifndef OPENMW_ESSIMPORT_IMPORTINVENTORY_H #define OPENMW_ESSIMPORT_IMPORTINVENTORY_H +#include #include #include @@ -19,7 +20,7 @@ namespace ESSImport struct ContItem { - int mCount; + int32_t mCount; ESM::NAME32 mItem; }; @@ -28,8 +29,8 @@ namespace ESSImport struct InventoryItem : public ESM::CellRef { std::string mId; - int mCount; - int mRelativeEquipmentSlot; + int32_t mCount; + int32_t mRelativeEquipmentSlot; SCRI mSCRI; }; std::vector mItems; diff --git a/apps/essimporter/importklst.cpp b/apps/essimporter/importklst.cpp index d4cfc7f769..2d5e09e913 100644 --- a/apps/essimporter/importklst.cpp +++ b/apps/essimporter/importklst.cpp @@ -10,7 +10,7 @@ namespace ESSImport while (esm.isNextSub("KNAM")) { std::string refId = esm.getHString(); - int count; + int32_t count; esm.getHNT(count, "CNAM"); mKillCounter[refId] = count; } diff --git a/apps/essimporter/importklst.hpp b/apps/essimporter/importklst.hpp index 7c1ff03bb6..9cdb2d701b 100644 --- a/apps/essimporter/importklst.hpp +++ b/apps/essimporter/importklst.hpp @@ -1,6 +1,7 @@ #ifndef OPENMW_ESSIMPORT_KLST_H #define OPENMW_ESSIMPORT_KLST_H +#include #include #include @@ -18,9 +19,9 @@ namespace ESSImport void load(ESM::ESMReader& esm); /// RefId, kill count - std::map mKillCounter; + std::map mKillCounter; - int mWerewolfKills; + int32_t mWerewolfKills; }; } diff --git a/apps/essimporter/importnpcc.cpp b/apps/essimporter/importnpcc.cpp index c115040074..c1a53b6cef 100644 --- a/apps/essimporter/importnpcc.cpp +++ b/apps/essimporter/importnpcc.cpp @@ -7,7 +7,7 @@ namespace ESSImport void NPCC::load(ESM::ESMReader& esm) { - esm.getHNTSized<8>(mNPDT, "NPDT"); + esm.getHNT("NPDT", mNPDT.mDisposition, mNPDT.unknown, mNPDT.mReputation, mNPDT.unknown2, mNPDT.mIndex); while (esm.isNextSub("AI_W") || esm.isNextSub("AI_E") || esm.isNextSub("AI_T") || esm.isNextSub("AI_F") || esm.isNextSub("AI_A")) diff --git a/apps/essimporter/importnpcc.hpp b/apps/essimporter/importnpcc.hpp index 762add1906..47925226e4 100644 --- a/apps/essimporter/importnpcc.hpp +++ b/apps/essimporter/importnpcc.hpp @@ -2,6 +2,7 @@ #define OPENMW_ESSIMPORT_NPCC_H #include +#include #include "importinventory.hpp" @@ -21,7 +22,7 @@ namespace ESSImport unsigned char unknown; unsigned char mReputation; unsigned char unknown2; - int mIndex; + int32_t mIndex; } mNPDT; Inventory mInventory; diff --git a/apps/essimporter/importplayer.cpp b/apps/essimporter/importplayer.cpp index 165926d15a..f4c280541d 100644 --- a/apps/essimporter/importplayer.cpp +++ b/apps/essimporter/importplayer.cpp @@ -19,7 +19,12 @@ namespace ESSImport mMNAM = esm.getHString(); } - esm.getHNTSized<212>(mPNAM, "PNAM"); + esm.getHNT("PNAM", mPNAM.mPlayerFlags, mPNAM.mLevelProgress, mPNAM.mSkillProgress, mPNAM.mSkillIncreases, + mPNAM.mTelekinesisRangeBonus, mPNAM.mVisionBonus, mPNAM.mDetectKeyMagnitude, + mPNAM.mDetectEnchantmentMagnitude, mPNAM.mDetectAnimalMagnitude, mPNAM.mMarkLocation.mX, + mPNAM.mMarkLocation.mY, mPNAM.mMarkLocation.mZ, mPNAM.mMarkLocation.mRotZ, mPNAM.mMarkLocation.mCellX, + mPNAM.mMarkLocation.mCellY, mPNAM.mUnknown3, mPNAM.mVerticalRotation.mData, mPNAM.mSpecIncreases, + mPNAM.mUnknown4); if (esm.isNextSub("SNAM")) esm.skipHSub(); @@ -50,12 +55,7 @@ namespace ESSImport if (esm.isNextSub("NAM3")) esm.skipHSub(); - mHasENAM = false; - if (esm.isNextSub("ENAM")) - { - mHasENAM = true; - esm.getHTSized<8>(mENAM); - } + mHasENAM = esm.getHNOT("ENAM", mENAM.mCellX, mENAM.mCellY); if (esm.isNextSub("LNAM")) esm.skipHSub(); @@ -63,16 +63,12 @@ namespace ESSImport while (esm.isNextSub("FNAM")) { FNAM fnam; - esm.getHTSized<44>(fnam); + esm.getHT( + fnam.mRank, fnam.mUnknown1, fnam.mReputation, fnam.mFlags, fnam.mUnknown2, fnam.mFactionName.mData); mFactions.push_back(fnam); } - mHasAADT = false; - if (esm.isNextSub("AADT")) // Attack animation data? - { - mHasAADT = true; - esm.getHTSized<44>(mAADT); - } + mHasAADT = esm.getHNOT("AADT", mAADT.animGroupIndex, mAADT.mUnknown5); // Attack animation data? if (esm.isNextSub("KNAM")) esm.skipHSub(); // assigned Quick Keys, I think diff --git a/apps/essimporter/importplayer.hpp b/apps/essimporter/importplayer.hpp index 0fb820cb64..89957bf4b4 100644 --- a/apps/essimporter/importplayer.hpp +++ b/apps/essimporter/importplayer.hpp @@ -1,6 +1,7 @@ #ifndef OPENMW_ESSIMPORT_PLAYER_H #define OPENMW_ESSIMPORT_PLAYER_H +#include #include #include @@ -17,7 +18,7 @@ namespace ESSImport /// Other player data struct PCDT { - int mBounty; + int32_t mBounty; std::string mBirthsign; std::vector mKnownDialogueTopics; @@ -41,13 +42,11 @@ namespace ESSImport PlayerFlags_LevitationDisabled = 0x80000 }; -#pragma pack(push) -#pragma pack(1) struct FNAM { unsigned char mRank; unsigned char mUnknown1[3]; - int mReputation; + int32_t mReputation; unsigned char mFlags; // 0x1: unknown, 0x2: expelled unsigned char mUnknown2[3]; ESM::NAME32 mFactionName; @@ -59,7 +58,7 @@ namespace ESSImport { float mX, mY, mZ; // worldspace position float mRotZ; // Z angle in radians - int mCellX, mCellY; // grid coordinates; for interior cells this is always (0, 0) + int32_t mCellX, mCellY; // grid coordinates; for interior cells this is always (0, 0) }; struct Rotation @@ -67,15 +66,15 @@ namespace ESSImport float mData[3][3]; }; - int mPlayerFlags; // controls, camera and draw state - unsigned int mLevelProgress; + int32_t mPlayerFlags; // controls, camera and draw state + uint32_t mLevelProgress; float mSkillProgress[27]; // skill progress, non-uniform scaled unsigned char mSkillIncreases[8]; // number of skill increases for each attribute - int mTelekinesisRangeBonus; // in units; seems redundant + int32_t mTelekinesisRangeBonus; // in units; seems redundant float mVisionBonus; // range: <0.0, 1.0>; affected by light spells and Get/Mod/SetPCVisionBonus - int mDetectKeyMagnitude; // seems redundant - int mDetectEnchantmentMagnitude; // seems redundant - int mDetectAnimalMagnitude; // seems redundant + int32_t mDetectKeyMagnitude; // seems redundant + int32_t mDetectEnchantmentMagnitude; // seems redundant + int32_t mDetectAnimalMagnitude; // seems redundant MarkLocation mMarkLocation; unsigned char mUnknown3[4]; Rotation mVerticalRotation; @@ -85,16 +84,15 @@ namespace ESSImport struct ENAM { - int mCellX; - int mCellY; + int32_t mCellX; + int32_t mCellY; }; struct AADT // 44 bytes { - int animGroupIndex; // See convertANIS() for the mapping. + int32_t animGroupIndex; // See convertANIS() for the mapping. unsigned char mUnknown5[40]; }; -#pragma pack(pop) std::vector mFactions; PNAM mPNAM; diff --git a/apps/essimporter/importproj.cpp b/apps/essimporter/importproj.cpp index f9a92095e0..a09ade81dd 100644 --- a/apps/essimporter/importproj.cpp +++ b/apps/essimporter/importproj.cpp @@ -10,7 +10,9 @@ namespace ESSImport while (esm.isNextSub("PNAM")) { PNAM pnam; - esm.getHTSized<184>(pnam); + esm.getHT(pnam.mAttackStrength, pnam.mSpeed, pnam.mUnknown, pnam.mFlightTime, pnam.mSplmIndex, + pnam.mUnknown2, pnam.mVelocity.mValues, pnam.mPosition.mValues, pnam.mUnknown3, pnam.mActorId.mData, + pnam.mArrowId.mData, pnam.mBowId.mData); mProjectiles.push_back(pnam); } } diff --git a/apps/essimporter/importproj.h b/apps/essimporter/importproj.h index d1c544f66f..a2e03b5ba3 100644 --- a/apps/essimporter/importproj.h +++ b/apps/essimporter/importproj.h @@ -3,6 +3,7 @@ #include #include +#include #include namespace ESM @@ -16,15 +17,13 @@ namespace ESSImport struct PROJ { -#pragma pack(push) -#pragma pack(1) struct PNAM // 184 bytes { float mAttackStrength; float mSpeed; unsigned char mUnknown[4 * 2]; float mFlightTime; - int mSplmIndex; // reference to a SPLM record (0 for ballistic projectiles) + int32_t mSplmIndex; // reference to a SPLM record (0 for ballistic projectiles) unsigned char mUnknown2[4]; ESM::Vector3 mVelocity; ESM::Vector3 mPosition; @@ -35,7 +34,6 @@ namespace ESSImport bool isMagic() const { return mSplmIndex != 0; } }; -#pragma pack(pop) std::vector mProjectiles; diff --git a/apps/essimporter/importscpt.cpp b/apps/essimporter/importscpt.cpp index 746d0b90e7..8fe4afd336 100644 --- a/apps/essimporter/importscpt.cpp +++ b/apps/essimporter/importscpt.cpp @@ -7,7 +7,8 @@ namespace ESSImport void SCPT::load(ESM::ESMReader& esm) { - esm.getHNTSized<52>(mSCHD, "SCHD"); + esm.getHNT("SCHD", mSCHD.mName.mData, mSCHD.mData.mNumShorts, mSCHD.mData.mNumLongs, mSCHD.mData.mNumFloats, + mSCHD.mData.mScriptDataSize, mSCHD.mData.mStringTableSize); mSCRI.load(esm); diff --git a/apps/essimporter/importscpt.hpp b/apps/essimporter/importscpt.hpp index 8f60532447..af383b674c 100644 --- a/apps/essimporter/importscpt.hpp +++ b/apps/essimporter/importscpt.hpp @@ -3,6 +3,8 @@ #include "importscri.hpp" +#include + #include #include @@ -29,7 +31,7 @@ namespace ESSImport SCRI mSCRI; bool mRunning; - int mRefNum; // Targeted reference, -1: no reference + int32_t mRefNum; // Targeted reference, -1: no reference void load(ESM::ESMReader& esm); }; diff --git a/apps/essimporter/importscri.cpp b/apps/essimporter/importscri.cpp index b6c1d4094c..c0425cef32 100644 --- a/apps/essimporter/importscri.cpp +++ b/apps/essimporter/importscri.cpp @@ -9,7 +9,7 @@ namespace ESSImport { mScript = esm.getHNOString("SCRI"); - int numShorts = 0, numLongs = 0, numFloats = 0; + int32_t numShorts = 0, numLongs = 0, numFloats = 0; if (esm.isNextSub("SLCS")) { esm.getSubHeader(); @@ -23,7 +23,7 @@ namespace ESSImport esm.getSubHeader(); for (int i = 0; i < numShorts; ++i) { - short val; + int16_t val; esm.getT(val); mShorts.push_back(val); } @@ -35,7 +35,7 @@ namespace ESSImport esm.getSubHeader(); for (int i = 0; i < numLongs; ++i) { - int val; + int32_t val; esm.getT(val); mLongs.push_back(val); } diff --git a/apps/essimporter/importscri.hpp b/apps/essimporter/importscri.hpp index 73d8942f81..0c83a4d3be 100644 --- a/apps/essimporter/importscri.hpp +++ b/apps/essimporter/importscri.hpp @@ -3,6 +3,7 @@ #include +#include #include namespace ESM diff --git a/apps/essimporter/importsplm.cpp b/apps/essimporter/importsplm.cpp index a0478f4d92..6019183f83 100644 --- a/apps/essimporter/importsplm.cpp +++ b/apps/essimporter/importsplm.cpp @@ -11,13 +11,15 @@ namespace ESSImport { ActiveSpell spell; esm.getHT(spell.mIndex); - esm.getHNTSized<160>(spell.mSPDT, "SPDT"); + esm.getHNT("SPDT", spell.mSPDT.mType, spell.mSPDT.mId.mData, spell.mSPDT.mUnknown, + spell.mSPDT.mCasterId.mData, spell.mSPDT.mSourceId.mData, spell.mSPDT.mUnknown2); spell.mTarget = esm.getHNOString("TNAM"); while (esm.isNextSub("NPDT")) { ActiveEffect effect; - esm.getHTSized<56>(effect.mNPDT); + esm.getHT(effect.mNPDT.mAffectedActorId.mData, effect.mNPDT.mUnknown, effect.mNPDT.mMagnitude, + effect.mNPDT.mSecondsActive, effect.mNPDT.mUnknown2); // Effect-specific subrecords can follow: // - INAM for disintegration and bound effects diff --git a/apps/essimporter/importsplm.h b/apps/essimporter/importsplm.h index 8187afb131..762e32d9da 100644 --- a/apps/essimporter/importsplm.h +++ b/apps/essimporter/importsplm.h @@ -2,6 +2,7 @@ #define OPENMW_ESSIMPORT_IMPORTSPLM_H #include +#include #include namespace ESM @@ -15,11 +16,9 @@ namespace ESSImport struct SPLM { -#pragma pack(push) -#pragma pack(1) struct SPDT // 160 bytes { - int mType; // 1 = spell, 2 = enchantment, 3 = potion + int32_t mType; // 1 = spell, 2 = enchantment, 3 = potion ESM::NAME32 mId; // base ID of a spell/enchantment/potion unsigned char mUnknown[4 * 4]; ESM::NAME32 mCasterId; @@ -31,31 +30,29 @@ namespace ESSImport { ESM::NAME32 mAffectedActorId; unsigned char mUnknown[4 * 2]; - int mMagnitude; + int32_t mMagnitude; float mSecondsActive; unsigned char mUnknown2[4 * 2]; }; struct INAM // 40 bytes { - int mUnknown; + int32_t mUnknown; unsigned char mUnknown2; ESM::FixedString<35> mItemId; // disintegrated item / bound item / item to re-equip after expiration }; struct CNAM // 36 bytes { - int mUnknown; // seems to always be 0 + int32_t mUnknown; // seems to always be 0 ESM::NAME32 mSummonedOrCommandedActor[32]; }; struct VNAM // 4 bytes { - int mUnknown; + int32_t mUnknown; }; -#pragma pack(pop) - struct ActiveEffect { NPDT mNPDT; @@ -63,7 +60,7 @@ namespace ESSImport struct ActiveSpell { - int mIndex; + int32_t mIndex; SPDT mSPDT; std::string mTarget; std::vector mActiveEffects; diff --git a/apps/essimporter/main.cpp b/apps/essimporter/main.cpp index 7d3ad10bb1..f0833e9d81 100644 --- a/apps/essimporter/main.cpp +++ b/apps/essimporter/main.cpp @@ -42,8 +42,8 @@ Allowed options)"); Files::ConfigurationManager cfgManager(true); cfgManager.readConfiguration(variables, desc); - const auto essFile = variables["mwsave"].as(); - const auto outputFile = variables["output"].as(); + const auto& essFile = variables["mwsave"].as(); + const auto& outputFile = variables["output"].as(); std::string encoding = variables["encoding"].as(); ESSImport::Importer importer(essFile, outputFile, encoding); diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index daae65dc66..aa3970efdb 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -35,13 +35,12 @@ set(LAUNCHER_HEADER # Headers that must be pre-processed set(LAUNCHER_UI - ${CMAKE_SOURCE_DIR}/files/ui/datafilespage.ui - ${CMAKE_SOURCE_DIR}/files/ui/graphicspage.ui - ${CMAKE_SOURCE_DIR}/files/ui/mainwindow.ui - ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui - ${CMAKE_SOURCE_DIR}/files/ui/importpage.ui - ${CMAKE_SOURCE_DIR}/files/ui/settingspage.ui - ${CMAKE_SOURCE_DIR}/files/ui/directorypicker.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/datafilespage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/graphicspage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/mainwindow.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/importpage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/settingspage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/directorypicker.ui ) source_group(launcher FILES ${LAUNCHER} ${LAUNCHER_HEADER}) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 4d3f0cc64f..114221ce92 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -125,27 +125,6 @@ namespace Launcher { return Settings::navigator().mMaxNavmeshdbFileSize / (1024 * 1024); } - - std::optional findFirstPath(const QStringList& directories, const QString& fileName) - { - for (const QString& directoryPath : directories) - { - const QString filePath = QDir(directoryPath).absoluteFilePath(fileName); - if (QFile::exists(filePath)) - return filePath; - } - return std::nullopt; - } - - QStringList findAllFilePaths(const QStringList& directories, const QStringList& fileNames) - { - QStringList result; - result.reserve(fileNames.size()); - for (const QString& fileName : fileNames) - if (const auto filepath = findFirstPath(directories, fileName)) - result.append(*filepath); - return result; - } } } @@ -164,11 +143,14 @@ Launcher::DataFilesPage::DataFilesPage(const Files::ConfigurationManager& cfg, C const QString encoding = mGameSettings.value("encoding", "win1252"); mSelector->setEncoding(encoding); - QStringList languages; - languages << tr("English") << tr("French") << tr("German") << tr("Italian") << tr("Polish") << tr("Russian") - << tr("Spanish"); + QVector> languages = { { "English", tr("English") }, { "French", tr("French") }, + { "German", tr("German") }, { "Italian", tr("Italian") }, { "Polish", tr("Polish") }, + { "Russian", tr("Russian") }, { "Spanish", tr("Spanish") } }; - mSelector->languageBox()->addItems(languages); + for (auto lang : languages) + { + mSelector->languageBox()->addItem(lang.second, lang.first); + } mNewProfileDialog = new TextInputDialog(tr("New Content List"), tr("Content List name:"), this); mCloneProfileDialog = new TextInputDialog(tr("Clone Content List"), tr("Content List name:"), this); @@ -254,9 +236,17 @@ bool Launcher::DataFilesPage::loadSettings() if (!currentProfile.isEmpty()) addProfile(currentProfile, true); - const int index = mSelector->languageBox()->findText(mLauncherSettings.getLanguage()); - if (index != -1) - mSelector->languageBox()->setCurrentIndex(index); + auto language = mLauncherSettings.getLanguage(); + + for (int i = 0; i < mSelector->languageBox()->count(); ++i) + { + QString languageItem = mSelector->languageBox()->itemData(i).toString(); + if (language == languageItem) + { + mSelector->languageBox()->setCurrentIndex(i); + break; + } + } return true; } @@ -301,12 +291,14 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) auto row = ui.directoryListWidget->count() - 1; auto* item = ui.directoryListWidget->item(row); - // Display new content with green background + // Display new content with custom formatting if (mNewDataDirs.contains(canonicalDirPath)) { tooltip += "Will be added to the current profile\n"; - item->setBackground(Qt::green); - item->setForeground(Qt::black); + QFont font = item->font(); + font.setBold(true); + font.setItalic(true); + item->setFont(font); } // deactivate data-local and global data directory: they are always included @@ -353,8 +345,7 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) row++; } - mSelector->setProfileContent( - findAllFilePaths(directories, mLauncherSettings.getContentListFiles(contentModelName))); + mSelector->setProfileContent(mLauncherSettings.getContentListFiles(contentModelName)); } void Launcher::DataFilesPage::saveSettings(const QString& profile) @@ -384,7 +375,7 @@ void Launcher::DataFilesPage::saveSettings(const QString& profile) mLauncherSettings.setContentList(profileName, dirList, selectedArchivePaths(), fileNames); mGameSettings.setContentList(dirList, selectedArchivePaths(), fileNames); - QString language(mSelector->languageBox()->currentText()); + QString language(mSelector->languageBox()->currentData().toString()); mLauncherSettings.setLanguage(language); @@ -737,8 +728,11 @@ void Launcher::DataFilesPage::addArchive(const QString& name, Qt::CheckState sel ui.archiveListWidget->item(row)->setCheckState(selected); if (mKnownArchives.filter(name).isEmpty()) // XXX why contains doesn't work here ??? { - ui.archiveListWidget->item(row)->setBackground(Qt::green); - ui.archiveListWidget->item(row)->setForeground(Qt::black); + auto item = ui.archiveListWidget->item(row); + QFont font = item->font(); + font.setBold(true); + font.setItalic(true); + item->setFont(font); } } diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index 84d5049d6c..b360c215e6 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -154,8 +154,13 @@ bool Launcher::GraphicsPage::loadSettings() if (Settings::shadows().mEnableIndoorShadows) indoorShadowsCheckBox->setCheckState(Qt::Checked); - shadowComputeSceneBoundsComboBox->setCurrentIndex( - shadowComputeSceneBoundsComboBox->findText(QString(tr(Settings::shadows().mComputeSceneBounds.get().c_str())))); + const auto& boundMethod = Settings::shadows().mComputeSceneBounds.get(); + if (boundMethod == "bounds") + shadowComputeSceneBoundsComboBox->setCurrentIndex(0); + else if (boundMethod == "primitives") + shadowComputeSceneBoundsComboBox->setCurrentIndex(1); + else + shadowComputeSceneBoundsComboBox->setCurrentIndex(2); const int shadowDistLimit = Settings::shadows().mMaximumShadowMapDistance; if (shadowDistLimit > 0) @@ -254,7 +259,14 @@ void Launcher::GraphicsPage::saveSettings() Settings::shadows().mEnableIndoorShadows.set(indoorShadowsCheckBox->checkState() != Qt::Unchecked); Settings::shadows().mShadowMapResolution.set(shadowResolutionComboBox->currentText().toInt()); - Settings::shadows().mComputeSceneBounds.set(shadowComputeSceneBoundsComboBox->currentText().toStdString()); + + auto index = shadowComputeSceneBoundsComboBox->currentIndex(); + if (index == 0) + Settings::shadows().mComputeSceneBounds.set("bounds"); + else if (index == 1) + Settings::shadows().mComputeSceneBounds.set("primitives"); + else + Settings::shadows().mComputeSceneBounds.set("none"); } QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen) diff --git a/apps/launcher/importpage.cpp b/apps/launcher/importpage.cpp index fa91ad1654..44c5867c0d 100644 --- a/apps/launcher/importpage.cpp +++ b/apps/launcher/importpage.cpp @@ -104,9 +104,9 @@ void Launcher::ImportPage::on_importerButton_clicked() msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("

Could not open or create %1 for writing

\ -

Please make sure you have the right permissions \ - and try again.

") + tr("

Could not open or create %1 for writing

" + "

Please make sure you have the right permissions " + "and try again.

") .arg(file.fileName())); msgBox.exec(); return; diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index 4aac90fb6e..78323458ce 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -41,11 +41,6 @@ int runLauncher(int argc, char* argv[]) appTranslator.load(":/translations/" + locale + ".qm"); app.installTranslator(&appTranslator); - // Now we make sure the current dir is set to application path - QDir dir(QCoreApplication::applicationDirPath()); - - QDir::setCurrent(dir.absolutePath()); - Launcher::MainDialog mainWin(configurationManager); Launcher::FirstRunDialogResult result = mainWin.showFirstRunDialog(); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index bba3bbe5e1..5d558ef38f 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -118,13 +118,14 @@ Launcher::FirstRunDialogResult Launcher::MainDialog::showFirstRunDialog() const auto& userConfigDir = mCfgMgr.getUserConfigPath(); if (!exists(userConfigDir)) { - if (!create_directories(userConfigDir)) + std::error_code ec; + if (!create_directories(userConfigDir, ec)) { - cfgError(tr("Error opening OpenMW configuration file"), - tr("
Could not create directory %0

\ - Please make sure you have the right permissions \ - and try again.
") - .arg(Files::pathToQString(canonical(userConfigDir)))); + cfgError(tr("Error creating OpenMW configuration directory: code %0").arg(ec.value()), + tr("
Could not create directory %0

" + "%1
") + .arg(Files::pathToQString(userConfigDir)) + .arg(QString(ec.message().c_str()))); return FirstRunDialogResultFailure; } } @@ -136,10 +137,10 @@ Launcher::FirstRunDialogResult Launcher::MainDialog::showFirstRunDialog() msgBox.setIcon(QMessageBox::Question); msgBox.setStandardButtons(QMessageBox::NoButton); msgBox.setText( - tr("

Welcome to OpenMW!

\ -

It is recommended to run the Installation Wizard.

\ -

The Wizard will let you select an existing Morrowind installation, \ - or install Morrowind for OpenMW to use.

")); + tr("

Welcome to OpenMW!

" + "

It is recommended to run the Installation Wizard.

" + "

The Wizard will let you select an existing Morrowind installation, " + "or install Morrowind for OpenMW to use.

")); QAbstractButton* wizardButton = msgBox.addButton(tr("Run &Installation Wizard"), QMessageBox::AcceptRole); // ActionRole doesn't work?! @@ -297,9 +298,9 @@ bool Launcher::MainDialog::setupLauncherSettings() if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { cfgError(tr("Error opening OpenMW configuration file"), - tr("
Could not open %0 for reading:

%1

\ - Please make sure you have the right permissions \ - and try again.
") + tr("
Could not open %0 for reading:

%1

" + "Please make sure you have the right permissions " + "and try again.
") .arg(file.fileName()) .arg(file.errorString())); return false; @@ -327,9 +328,9 @@ bool Launcher::MainDialog::setupGameSettings() if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { cfgError(tr("Error opening OpenMW configuration file"), - tr("
Could not open %0 for reading

\ - Please make sure you have the right permissions \ - and try again.
") + tr("
Could not open %0 for reading

" + "Please make sure you have the right permissions " + "and try again.
") .arg(file.fileName())); return {}; } @@ -388,8 +389,8 @@ bool Launcher::MainDialog::setupGameData() msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::NoButton); msgBox.setText( - tr("
Could not find the Data Files location

\ - The directory containing the data files was not found.")); + tr("
Could not find the Data Files location

" + "The directory containing the data files was not found.")); QAbstractButton* wizardButton = msgBox.addButton(tr("Run &Installation Wizard..."), QMessageBox::ActionRole); QAbstractButton* skipButton = msgBox.addButton(tr("Skip"), QMessageBox::RejectRole); @@ -419,8 +420,8 @@ bool Launcher::MainDialog::setupGraphicsSettings() catch (std::exception& e) { cfgError(tr("Error reading OpenMW configuration files"), - tr("
The problem may be due to an incomplete installation of OpenMW.
\ - Reinstalling OpenMW may resolve the problem.
") + tr("
The problem may be due to an incomplete installation of OpenMW.
" + "Reinstalling OpenMW may resolve the problem.
") + e.what()); return false; } @@ -457,13 +458,14 @@ bool Launcher::MainDialog::writeSettings() if (!exists(userPath)) { - if (!create_directories(userPath)) + std::error_code ec; + if (!create_directories(userPath, ec)) { - cfgError(tr("Error creating OpenMW configuration directory"), - tr("
Could not create %0

\ - Please make sure you have the right permissions \ - and try again.
") - .arg(Files::pathToQString(userPath))); + cfgError(tr("Error creating OpenMW configuration directory: code %0").arg(ec.value()), + tr("
Could not create directory %0

" + "%1
") + .arg(Files::pathToQString(userPath)) + .arg(QString(ec.message().c_str()))); return false; } } @@ -479,9 +481,9 @@ bool Launcher::MainDialog::writeSettings() { // File cannot be opened or created cfgError(tr("Error writing OpenMW configuration file"), - tr("
Could not open or create %0 for writing

\ - Please make sure you have the right permissions \ - and try again.
") + tr("
Could not open or create %0 for writing

" + "Please make sure you have the right permissions " + "and try again.
") .arg(file.fileName())); return false; } @@ -510,9 +512,9 @@ bool Launcher::MainDialog::writeSettings() { // File cannot be opened or created cfgError(tr("Error writing Launcher configuration file"), - tr("
Could not open or create %0 for writing

\ - Please make sure you have the right permissions \ - and try again.
") + tr("
Could not open or create %0 for writing

" + "Please make sure you have the right permissions " + "and try again.
") .arg(file.fileName())); return false; } @@ -562,8 +564,8 @@ void Launcher::MainDialog::play() msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("
You do not have a game file selected.

\ - OpenMW will not start without a game file selected.
")); + tr("
You do not have a game file selected.

" + "OpenMW will not start without a game file selected.
")); msgBox.exec(); return; } diff --git a/files/ui/datafilespage.ui b/apps/launcher/ui/datafilespage.ui similarity index 94% rename from files/ui/datafilespage.ui rename to apps/launcher/ui/datafilespage.ui index 239df34961..249207123e 100644 --- a/files/ui/datafilespage.ui +++ b/apps/launcher/ui/datafilespage.ui @@ -6,7 +6,7 @@ 0 0 - 571 + 573 384 @@ -30,7 +30,7 @@ - <html><head/><body><p><span style=" font-style:italic;">note: content files that are not part of current Content List are </span><span style=" font-style:italic; background-color:#00ff00;">highlighted</span></p></body></html> + <html><head/><body><p>note: content files that are not part of current Content List are <span style=" font-style:italic;font-weight: bold">highlighted</span></p></body></html> @@ -57,7 +57,7 @@ - <html><head/><body><p><span style=" font-style:italic;">note: directories that are not part of current Content List are </span><span style=" font-style:italic; background-color:#00ff00;">highlighted</span></p></body></html> + <html><head/><body><p>note: directories that are not part of current Content List are <span style=" font-style:italic;font-weight: bold">highlighted</span></p></body></html> @@ -210,7 +210,7 @@ - <html><head/><body><p><span style=" font-style:italic;">note: archives that are not part of current Content List are </span><span style=" font-style:italic; background-color:#00ff00;">highlighted</span></p></body></html> + <html><head/><body><p>note: archives that are not part of current Content List are <span style=" font-style:italic;font-weight: bold">highlighted</span></p></body></html> diff --git a/files/ui/directorypicker.ui b/apps/launcher/ui/directorypicker.ui similarity index 100% rename from files/ui/directorypicker.ui rename to apps/launcher/ui/directorypicker.ui diff --git a/files/ui/graphicspage.ui b/apps/launcher/ui/graphicspage.ui similarity index 99% rename from files/ui/graphicspage.ui rename to apps/launcher/ui/graphicspage.ui index 70ab1f0728..c0e2b0be06 100644 --- a/files/ui/graphicspage.ui +++ b/apps/launcher/ui/graphicspage.ui @@ -107,7 +107,7 @@ - x + × diff --git a/files/ui/importpage.ui b/apps/launcher/ui/importpage.ui similarity index 100% rename from files/ui/importpage.ui rename to apps/launcher/ui/importpage.ui diff --git a/files/ui/mainwindow.ui b/apps/launcher/ui/mainwindow.ui similarity index 100% rename from files/ui/mainwindow.ui rename to apps/launcher/ui/mainwindow.ui diff --git a/files/ui/settingspage.ui b/apps/launcher/ui/settingspage.ui similarity index 100% rename from files/ui/settingspage.ui rename to apps/launcher/ui/settingspage.ui diff --git a/apps/launcher/utils/openalutil.cpp b/apps/launcher/utils/openalutil.cpp index 1a332e9788..9a9ae9981b 100644 --- a/apps/launcher/utils/openalutil.cpp +++ b/apps/launcher/utils/openalutil.cpp @@ -1,7 +1,7 @@ #include #include -#include +#include "apps/openmw/mwsound/alext.h" #include "openalutil.hpp" diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp index 793a08ba2c..8604bcdfb0 100644 --- a/apps/navmeshtool/main.cpp +++ b/apps/navmeshtool/main.cpp @@ -164,12 +164,12 @@ namespace NavMeshTool config.filterOutNonExistingPaths(dataDirs); - const auto resDir = variables["resources"].as(); + const auto& resDir = variables["resources"].as(); Log(Debug::Info) << Version::getOpenmwVersionDescription(); dataDirs.insert(dataDirs.begin(), resDir / "vfs"); - const auto fileCollections = Files::Collections(dataDirs); - const auto archives = variables["fallback-archive"].as(); - const auto contentFiles = variables["content"].as(); + const Files::Collections fileCollections(dataDirs); + const auto& archives = variables["fallback-archive"].as(); + const auto& contentFiles = variables["content"].as(); const std::size_t threadsNumber = variables["threads"].as(); if (threadsNumber < 1) diff --git a/apps/navmeshtool/worldspacedata.cpp b/apps/navmeshtool/worldspacedata.cpp index 7f579f8fde..0b3a1202d0 100644 --- a/apps/navmeshtool/worldspacedata.cpp +++ b/apps/navmeshtool/worldspacedata.cpp @@ -131,7 +131,7 @@ namespace NavMeshTool osg::ref_ptr shape = [&] { try { - return bulletShapeManager.getShape(Misc::ResourceHelpers::correctMeshPath(model, &vfs)); + return bulletShapeManager.getShape(Misc::ResourceHelpers::correctMeshPath(model)); } catch (const std::exception& e) { diff --git a/apps/niftest/niftest.cpp b/apps/niftest/niftest.cpp index 004e45765c..29488fb677 100644 --- a/apps/niftest/niftest.cpp +++ b/apps/niftest/niftest.cpp @@ -45,9 +45,6 @@ std::unique_ptr makeBsaArchive(const std::filesystem::path& path) { switch (Bsa::BSAFile::detectVersion(path)) { - case Bsa::BSAVER_UNKNOWN: - std::cerr << '"' << path << "\" is unknown BSA archive" << std::endl; - return nullptr; case Bsa::BSAVER_COMPRESSED: return std::make_unique::type>(path); case Bsa::BSAVER_BA2_GNRL: @@ -56,11 +53,11 @@ std::unique_ptr makeBsaArchive(const std::filesystem::path& path) return std::make_unique::type>(path); case Bsa::BSAVER_UNCOMPRESSED: return std::make_unique::type>(path); + case Bsa::BSAVER_UNKNOWN: + default: + std::cerr << "'" << Files::pathToUnicodeString(path) << "' is not a recognized BSA archive" << std::endl; + return nullptr; } - - std::cerr << '"' << path << "\" is unsupported BSA archive" << std::endl; - - return nullptr; } std::unique_ptr makeArchive(const std::filesystem::path& path) @@ -72,58 +69,86 @@ std::unique_ptr makeArchive(const std::filesystem::path& path) return nullptr; } +void readNIF( + const std::filesystem::path& source, const std::filesystem::path& path, const VFS::Manager* vfs, bool quiet) +{ + const std::string pathStr = Files::pathToUnicodeString(path); + if (!quiet) + { + std::cout << "Reading NIF file '" << pathStr << "'"; + if (!source.empty()) + std::cout << " from '" << Files::pathToUnicodeString(isBSA(source) ? source.filename() : source) << "'"; + std::cout << std::endl; + } + std::filesystem::path fullPath = !source.empty() ? source / path : path; + try + { + Nif::NIFFile file(fullPath); + Nif::Reader reader(file); + if (vfs != nullptr) + reader.parse(vfs->get(pathStr)); + else + reader.parse(Files::openConstrainedFileStream(fullPath)); + } + catch (std::exception& e) + { + std::cerr << "Failed to read '" << pathStr << "':" << std::endl << e.what() << std::endl; + } +} + /// Check all the nif files in a given VFS::Archive /// \note Can not read a bsa file inside of a bsa file. -void readVFS(std::unique_ptr&& anArchive, const std::filesystem::path& archivePath = {}) +void readVFS(std::unique_ptr&& archive, const std::filesystem::path& archivePath, bool quiet) { - if (anArchive == nullptr) + if (archive == nullptr) return; - VFS::Manager myManager; - myManager.addArchive(std::move(anArchive)); - myManager.buildIndex(); + if (!quiet) + std::cout << "Reading data source '" << Files::pathToUnicodeString(archivePath) << "'" << std::endl; - for (const auto& name : myManager.getRecursiveDirectoryIterator("")) + VFS::Manager vfs; + vfs.addArchive(std::move(archive)); + vfs.buildIndex(); + + for (const auto& name : vfs.getRecursiveDirectoryIterator("")) { - try + if (isNIF(name)) { - if (isNIF(name)) - { - // std::cout << "Decoding: " << name << std::endl; - Nif::NIFFile file(archivePath / name); - Nif::Reader reader(file); - reader.parse(myManager.get(name)); - } - else if (isBSA(name)) - { - if (!archivePath.empty() && !isBSA(archivePath)) - { - // std::cout << "Reading BSA File: " << name << std::endl; - readVFS(makeBsaArchive(archivePath / name), archivePath / name); - // std::cout << "Done with BSA File: " << name << std::endl; - } - } + readNIF(archivePath, name, &vfs, quiet); } - catch (std::exception& e) + } + + if (!archivePath.empty() && !isBSA(archivePath)) + { + Files::PathContainer dataDirs = { archivePath }; + const Files::Collections fileCollections = Files::Collections(dataDirs); + const Files::MultiDirCollection& bsaCol = fileCollections.getCollection(".bsa"); + const Files::MultiDirCollection& ba2Col = fileCollections.getCollection(".ba2"); + for (auto& file : bsaCol) { - std::cerr << "ERROR, an exception has occurred: " << e.what() << std::endl; + readVFS(makeBsaArchive(file.second), file.second, quiet); + } + for (auto& file : ba2Col) + { + readVFS(makeBsaArchive(file.second), file.second, quiet); } } } -bool parseOptions(int argc, char** argv, std::vector& files, bool& writeDebugLog, - std::vector& archives) +bool parseOptions(int argc, char** argv, Files::PathContainer& files, Files::PathContainer& archives, + bool& writeDebugLog, bool& quiet) { bpo::options_description desc(R"(Ensure that OpenMW can use the provided NIF and BSA files Usages: - niftool - Scan the file or directories for nif errors. + niftest + Scan the file or directories for NIF errors. Allowed options)"); auto addOption = desc.add_options(); addOption("help,h", "print help message."); addOption("write-debug-log,v", "write debug log for unsupported nif files"); + addOption("quiet,q", "do not log read archives/files"); addOption("archives", bpo::value(), "path to archive files to provide files"); addOption("input-file", bpo::value(), "input file"); @@ -143,17 +168,18 @@ Allowed options)"); return false; } writeDebugLog = variables.count("write-debug-log") > 0; + quiet = variables.count("quiet") > 0; if (variables.count("input-file")) { - files = variables["input-file"].as(); + files = asPathContainer(variables["input-file"].as()); if (const auto it = variables.find("archives"); it != variables.end()) - archives = it->second.as(); + archives = asPathContainer(it->second.as()); return true; } } catch (std::exception& e) { - std::cout << "ERROR parsing arguments: " << e.what() << "\n\n" << desc << std::endl; + std::cout << "Error parsing arguments: " << e.what() << "\n\n" << desc << std::endl; return false; } @@ -164,64 +190,62 @@ Allowed options)"); int main(int argc, char** argv) { - std::vector files; + Files::PathContainer files, sources; bool writeDebugLog = false; - std::vector archives; - if (!parseOptions(argc, argv, files, writeDebugLog, archives)) + bool quiet = false; + if (!parseOptions(argc, argv, files, sources, writeDebugLog, quiet)) return 1; Nif::Reader::setLoadUnsupportedFiles(true); Nif::Reader::setWriteNifDebugLog(writeDebugLog); std::unique_ptr vfs; - if (!archives.empty()) + if (!sources.empty()) { vfs = std::make_unique(); - for (const std::filesystem::path& path : archives) + for (const std::filesystem::path& path : sources) { + const std::string pathStr = Files::pathToUnicodeString(path); + if (!quiet) + std::cout << "Adding data source '" << pathStr << "'" << std::endl; + try { if (auto archive = makeArchive(path)) vfs->addArchive(std::move(archive)); else - std::cerr << '"' << path << "\" is unsupported archive" << std::endl; - vfs->buildIndex(); + std::cerr << "Error: '" << pathStr << "' is not an archive or directory" << std::endl; } catch (std::exception& e) { - std::cerr << "ERROR, an exception has occurred: " << e.what() << std::endl; + std::cerr << "Failed to add data source '" << pathStr << "': " << e.what() << std::endl; } } + + vfs->buildIndex(); } - // std::cout << "Reading Files" << std::endl; for (const auto& path : files) { + const std::string pathStr = Files::pathToUnicodeString(path); try { if (isNIF(path)) { - // std::cout << "Decoding: " << name << std::endl; - Nif::NIFFile file(path); - Nif::Reader reader(file); - if (vfs != nullptr) - reader.parse(vfs->get(Files::pathToUnicodeString(path))); - else - reader.parse(Files::openConstrainedFileStream(path)); + readNIF({}, path, vfs.get(), quiet); } else if (auto archive = makeArchive(path)) { - readVFS(std::move(archive), path); + readVFS(std::move(archive), path, quiet); } else { - std::cerr << "ERROR: \"" << Files::pathToUnicodeString(path) - << "\" is not a nif file, bsa/ba2 file, or directory!" << std::endl; + std::cerr << "Error: '" << pathStr << "' is not a NIF file, BSA/BA2 archive, or directory" << std::endl; } } catch (std::exception& e) { - std::cerr << "ERROR, an exception has occurred: " << e.what() << std::endl; + std::cerr << "Failed to read '" << pathStr << "': " << e.what() << std::endl; } } return 0; diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 20bd62d145..610c5157aa 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -1,5 +1,4 @@ set (OPENCS_SRC - ${CMAKE_SOURCE_DIR}/files/windows/opencs.rc ) opencs_units (. editor) @@ -116,7 +115,7 @@ opencs_units (view/prefs opencs_units (model/prefs state setting intsetting doublesetting boolsetting enumsetting coloursetting shortcut - shortcuteventhandler shortcutmanager shortcutsetting modifiersetting stringsetting + shortcuteventhandler shortcutmanager shortcutsetting modifiersetting stringsetting subcategory ) opencs_units (model/prefs @@ -139,14 +138,16 @@ set (OPENCS_RES ${CMAKE_SOURCE_DIR}/files/opencs/resources.qrc ) set (OPENCS_UI - ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui - ${CMAKE_SOURCE_DIR}/files/ui/filedialog.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/filedialog.ui ) source_group (openmw-cs FILES main.cpp ${OPENCS_SRC} ${OPENCS_HDR}) if(WIN32) set(QT_USE_QTMAIN TRUE) + set(OPENCS_RC_FILE ${CMAKE_SOURCE_DIR}/files/windows/opencs.rc) +else(WIN32) + set(OPENCS_RC_FILE "") endif(WIN32) if (QT_VERSION_MAJOR VERSION_EQUAL 5) @@ -187,6 +188,7 @@ if(BUILD_OPENCS) ${OPENCS_CFG} ${OPENCS_DEFAULT_FILTERS_FILE} ${OPENCS_OPENMW_CFG} + ${OPENCS_RC_FILE} main.cpp ) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index c21fc12a05..05f90b96f3 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -200,6 +200,8 @@ std::pair> CS::Editor::readConfig dataDirs.insert(dataDirs.end(), dataLocal.begin(), dataLocal.end()); + dataDirs.insert(dataDirs.begin(), mResources / "vfs"); + // iterate the data directories and add them to the file dialog for loading mFileDialog.addFiles(dataDirs); diff --git a/apps/opencs/main.cpp b/apps/opencs/main.cpp index ecab9614a1..e7f980dc0d 100644 --- a/apps/opencs/main.cpp +++ b/apps/opencs/main.cpp @@ -81,11 +81,6 @@ int runApplication(int argc, char* argv[]) Application application(argc, argv); -#ifdef Q_OS_MAC - QDir dir(QCoreApplication::applicationDirPath()); - QDir::setCurrent(dir.absolutePath()); -#endif - application.setWindowIcon(QIcon(":./openmw-cs.png")); CS::Editor editor(argc, argv); diff --git a/apps/opencs/model/doc/saving.cpp b/apps/opencs/model/doc/saving.cpp index b2e4d4649a..ed785c38fe 100644 --- a/apps/opencs/model/doc/saving.cpp +++ b/apps/opencs/model/doc/saving.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include "../world/data.hpp" #include "../world/idcollection.hpp" @@ -52,6 +53,9 @@ CSMDoc::Saving::Saving(Document& document, const std::filesystem::path& projectP appendStage(new WriteCollectionStage>( mDocument.getData().getScripts(), mState, CSMWorld::Scope_Project)); + appendStage(new WriteCollectionStage>( + mDocument.getData().getSelectionGroups(), mState, CSMWorld::Scope_Project)); + appendStage(new CloseSaveStage(mState)); // save content file diff --git a/apps/opencs/model/prefs/boolsetting.cpp b/apps/opencs/model/prefs/boolsetting.cpp index c668bc0af4..44262e2012 100644 --- a/apps/opencs/model/prefs/boolsetting.cpp +++ b/apps/opencs/model/prefs/boolsetting.cpp @@ -11,9 +11,8 @@ #include "state.hpp" CSMPrefs::BoolSetting::BoolSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, bool default_) - : Setting(parent, mutex, key, label) - , mDefault(default_) + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mWidget(nullptr) { } @@ -24,10 +23,10 @@ CSMPrefs::BoolSetting& CSMPrefs::BoolSetting::setTooltip(const std::string& tool return *this; } -std::pair CSMPrefs::BoolSetting::makeWidgets(QWidget* parent) +CSMPrefs::SettingWidgets CSMPrefs::BoolSetting::makeWidgets(QWidget* parent) { - mWidget = new QCheckBox(QString::fromUtf8(getLabel().c_str()), parent); - mWidget->setCheckState(mDefault ? Qt::Checked : Qt::Unchecked); + mWidget = new QCheckBox(getLabel(), parent); + mWidget->setCheckState(getValue() ? Qt::Checked : Qt::Unchecked); if (!mTooltip.empty()) { @@ -37,24 +36,19 @@ std::pair CSMPrefs::BoolSetting::makeWidgets(QWidget* parent connect(mWidget, &QCheckBox::stateChanged, this, &BoolSetting::valueChanged); - return std::make_pair(static_cast(nullptr), mWidget); + return SettingWidgets{ .mLabel = nullptr, .mInput = mWidget }; } void CSMPrefs::BoolSetting::updateWidget() { if (mWidget) { - mWidget->setCheckState( - Settings::Manager::getBool(getKey(), getParent()->getKey()) ? Qt::Checked : Qt::Unchecked); + mWidget->setCheckState(getValue() ? Qt::Checked : Qt::Unchecked); } } void CSMPrefs::BoolSetting::valueChanged(int value) { - { - QMutexLocker lock(getMutex()); - Settings::Manager::setBool(getKey(), getParent()->getKey(), value); - } - + setValue(value != Qt::Unchecked); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/boolsetting.hpp b/apps/opencs/model/prefs/boolsetting.hpp index e75ea1a346..edabf85058 100644 --- a/apps/opencs/model/prefs/boolsetting.hpp +++ b/apps/opencs/model/prefs/boolsetting.hpp @@ -12,21 +12,21 @@ namespace CSMPrefs { class Category; - class BoolSetting : public Setting + class BoolSetting final : public TypedSetting { Q_OBJECT std::string mTooltip; - bool mDefault; QCheckBox* mWidget; public: - BoolSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label, bool default_); + explicit BoolSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); BoolSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/category.cpp b/apps/opencs/model/prefs/category.cpp index 5a82be08fc..3ae4826953 100644 --- a/apps/opencs/model/prefs/category.cpp +++ b/apps/opencs/model/prefs/category.cpp @@ -5,6 +5,7 @@ #include "setting.hpp" #include "state.hpp" +#include "subcategory.hpp" CSMPrefs::Category::Category(State* parent, const std::string& key) : mParent(parent) @@ -23,6 +24,14 @@ CSMPrefs::State* CSMPrefs::Category::getState() const } void CSMPrefs::Category::addSetting(Setting* setting) +{ + if (!mIndex.emplace(setting->getKey(), setting).second) + throw std::logic_error("Category " + mKey + " already has setting: " + setting->getKey()); + + mSettings.push_back(setting); +} + +void CSMPrefs::Category::addSubcategory(Subcategory* setting) { mSettings.push_back(setting); } @@ -39,11 +48,12 @@ CSMPrefs::Category::Iterator CSMPrefs::Category::end() CSMPrefs::Setting& CSMPrefs::Category::operator[](const std::string& key) { - for (Iterator iter = mSettings.begin(); iter != mSettings.end(); ++iter) - if ((*iter)->getKey() == key) - return **iter; + const auto it = mIndex.find(key); - throw std::logic_error("Invalid user setting: " + key); + if (it != mIndex.end()) + return *it->second; + + throw std::logic_error("Invalid user setting in " + mKey + " category: " + key); } void CSMPrefs::Category::update() diff --git a/apps/opencs/model/prefs/category.hpp b/apps/opencs/model/prefs/category.hpp index 5c75f99067..ef67c82138 100644 --- a/apps/opencs/model/prefs/category.hpp +++ b/apps/opencs/model/prefs/category.hpp @@ -3,12 +3,14 @@ #include #include +#include #include namespace CSMPrefs { class State; class Setting; + class Subcategory; class Category { @@ -20,6 +22,7 @@ namespace CSMPrefs State* mParent; std::string mKey; Container mSettings; + std::unordered_map mIndex; public: Category(State* parent, const std::string& key); @@ -30,6 +33,8 @@ namespace CSMPrefs void addSetting(Setting* setting); + void addSubcategory(Subcategory* setting); + Iterator begin(); Iterator end(); diff --git a/apps/opencs/model/prefs/coloursetting.cpp b/apps/opencs/model/prefs/coloursetting.cpp index 86f3a5d772..10ca9d7f68 100644 --- a/apps/opencs/model/prefs/coloursetting.cpp +++ b/apps/opencs/model/prefs/coloursetting.cpp @@ -14,9 +14,8 @@ #include "state.hpp" CSMPrefs::ColourSetting::ColourSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, QColor default_) - : Setting(parent, mutex, key, label) - , mDefault(std::move(default_)) + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mWidget(nullptr) { } @@ -27,11 +26,11 @@ CSMPrefs::ColourSetting& CSMPrefs::ColourSetting::setTooltip(const std::string& return *this; } -std::pair CSMPrefs::ColourSetting::makeWidgets(QWidget* parent) +CSMPrefs::SettingWidgets CSMPrefs::ColourSetting::makeWidgets(QWidget* parent) { - QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); - mWidget = new CSVWidget::ColorEditor(mDefault, parent); + mWidget = new CSVWidget::ColorEditor(toColor(), parent); if (!mTooltip.empty()) { @@ -42,24 +41,18 @@ std::pair CSMPrefs::ColourSetting::makeWidgets(QWidget* pare connect(mWidget, &CSVWidget::ColorEditor::pickingFinished, this, &ColourSetting::valueChanged); - return std::make_pair(label, mWidget); + return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::ColourSetting::updateWidget() { if (mWidget) - { - mWidget->setColor(QString::fromStdString(Settings::Manager::getString(getKey(), getParent()->getKey()))); - } + mWidget->setColor(toColor()); } void CSMPrefs::ColourSetting::valueChanged() { CSVWidget::ColorEditor& widget = dynamic_cast(*sender()); - { - QMutexLocker lock(getMutex()); - Settings::Manager::setString(getKey(), getParent()->getKey(), widget.color().name().toUtf8().data()); - } - + setValue(widget.color().name().toStdString()); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/coloursetting.hpp b/apps/opencs/model/prefs/coloursetting.hpp index 0c22d9cc5d..85e43f28bd 100644 --- a/apps/opencs/model/prefs/coloursetting.hpp +++ b/apps/opencs/model/prefs/coloursetting.hpp @@ -6,6 +6,7 @@ #include #include +#include #include class QMutex; @@ -20,22 +21,22 @@ namespace CSVWidget namespace CSMPrefs { class Category; - class ColourSetting : public Setting + + class ColourSetting final : public TypedSetting { Q_OBJECT std::string mTooltip; - QColor mDefault; CSVWidget::ColorEditor* mWidget; public: - ColourSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, QColor default_); + explicit ColourSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); ColourSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/doublesetting.cpp b/apps/opencs/model/prefs/doublesetting.cpp index 7e3aadb0c3..bbe573f800 100644 --- a/apps/opencs/model/prefs/doublesetting.cpp +++ b/apps/opencs/model/prefs/doublesetting.cpp @@ -15,12 +15,11 @@ #include "state.hpp" CSMPrefs::DoubleSetting::DoubleSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, double default_) - : Setting(parent, mutex, key, label) + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mPrecision(2) , mMin(0) , mMax(std::numeric_limits::max()) - , mDefault(default_) , mWidget(nullptr) { } @@ -56,14 +55,14 @@ CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setTooltip(const std::string& return *this; } -std::pair CSMPrefs::DoubleSetting::makeWidgets(QWidget* parent) +CSMPrefs::SettingWidgets CSMPrefs::DoubleSetting::makeWidgets(QWidget* parent) { - QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); mWidget = new QDoubleSpinBox(parent); mWidget->setDecimals(mPrecision); mWidget->setRange(mMin, mMax); - mWidget->setValue(mDefault); + mWidget->setValue(getValue()); if (!mTooltip.empty()) { @@ -74,23 +73,17 @@ std::pair CSMPrefs::DoubleSetting::makeWidgets(QWidget* pare connect(mWidget, qOverload(&QDoubleSpinBox::valueChanged), this, &DoubleSetting::valueChanged); - return std::make_pair(label, mWidget); + return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::DoubleSetting::updateWidget() { if (mWidget) - { - mWidget->setValue(Settings::Manager::getFloat(getKey(), getParent()->getKey())); - } + mWidget->setValue(getValue()); } void CSMPrefs::DoubleSetting::valueChanged(double value) { - { - QMutexLocker lock(getMutex()); - Settings::Manager::setFloat(getKey(), getParent()->getKey(), value); - } - + setValue(value); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/doublesetting.hpp b/apps/opencs/model/prefs/doublesetting.hpp index c951d2a88c..856cebcb46 100644 --- a/apps/opencs/model/prefs/doublesetting.hpp +++ b/apps/opencs/model/prefs/doublesetting.hpp @@ -9,7 +9,7 @@ namespace CSMPrefs { class Category; - class DoubleSetting : public Setting + class DoubleSetting final : public TypedSetting { Q_OBJECT @@ -17,12 +17,11 @@ namespace CSMPrefs double mMin; double mMax; std::string mTooltip; - double mDefault; QDoubleSpinBox* mWidget; public: - DoubleSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, double default_); + explicit DoubleSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); DoubleSetting& setPrecision(int precision); @@ -36,7 +35,7 @@ namespace CSMPrefs DoubleSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/enumsetting.cpp b/apps/opencs/model/prefs/enumsetting.cpp index a3ac9bce2b..aaa4c28c61 100644 --- a/apps/opencs/model/prefs/enumsetting.cpp +++ b/apps/opencs/model/prefs/enumsetting.cpp @@ -15,39 +15,10 @@ #include "category.hpp" #include "state.hpp" -CSMPrefs::EnumValue::EnumValue(const std::string& value, const std::string& tooltip) - : mValue(value) - , mTooltip(tooltip) -{ -} - -CSMPrefs::EnumValue::EnumValue(const char* value) - : mValue(value) -{ -} - -CSMPrefs::EnumValues& CSMPrefs::EnumValues::add(const EnumValues& values) -{ - mValues.insert(mValues.end(), values.mValues.begin(), values.mValues.end()); - return *this; -} - -CSMPrefs::EnumValues& CSMPrefs::EnumValues::add(const EnumValue& value) -{ - mValues.push_back(value); - return *this; -} - -CSMPrefs::EnumValues& CSMPrefs::EnumValues::add(const std::string& value, const std::string& tooltip) -{ - mValues.emplace_back(value, tooltip); - return *this; -} - -CSMPrefs::EnumSetting::EnumSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, const EnumValue& default_) - : Setting(parent, mutex, key, label) - , mDefault(default_) +CSMPrefs::EnumSetting::EnumSetting(Category* parent, QMutex* mutex, std::string_view key, const QString& label, + std::span values, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) + , mValues(values) , mWidget(nullptr) { } @@ -58,43 +29,28 @@ CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::setTooltip(const std::string& tool return *this; } -CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::addValues(const EnumValues& values) +CSMPrefs::SettingWidgets CSMPrefs::EnumSetting::makeWidgets(QWidget* parent) { - mValues.add(values); - return *this; -} - -CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::addValue(const EnumValue& value) -{ - mValues.add(value); - return *this; -} - -CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::addValue(const std::string& value, const std::string& tooltip) -{ - mValues.add(value, tooltip); - return *this; -} - -std::pair CSMPrefs::EnumSetting::makeWidgets(QWidget* parent) -{ - QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); mWidget = new QComboBox(parent); - size_t index = 0; - - for (size_t i = 0; i < mValues.mValues.size(); ++i) + for (std::size_t i = 0; i < mValues.size(); ++i) { - if (mDefault.mValue == mValues.mValues[i].mValue) - index = i; + const EnumValueView& v = mValues[i]; - mWidget->addItem(QString::fromUtf8(mValues.mValues[i].mValue.c_str())); + mWidget->addItem(QString::fromUtf8(v.mValue.data(), static_cast(v.mValue.size()))); - if (!mValues.mValues[i].mTooltip.empty()) - mWidget->setItemData(i, QString::fromUtf8(mValues.mValues[i].mTooltip.c_str()), Qt::ToolTipRole); + if (!v.mTooltip.empty()) + mWidget->setItemData(static_cast(i), + QString::fromUtf8(v.mTooltip.data(), static_cast(v.mTooltip.size())), Qt::ToolTipRole); } + const std::string value = getValue(); + const std::size_t index = std::find_if(mValues.begin(), mValues.end(), [&](const EnumValueView& v) { + return v.mValue == value; + }) - mValues.begin(); + mWidget->setCurrentIndex(static_cast(index)); if (!mTooltip.empty()) @@ -105,26 +61,20 @@ std::pair CSMPrefs::EnumSetting::makeWidgets(QWidget* parent connect(mWidget, qOverload(&QComboBox::currentIndexChanged), this, &EnumSetting::valueChanged); - return std::make_pair(label, mWidget); + return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::EnumSetting::updateWidget() { if (mWidget) - { - int index - = mWidget->findText(QString::fromStdString(Settings::Manager::getString(getKey(), getParent()->getKey()))); - - mWidget->setCurrentIndex(index); - } + mWidget->setCurrentIndex(mWidget->findText(QString::fromStdString(getValue()))); } void CSMPrefs::EnumSetting::valueChanged(int value) { - { - QMutexLocker lock(getMutex()); - Settings::Manager::setString(getKey(), getParent()->getKey(), mValues.mValues.at(value).mValue); - } + if (value < 0 || static_cast(value) >= mValues.size()) + throw std::logic_error("Invalid enum setting \"" + getKey() + "\" value index: " + std::to_string(value)); + setValue(std::string(mValues[value].mValue)); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/enumsetting.hpp b/apps/opencs/model/prefs/enumsetting.hpp index 57bd2115ce..00953f914e 100644 --- a/apps/opencs/model/prefs/enumsetting.hpp +++ b/apps/opencs/model/prefs/enumsetting.hpp @@ -1,10 +1,13 @@ #ifndef CSM_PREFS_ENUMSETTING_H #define CSM_PREFS_ENUMSETTING_H +#include #include +#include #include #include +#include "enumvalueview.hpp" #include "setting.hpp" class QComboBox; @@ -13,50 +16,22 @@ namespace CSMPrefs { class Category; - struct EnumValue - { - std::string mValue; - std::string mTooltip; - - EnumValue(const std::string& value, const std::string& tooltip = ""); - - EnumValue(const char* value); - }; - - struct EnumValues - { - std::vector mValues; - - EnumValues& add(const EnumValues& values); - - EnumValues& add(const EnumValue& value); - - EnumValues& add(const std::string& value, const std::string& tooltip); - }; - - class EnumSetting : public Setting + class EnumSetting final : public TypedSetting { Q_OBJECT std::string mTooltip; - EnumValue mDefault; - EnumValues mValues; + std::span mValues; QComboBox* mWidget; public: - EnumSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label, - const EnumValue& default_); + explicit EnumSetting(Category* parent, QMutex* mutex, std::string_view key, const QString& label, + std::span values, Settings::Index& index); EnumSetting& setTooltip(const std::string& tooltip); - EnumSetting& addValues(const EnumValues& values); - - EnumSetting& addValue(const EnumValue& value); - - EnumSetting& addValue(const std::string& value, const std::string& tooltip); - /// Return label, input widget. - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/enumvalueview.hpp b/apps/opencs/model/prefs/enumvalueview.hpp new file mode 100644 index 0000000000..f46d250a81 --- /dev/null +++ b/apps/opencs/model/prefs/enumvalueview.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_APPS_OPENCS_MODEL_PREFS_ENUMVALUEVIEW_H +#define OPENMW_APPS_OPENCS_MODEL_PREFS_ENUMVALUEVIEW_H + +#include + +namespace CSMPrefs +{ + struct EnumValueView + { + std::string_view mValue; + std::string_view mTooltip; + }; +} + +#endif diff --git a/apps/opencs/model/prefs/intsetting.cpp b/apps/opencs/model/prefs/intsetting.cpp index 90cc77c788..a593b6f688 100644 --- a/apps/opencs/model/prefs/intsetting.cpp +++ b/apps/opencs/model/prefs/intsetting.cpp @@ -15,11 +15,10 @@ #include "state.hpp" CSMPrefs::IntSetting::IntSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, int default_) - : Setting(parent, mutex, key, label) + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mMin(0) , mMax(std::numeric_limits::max()) - , mDefault(default_) , mWidget(nullptr) { } @@ -49,13 +48,13 @@ CSMPrefs::IntSetting& CSMPrefs::IntSetting::setTooltip(const std::string& toolti return *this; } -std::pair CSMPrefs::IntSetting::makeWidgets(QWidget* parent) +CSMPrefs::SettingWidgets CSMPrefs::IntSetting::makeWidgets(QWidget* parent) { - QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); mWidget = new QSpinBox(parent); mWidget->setRange(mMin, mMax); - mWidget->setValue(mDefault); + mWidget->setValue(getValue()); if (!mTooltip.empty()) { @@ -66,23 +65,17 @@ std::pair CSMPrefs::IntSetting::makeWidgets(QWidget* parent) connect(mWidget, qOverload(&QSpinBox::valueChanged), this, &IntSetting::valueChanged); - return std::make_pair(label, mWidget); + return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::IntSetting::updateWidget() { if (mWidget) - { - mWidget->setValue(Settings::Manager::getInt(getKey(), getParent()->getKey())); - } + mWidget->setValue(getValue()); } void CSMPrefs::IntSetting::valueChanged(int value) { - { - QMutexLocker lock(getMutex()); - Settings::Manager::setInt(getKey(), getParent()->getKey(), value); - } - + setValue(value); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/intsetting.hpp b/apps/opencs/model/prefs/intsetting.hpp index 8a655178a4..e2926456aa 100644 --- a/apps/opencs/model/prefs/intsetting.hpp +++ b/apps/opencs/model/prefs/intsetting.hpp @@ -12,18 +12,18 @@ namespace CSMPrefs { class Category; - class IntSetting : public Setting + class IntSetting final : public TypedSetting { Q_OBJECT int mMin; int mMax; std::string mTooltip; - int mDefault; QSpinBox* mWidget; public: - IntSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label, int default_); + explicit IntSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); // defaults to [0, std::numeric_limits::max()] IntSetting& setRange(int min, int max); @@ -35,7 +35,7 @@ namespace CSMPrefs IntSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/modifiersetting.cpp b/apps/opencs/model/prefs/modifiersetting.cpp index 8752a4d51e..4bb7d64e60 100644 --- a/apps/opencs/model/prefs/modifiersetting.cpp +++ b/apps/opencs/model/prefs/modifiersetting.cpp @@ -19,21 +19,22 @@ class QWidget; namespace CSMPrefs { - ModifierSetting::ModifierSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label) - : Setting(parent, mutex, key, label) + ModifierSetting::ModifierSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mButton(nullptr) , mEditorActive(false) { } - std::pair ModifierSetting::makeWidgets(QWidget* parent) + SettingWidgets ModifierSetting::makeWidgets(QWidget* parent) { int modifier = 0; State::get().getShortcutManager().getModifier(getKey(), modifier); QString text = QString::fromUtf8(State::get().getShortcutManager().convertToString(modifier).c_str()); - QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); QPushButton* widget = new QPushButton(text, parent); widget->setCheckable(true); @@ -46,14 +47,14 @@ namespace CSMPrefs connect(widget, &QPushButton::toggled, this, &ModifierSetting::buttonToggled); - return std::make_pair(label, widget); + return SettingWidgets{ .mLabel = label, .mInput = widget }; } void ModifierSetting::updateWidget() { if (mButton) { - const std::string& shortcut = Settings::Manager::getString(getKey(), getParent()->getKey()); + const std::string& shortcut = getValue(); int modifier; State::get().getShortcutManager().convertFromString(shortcut, modifier); @@ -131,15 +132,7 @@ namespace CSMPrefs void ModifierSetting::storeValue(int modifier) { State::get().getShortcutManager().setModifier(getKey(), modifier); - - // Convert to string and assign - std::string value = State::get().getShortcutManager().convertToString(modifier); - - { - QMutexLocker lock(getMutex()); - Settings::Manager::setString(getKey(), getParent()->getKey(), value); - } - + setValue(State::get().getShortcutManager().convertToString(modifier)); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/modifiersetting.hpp b/apps/opencs/model/prefs/modifiersetting.hpp index ae984243ac..76a3a82e71 100644 --- a/apps/opencs/model/prefs/modifiersetting.hpp +++ b/apps/opencs/model/prefs/modifiersetting.hpp @@ -15,14 +15,16 @@ class QPushButton; namespace CSMPrefs { class Category; - class ModifierSetting : public Setting + + class ModifierSetting final : public TypedSetting { Q_OBJECT public: - ModifierSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label); + explicit ModifierSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/setting.cpp b/apps/opencs/model/prefs/setting.cpp index efe360d1e8..3c2ac65c94 100644 --- a/apps/opencs/model/prefs/setting.cpp +++ b/apps/opencs/model/prefs/setting.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "category.hpp" #include "state.hpp" @@ -14,22 +15,17 @@ QMutex* CSMPrefs::Setting::getMutex() return mMutex; } -CSMPrefs::Setting::Setting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label) +CSMPrefs::Setting::Setting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) : QObject(parent->getState()) , mParent(parent) , mMutex(mutex) , mKey(key) , mLabel(label) + , mIndex(index) { } -std::pair CSMPrefs::Setting::makeWidgets(QWidget* parent) -{ - return std::pair(0, 0); -} - -void CSMPrefs::Setting::updateWidget() {} - const CSMPrefs::Category* CSMPrefs::Setting::getParent() const { return mParent; @@ -40,35 +36,6 @@ const std::string& CSMPrefs::Setting::getKey() const return mKey; } -const std::string& CSMPrefs::Setting::getLabel() const -{ - return mLabel; -} - -int CSMPrefs::Setting::toInt() const -{ - QMutexLocker lock(mMutex); - return Settings::Manager::getInt(mKey, mParent->getKey()); -} - -double CSMPrefs::Setting::toDouble() const -{ - QMutexLocker lock(mMutex); - return Settings::Manager::getFloat(mKey, mParent->getKey()); -} - -std::string CSMPrefs::Setting::toString() const -{ - QMutexLocker lock(mMutex); - return Settings::Manager::getString(mKey, mParent->getKey()); -} - -bool CSMPrefs::Setting::isTrue() const -{ - QMutexLocker lock(mMutex); - return Settings::Manager::getBool(mKey, mParent->getKey()); -} - QColor CSMPrefs::Setting::toColor() const { // toString() handles lock diff --git a/apps/opencs/model/prefs/setting.hpp b/apps/opencs/model/prefs/setting.hpp index f63271b3f2..faadbcadd1 100644 --- a/apps/opencs/model/prefs/setting.hpp +++ b/apps/opencs/model/prefs/setting.hpp @@ -4,15 +4,26 @@ #include #include +#include #include +#include + +#include "category.hpp" + class QWidget; class QColor; class QMutex; +class QGridLayout; +class QLabel; namespace CSMPrefs { - class Category; + struct SettingWidgets + { + QLabel* mLabel; + QWidget* mInput; + }; class Setting : public QObject { @@ -21,44 +32,82 @@ namespace CSMPrefs Category* mParent; QMutex* mMutex; std::string mKey; - std::string mLabel; + QString mLabel; + Settings::Index& mIndex; protected: QMutex* getMutex(); + template + void resetValueImpl() + { + QMutexLocker lock(mMutex); + return mIndex.get(mParent->getKey(), mKey).reset(); + } + + template + T getValueImpl() const + { + QMutexLocker lock(mMutex); + return mIndex.get(mParent->getKey(), mKey).get(); + } + + template + void setValueImpl(const T& value) + { + QMutexLocker lock(mMutex); + return mIndex.get(mParent->getKey(), mKey).set(value); + } + public: - Setting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label); + explicit Setting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); ~Setting() override = default; - /// Return label, input widget. - /// - /// \note first can be a 0-pointer, which means that the label is part of the input - /// widget. - virtual std::pair makeWidgets(QWidget* parent); + virtual SettingWidgets makeWidgets(QWidget* parent) = 0; /// Updates the widget returned by makeWidgets() to the current setting. /// /// \note If make_widgets() has not been called yet then nothing happens. - virtual void updateWidget(); + virtual void updateWidget() = 0; + + virtual void reset() = 0; const Category* getParent() const; const std::string& getKey() const; - const std::string& getLabel() const; + const QString& getLabel() const { return mLabel; } - int toInt() const; + int toInt() const { return getValueImpl(); } - double toDouble() const; + double toDouble() const { return getValueImpl(); } - std::string toString() const; + std::string toString() const { return getValueImpl(); } - bool isTrue() const; + bool isTrue() const { return getValueImpl(); } QColor toColor() const; }; + template + class TypedSetting : public Setting + { + public: + using Setting::Setting; + + void reset() final + { + resetValueImpl(); + updateWidget(); + } + + T getValue() const { return getValueImpl(); } + + void setValue(const T& value) { return setValueImpl(value); } + }; + // note: fullKeys have the format categoryKey/settingKey bool operator==(const Setting& setting, const std::string& fullKey); bool operator==(const std::string& fullKey, const Setting& setting); diff --git a/apps/opencs/model/prefs/shortcutmanager.cpp b/apps/opencs/model/prefs/shortcutmanager.cpp index a6f1da4f85..d6686d31d9 100644 --- a/apps/opencs/model/prefs/shortcutmanager.cpp +++ b/apps/opencs/model/prefs/shortcutmanager.cpp @@ -43,7 +43,7 @@ namespace CSMPrefs mEventHandler->removeShortcut(shortcut); } - bool ShortcutManager::getSequence(const std::string& name, QKeySequence& sequence) const + bool ShortcutManager::getSequence(std::string_view name, QKeySequence& sequence) const { SequenceMap::const_iterator item = mSequences.find(name); if (item != mSequences.end()) @@ -56,7 +56,7 @@ namespace CSMPrefs return false; } - void ShortcutManager::setSequence(const std::string& name, const QKeySequence& sequence) + void ShortcutManager::setSequence(std::string_view name, const QKeySequence& sequence) { // Add to map/modify SequenceMap::iterator item = mSequences.find(name); @@ -91,7 +91,7 @@ namespace CSMPrefs return false; } - void ShortcutManager::setModifier(const std::string& name, int modifier) + void ShortcutManager::setModifier(std::string_view name, int modifier) { // Add to map/modify ModifierMap::iterator item = mModifiers.find(name); diff --git a/apps/opencs/model/prefs/shortcutmanager.hpp b/apps/opencs/model/prefs/shortcutmanager.hpp index fc8db3f2b0..0cfe3ad86a 100644 --- a/apps/opencs/model/prefs/shortcutmanager.hpp +++ b/apps/opencs/model/prefs/shortcutmanager.hpp @@ -28,11 +28,11 @@ namespace CSMPrefs /// The shortcut class will do this automatically void removeShortcut(Shortcut* shortcut); - bool getSequence(const std::string& name, QKeySequence& sequence) const; - void setSequence(const std::string& name, const QKeySequence& sequence); + bool getSequence(std::string_view name, QKeySequence& sequence) const; + void setSequence(std::string_view name, const QKeySequence& sequence); bool getModifier(const std::string& name, int& modifier) const; - void setModifier(const std::string& name, int modifier); + void setModifier(std::string_view name, int modifier); std::string convertToString(const QKeySequence& sequence) const; std::string convertToString(int modifier) const; @@ -49,9 +49,9 @@ namespace CSMPrefs private: // Need a multimap in case multiple shortcuts share the same name - typedef std::multimap ShortcutMap; - typedef std::map SequenceMap; - typedef std::map ModifierMap; + typedef std::multimap> ShortcutMap; + typedef std::map> SequenceMap; + typedef std::map> ModifierMap; typedef std::map NameMap; typedef std::map KeyMap; diff --git a/apps/opencs/model/prefs/shortcutsetting.cpp b/apps/opencs/model/prefs/shortcutsetting.cpp index d8c71d7008..bdaf3a0fda 100644 --- a/apps/opencs/model/prefs/shortcutsetting.cpp +++ b/apps/opencs/model/prefs/shortcutsetting.cpp @@ -18,8 +18,9 @@ namespace CSMPrefs { - ShortcutSetting::ShortcutSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label) - : Setting(parent, mutex, key, label) + ShortcutSetting::ShortcutSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mButton(nullptr) , mEditorActive(false) , mEditorPos(0) @@ -30,14 +31,14 @@ namespace CSMPrefs } } - std::pair ShortcutSetting::makeWidgets(QWidget* parent) + SettingWidgets ShortcutSetting::makeWidgets(QWidget* parent) { QKeySequence sequence; State::get().getShortcutManager().getSequence(getKey(), sequence); QString text = QString::fromUtf8(State::get().getShortcutManager().convertToString(sequence).c_str()); - QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); QPushButton* widget = new QPushButton(text, parent); widget->setCheckable(true); @@ -50,14 +51,14 @@ namespace CSMPrefs connect(widget, &QPushButton::toggled, this, &ShortcutSetting::buttonToggled); - return std::make_pair(label, widget); + return SettingWidgets{ .mLabel = label, .mInput = widget }; } void ShortcutSetting::updateWidget() { if (mButton) { - const std::string& shortcut = Settings::Manager::getString(getKey(), getParent()->getKey()); + const std::string shortcut = getValue(); QKeySequence sequence; State::get().getShortcutManager().convertFromString(shortcut, sequence); @@ -170,15 +171,7 @@ namespace CSMPrefs void ShortcutSetting::storeValue(const QKeySequence& sequence) { State::get().getShortcutManager().setSequence(getKey(), sequence); - - // Convert to string and assign - std::string value = State::get().getShortcutManager().convertToString(sequence); - - { - QMutexLocker lock(getMutex()); - Settings::Manager::setString(getKey(), getParent()->getKey(), value); - } - + setValue(State::get().getShortcutManager().convertToString(sequence)); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/shortcutsetting.hpp b/apps/opencs/model/prefs/shortcutsetting.hpp index bef140dada..bcb7b89488 100644 --- a/apps/opencs/model/prefs/shortcutsetting.hpp +++ b/apps/opencs/model/prefs/shortcutsetting.hpp @@ -2,6 +2,7 @@ #define CSM_PREFS_SHORTCUTSETTING_H #include +#include #include #include @@ -17,14 +18,16 @@ class QWidget; namespace CSMPrefs { class Category; - class ShortcutSetting : public Setting + + class ShortcutSetting final : public TypedSetting { Q_OBJECT public: - ShortcutSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label); + explicit ShortcutSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index 97f29bc8be..c11996a6ea 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -24,50 +25,43 @@ #include "modifiersetting.hpp" #include "shortcutsetting.hpp" #include "stringsetting.hpp" +#include "values.hpp" CSMPrefs::State* CSMPrefs::State::sThis = nullptr; void CSMPrefs::State::declare() { declareCategory("Windows"); - declareInt("default-width", "Default window width", 800) + declareInt(mValues->mWindows.mDefaultWidth, "Default window width") .setTooltip("Newly opened top-level windows will open with this width.") .setMin(80); - declareInt("default-height", "Default window height", 600) + declareInt(mValues->mWindows.mDefaultHeight, "Default window height") .setTooltip("Newly opened top-level windows will open with this height.") .setMin(80); - declareBool("show-statusbar", "Show Status Bar", true) + declareBool(mValues->mWindows.mShowStatusbar, "Show Status Bar") .setTooltip( "If a newly open top level window is showing status bars or not. " " Note that this does not affect existing windows."); - declareSeparator(); - declareBool("reuse", "Reuse Subviews", true) + declareBool(mValues->mWindows.mReuse, "Reuse Subviews") .setTooltip( "When a new subview is requested and a matching subview already " " exist, do not open a new subview and use the existing one instead."); - declareInt("max-subviews", "Maximum number of subviews per top-level window", 256) + declareInt(mValues->mWindows.mMaxSubviews, "Maximum number of subviews per top-level window") .setTooltip( "If the maximum number is reached and a new subview is opened " "it will be placed into a new top-level window.") .setRange(1, 256); - declareBool("hide-subview", "Hide single subview", false) + declareBool(mValues->mWindows.mHideSubview, "Hide single subview") .setTooltip( "When a view contains only a single subview, hide the subview title " "bar and if this subview is closed also close the view (unless it is the last " "view for this document)"); - declareInt("minimum-width", "Minimum subview width", 325) + declareInt(mValues->mWindows.mMinimumWidth, "Minimum subview width") .setTooltip("Minimum width of subviews.") .setRange(50, 10000); - declareSeparator(); - EnumValue scrollbarOnly("Scrollbar Only", - "Simple addition of scrollbars, the view window " - "does not grow automatically."); - declareEnum("mainwindow-scrollbar", "Horizontal scrollbar mode for main window.", scrollbarOnly) - .addValue(scrollbarOnly) - .addValue("Grow Only", "The view window grows as subviews are added. No scrollbars.") - .addValue("Grow then Scroll", "The view window grows. The scrollbar appears once it cannot grow any further."); + declareEnum(mValues->mWindows.mMainwindowScrollbar, "Horizontal scrollbar mode for main window."); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - declareBool("grow-limit", "Grow Limit Screen", false) + declareBool(mValues->mWindows.mGrowLimit, "Grow Limit Screen") .setTooltip( "When \"Grow then Scroll\" option is selected, the window size grows to" " the width of the virtual desktop. \nIf this option is selected the the window growth" @@ -75,378 +69,343 @@ void CSMPrefs::State::declare() #endif declareCategory("Records"); - EnumValue iconAndText("Icon and Text"); - EnumValues recordValues; - recordValues.add(iconAndText).add("Icon Only").add("Text Only"); - declareEnum("status-format", "Modification status display format", iconAndText).addValues(recordValues); - declareEnum("type-format", "ID type display format", iconAndText).addValues(recordValues); + declareEnum(mValues->mRecords.mStatusFormat, "Modification status display format"); + declareEnum(mValues->mRecords.mTypeFormat, "ID type display format"); declareCategory("ID Tables"); - EnumValue inPlaceEdit("Edit in Place", "Edit the clicked cell"); - EnumValue editRecord("Edit Record", "Open a dialogue subview for the clicked record"); - EnumValue view("View", "Open a scene subview for the clicked record (not available everywhere)"); - EnumValue editRecordAndClose("Edit Record and Close"); - EnumValues doubleClickValues; - doubleClickValues.add(inPlaceEdit) - .add(editRecord) - .add(view) - .add("Revert") - .add("Delete") - .add(editRecordAndClose) - .add("View and Close", "Open a scene subview for the clicked record and close the table subview"); - declareEnum("double", "Double Click", inPlaceEdit).addValues(doubleClickValues); - declareEnum("double-s", "Shift Double Click", editRecord).addValues(doubleClickValues); - declareEnum("double-c", "Control Double Click", view).addValues(doubleClickValues); - declareEnum("double-sc", "Shift Control Double Click", editRecordAndClose).addValues(doubleClickValues); - declareSeparator(); - EnumValue jumpAndSelect("Jump and Select", "Scroll new record into view and make it the selection"); - declareEnum("jump-to-added", "Action on adding or cloning a record", jumpAndSelect) - .addValue(jumpAndSelect) - .addValue("Jump Only", "Scroll new record into view") - .addValue("No Jump", "No special action"); - declareBool("extended-config", "Manually specify affected record types for an extended delete/revert", false) + declareEnum(mValues->mIdTables.mDouble, "Double Click"); + declareEnum(mValues->mIdTables.mDoubleS, "Shift Double Click"); + declareEnum(mValues->mIdTables.mDoubleC, "Control Double Click"); + declareEnum(mValues->mIdTables.mDoubleSc, "Shift Control Double Click"); + declareEnum(mValues->mIdTables.mJumpToAdded, "Action on adding or cloning a record"); + declareBool( + mValues->mIdTables.mExtendedConfig, "Manually specify affected record types for an extended delete/revert") .setTooltip( "Delete and revert commands have an extended form that also affects " "associated records.\n\n" "If this option is enabled, types of affected records are selected " "manually before a command execution.\nOtherwise, all associated " "records are deleted/reverted immediately."); - declareBool("subview-new-window", "Open Record in new window", false) + declareBool(mValues->mIdTables.mSubviewNewWindow, "Open Record in new window") .setTooltip( "When editing a record, open the view in a new window," " rather than docked in the main view."); declareCategory("ID Dialogues"); - declareBool("toolbar", "Show toolbar", true); + declareBool(mValues->mIdDialogues.mToolbar, "Show toolbar"); declareCategory("Reports"); - EnumValue actionNone("None"); - EnumValue actionEdit("Edit", "Open a table or dialogue suitable for addressing the listed report"); - EnumValue actionRemove("Remove", "Remove the report from the report table"); - EnumValue actionEditAndRemove("Edit And Remove", - "Open a table or dialogue suitable for addressing the listed report, then remove the report from the report " - "table"); - EnumValues reportValues; - reportValues.add(actionNone).add(actionEdit).add(actionRemove).add(actionEditAndRemove); - declareEnum("double", "Double Click", actionEdit).addValues(reportValues); - declareEnum("double-s", "Shift Double Click", actionRemove).addValues(reportValues); - declareEnum("double-c", "Control Double Click", actionEditAndRemove).addValues(reportValues); - declareEnum("double-sc", "Shift Control Double Click", actionNone).addValues(reportValues); - declareBool("ignore-base-records", "Ignore base records in verifier", false); + declareEnum(mValues->mReports.mDouble, "Double Click"); + declareEnum(mValues->mReports.mDoubleS, "Shift Double Click"); + declareEnum(mValues->mReports.mDoubleC, "Control Double Click"); + declareEnum(mValues->mReports.mDoubleSc, "Shift Control Double Click"); + declareBool(mValues->mReports.mIgnoreBaseRecords, "Ignore base records in verifier"); declareCategory("Search & Replace"); - declareInt("char-before", "Characters before search string", 10) + declareInt(mValues->mSearchAndReplace.mCharBefore, "Characters before search string") .setTooltip("Maximum number of character to display in search result before the searched text"); - declareInt("char-after", "Characters after search string", 10) + declareInt(mValues->mSearchAndReplace.mCharAfter, "Characters after search string") .setTooltip("Maximum number of character to display in search result after the searched text"); - declareBool("auto-delete", "Delete row from result table after a successful replace", true); + declareBool(mValues->mSearchAndReplace.mAutoDelete, "Delete row from result table after a successful replace"); declareCategory("Scripts"); - declareBool("show-linenum", "Show Line Numbers", true) + declareBool(mValues->mScripts.mShowLinenum, "Show Line Numbers") .setTooltip( "Show line numbers to the left of the script editor window." "The current row and column numbers of the text cursor are shown at the bottom."); - declareBool("wrap-lines", "Wrap Lines", false).setTooltip("Wrap lines longer than width of script editor."); - declareBool("mono-font", "Use monospace font", true); - declareInt("tab-width", "Tab Width", 4).setTooltip("Number of characters for tab width").setRange(1, 10); - EnumValue warningsNormal("Normal", "Report warnings as warning"); - declareEnum("warnings", "Warning Mode", warningsNormal) - .addValue("Ignore", "Do not report warning") - .addValue(warningsNormal) - .addValue("Strict", "Promote warning to an error"); - declareBool("toolbar", "Show toolbar", true); - declareInt("compile-delay", "Delay between updating of source errors", 100) + declareBool(mValues->mScripts.mWrapLines, "Wrap Lines") + .setTooltip("Wrap lines longer than width of script editor."); + declareBool(mValues->mScripts.mMonoFont, "Use monospace font"); + declareInt(mValues->mScripts.mTabWidth, "Tab Width") + .setTooltip("Number of characters for tab width") + .setRange(1, 10); + declareEnum(mValues->mScripts.mWarnings, "Warning Mode"); + declareBool(mValues->mScripts.mToolbar, "Show toolbar"); + declareInt(mValues->mScripts.mCompileDelay, "Delay between updating of source errors") .setTooltip("Delay in milliseconds") .setRange(0, 10000); - declareInt("error-height", "Initial height of the error panel", 100).setRange(100, 10000); - declareBool("highlight-occurrences", "Highlight other occurrences of selected names", true); - declareColour("colour-highlight", "Colour of highlighted occurrences", QColor("lightcyan")); - declareSeparator(); - declareColour("colour-int", "Highlight Colour: Integer Literals", QColor("darkmagenta")); - declareColour("colour-float", "Highlight Colour: Float Literals", QColor("magenta")); - declareColour("colour-name", "Highlight Colour: Names", QColor("grey")); - declareColour("colour-keyword", "Highlight Colour: Keywords", QColor("red")); - declareColour("colour-special", "Highlight Colour: Special Characters", QColor("darkorange")); - declareColour("colour-comment", "Highlight Colour: Comments", QColor("green")); - declareColour("colour-id", "Highlight Colour: IDs", QColor("blue")); + declareInt(mValues->mScripts.mErrorHeight, "Initial height of the error panel").setRange(100, 10000); + declareBool(mValues->mScripts.mHighlightOccurrences, "Highlight other occurrences of selected names"); + declareColour(mValues->mScripts.mColourHighlight, "Colour of highlighted occurrences"); + declareColour(mValues->mScripts.mColourInt, "Highlight Colour: Integer Literals"); + declareColour(mValues->mScripts.mColourFloat, "Highlight Colour: Float Literals"); + declareColour(mValues->mScripts.mColourName, "Highlight Colour: Names"); + declareColour(mValues->mScripts.mColourKeyword, "Highlight Colour: Keywords"); + declareColour(mValues->mScripts.mColourSpecial, "Highlight Colour: Special Characters"); + declareColour(mValues->mScripts.mColourComment, "Highlight Colour: Comments"); + declareColour(mValues->mScripts.mColourId, "Highlight Colour: IDs"); declareCategory("General Input"); - declareBool("cycle", "Cyclic next/previous", false) + declareBool(mValues->mGeneralInput.mCycle, "Cyclic next/previous") .setTooltip( "When using next/previous functions at the last/first item of a " "list go to the first/last item"); declareCategory("3D Scene Input"); - declareDouble("navi-wheel-factor", "Camera Zoom Sensitivity", 8).setRange(-100.0, 100.0); - declareDouble("s-navi-sensitivity", "Secondary Camera Movement Sensitivity", 50.0).setRange(-1000.0, 1000.0); - declareSeparator(); + declareDouble(mValues->mSceneInput.mNaviWheelFactor, "Camera Zoom Sensitivity").setRange(-100.0, 100.0); + declareDouble(mValues->mSceneInput.mSNaviSensitivity, "Secondary Camera Movement Sensitivity") + .setRange(-1000.0, 1000.0); - declareDouble("p-navi-free-sensitivity", "Free Camera Sensitivity", 1 / 650.).setPrecision(5).setRange(0.0, 1.0); - declareBool("p-navi-free-invert", "Invert Free Camera Mouse Input", false); - declareDouble("navi-free-lin-speed", "Free Camera Linear Speed", 1000.0).setRange(1.0, 10000.0); - declareDouble("navi-free-rot-speed", "Free Camera Rotational Speed", 3.14 / 2).setRange(0.001, 6.28); - declareDouble("navi-free-speed-mult", "Free Camera Speed Multiplier (from Modifier)", 8).setRange(0.001, 1000.0); - declareSeparator(); - - declareDouble("p-navi-orbit-sensitivity", "Orbit Camera Sensitivity", 1 / 650.).setPrecision(5).setRange(0.0, 1.0); - declareBool("p-navi-orbit-invert", "Invert Orbit Camera Mouse Input", false); - declareDouble("navi-orbit-rot-speed", "Orbital Camera Rotational Speed", 3.14 / 4).setRange(0.001, 6.28); - declareDouble("navi-orbit-speed-mult", "Orbital Camera Speed Multiplier (from Modifier)", 4) + declareDouble(mValues->mSceneInput.mPNaviFreeSensitivity, "Free Camera Sensitivity") + .setPrecision(5) + .setRange(0.0, 1.0); + declareBool(mValues->mSceneInput.mPNaviFreeInvert, "Invert Free Camera Mouse Input"); + declareDouble(mValues->mSceneInput.mNaviFreeLinSpeed, "Free Camera Linear Speed").setRange(1.0, 10000.0); + declareDouble(mValues->mSceneInput.mNaviFreeRotSpeed, "Free Camera Rotational Speed").setRange(0.001, 6.28); + declareDouble(mValues->mSceneInput.mNaviFreeSpeedMult, "Free Camera Speed Multiplier (from Modifier)") .setRange(0.001, 1000.0); - declareBool("navi-orbit-const-roll", "Keep camera roll constant for orbital camera", true); - declareSeparator(); - declareBool("context-select", "Context Sensitive Selection", false); - declareDouble("drag-factor", "Mouse sensitivity during drag operations", 1.0).setRange(0.001, 100.0); - declareDouble("drag-wheel-factor", "Mouse wheel sensitivity during drag operations", 1.0).setRange(0.001, 100.0); - declareDouble("drag-shift-factor", "Shift-acceleration factor during drag operations", 4.0) + declareDouble(mValues->mSceneInput.mPNaviOrbitSensitivity, "Orbit Camera Sensitivity") + .setPrecision(5) + .setRange(0.0, 1.0); + declareBool(mValues->mSceneInput.mPNaviOrbitInvert, "Invert Orbit Camera Mouse Input"); + declareDouble(mValues->mSceneInput.mNaviOrbitRotSpeed, "Orbital Camera Rotational Speed").setRange(0.001, 6.28); + declareDouble(mValues->mSceneInput.mNaviOrbitSpeedMult, "Orbital Camera Speed Multiplier (from Modifier)") + .setRange(0.001, 1000.0); + declareBool(mValues->mSceneInput.mNaviOrbitConstRoll, "Keep camera roll constant for orbital camera"); + + declareBool(mValues->mSceneInput.mContextSelect, "Context Sensitive Selection"); + declareDouble(mValues->mSceneInput.mDragFactor, "Mouse sensitivity during drag operations").setRange(0.001, 100.0); + declareDouble(mValues->mSceneInput.mDragWheelFactor, "Mouse wheel sensitivity during drag operations") + .setRange(0.001, 100.0); + declareDouble(mValues->mSceneInput.mDragShiftFactor, "Shift-acceleration factor during drag operations") .setTooltip("Acceleration factor during drag operations while holding down shift") .setRange(0.001, 100.0); - declareDouble("rotate-factor", "Free rotation factor", 0.007).setPrecision(4).setRange(0.0001, 0.1); + declareDouble(mValues->mSceneInput.mRotateFactor, "Free rotation factor").setPrecision(4).setRange(0.0001, 0.1); declareCategory("Rendering"); - declareInt("framerate-limit", "FPS limit", 60) + declareInt(mValues->mRendering.mFramerateLimit, "FPS limit") .setTooltip("Framerate limit in 3D preview windows. Zero value means \"unlimited\".") .setRange(0, 10000); - declareInt("camera-fov", "Camera FOV", 90).setRange(10, 170); - declareBool("camera-ortho", "Orthographic projection for camera", false); - declareInt("camera-ortho-size", "Orthographic projection size parameter", 100) + declareInt(mValues->mRendering.mCameraFov, "Camera FOV").setRange(10, 170); + declareBool(mValues->mRendering.mCameraOrtho, "Orthographic projection for camera"); + declareInt(mValues->mRendering.mCameraOrthoSize, "Orthographic projection size parameter") .setTooltip("Size of the orthographic frustum, greater value will allow the camera to see more of the world.") .setRange(10, 10000); - declareDouble("object-marker-alpha", "Object Marker Transparency", 0.5).setPrecision(2).setRange(0, 1); - declareBool("scene-use-gradient", "Use Gradient Background", true); - declareColour("scene-day-background-colour", "Day Background Colour", QColor(110, 120, 128, 255)); - declareColour("scene-day-gradient-colour", "Day Gradient Colour", QColor(47, 51, 51, 255)) + declareDouble(mValues->mRendering.mObjectMarkerAlpha, "Object Marker Transparency").setPrecision(2).setRange(0, 1); + declareBool(mValues->mRendering.mSceneUseGradient, "Use Gradient Background"); + declareColour(mValues->mRendering.mSceneDayBackgroundColour, "Day Background Colour"); + declareColour(mValues->mRendering.mSceneDayGradientColour, "Day Gradient Colour") .setTooltip( "Sets the gradient color to use in conjunction with the day background color. Ignored if " "the gradient option is disabled."); - declareColour("scene-bright-background-colour", "Scene Bright Background Colour", QColor(79, 87, 92, 255)); - declareColour("scene-bright-gradient-colour", "Scene Bright Gradient Colour", QColor(47, 51, 51, 255)) + declareColour(mValues->mRendering.mSceneBrightBackgroundColour, "Scene Bright Background Colour"); + declareColour(mValues->mRendering.mSceneBrightGradientColour, "Scene Bright Gradient Colour") .setTooltip( "Sets the gradient color to use in conjunction with the bright background color. Ignored if " "the gradient option is disabled."); - declareColour("scene-night-background-colour", "Scene Night Background Colour", QColor(64, 77, 79, 255)); - declareColour("scene-night-gradient-colour", "Scene Night Gradient Colour", QColor(47, 51, 51, 255)) + declareColour(mValues->mRendering.mSceneNightBackgroundColour, "Scene Night Background Colour"); + declareColour(mValues->mRendering.mSceneNightGradientColour, "Scene Night Gradient Colour") .setTooltip( "Sets the gradient color to use in conjunction with the night background color. Ignored if " "the gradient option is disabled."); - declareBool("scene-day-night-switch-nodes", "Use Day/Night Switch Nodes", true); + declareBool(mValues->mRendering.mSceneDayNightSwitchNodes, "Use Day/Night Switch Nodes"); declareCategory("Tooltips"); - declareBool("scene", "Show Tooltips in 3D scenes", true); - declareBool("scene-hide-basic", "Hide basic 3D scenes tooltips", false); - declareInt("scene-delay", "Tooltip delay in milliseconds", 500).setMin(1); - - EnumValue createAndInsert("Create cell and insert"); - EnumValue showAndInsert("Show cell and insert"); - EnumValue dontInsert("Discard"); - EnumValue insertAnyway("Insert anyway"); - EnumValues insertOutsideCell; - insertOutsideCell.add(createAndInsert).add(dontInsert).add(insertAnyway); - EnumValues insertOutsideVisibleCell; - insertOutsideVisibleCell.add(showAndInsert).add(dontInsert).add(insertAnyway); - - EnumValue createAndLandEdit("Create cell and land, then edit"); - EnumValue showAndLandEdit("Show cell and edit"); - EnumValue dontLandEdit("Discard"); - EnumValues landeditOutsideCell; - landeditOutsideCell.add(createAndLandEdit).add(dontLandEdit); - EnumValues landeditOutsideVisibleCell; - landeditOutsideVisibleCell.add(showAndLandEdit).add(dontLandEdit); - - EnumValue SelectOnly("Select only"); - EnumValue SelectAdd("Add to selection"); - EnumValue SelectRemove("Remove from selection"); - EnumValue selectInvert("Invert selection"); - EnumValues primarySelectAction; - primarySelectAction.add(SelectOnly).add(SelectAdd).add(SelectRemove).add(selectInvert); - EnumValues secondarySelectAction; - secondarySelectAction.add(SelectOnly).add(SelectAdd).add(SelectRemove).add(selectInvert); + declareBool(mValues->mTooltips.mScene, "Show Tooltips in 3D scenes"); + declareBool(mValues->mTooltips.mSceneHideBasic, "Hide basic 3D scenes tooltips"); + declareInt(mValues->mTooltips.mSceneDelay, "Tooltip delay in milliseconds").setMin(1); declareCategory("3D Scene Editing"); - declareDouble("gridsnap-movement", "Grid snap size", 16); - declareDouble("gridsnap-rotation", "Angle snap size", 15); - declareDouble("gridsnap-scale", "Scale snap size", 0.25); - declareInt("distance", "Drop Distance", 50) + declareDouble(mValues->mSceneEditing.mGridsnapMovement, "Grid snap size"); + declareDouble(mValues->mSceneEditing.mGridsnapRotation, "Angle snap size"); + declareDouble(mValues->mSceneEditing.mGridsnapScale, "Scale snap size"); + declareInt(mValues->mSceneEditing.mDistance, "Drop Distance") .setTooltip( "If an instance drop can not be placed against another object at the " "insert point, it will be placed by this distance from the insert point instead"); - declareEnum("outside-drop", "Handling drops outside of cells", createAndInsert).addValues(insertOutsideCell); - declareEnum("outside-visible-drop", "Handling drops outside of visible cells", showAndInsert) - .addValues(insertOutsideVisibleCell); - declareEnum("outside-landedit", "Handling terrain edit outside of cells", createAndLandEdit) - .setTooltip("Behavior of terrain editing, if land editing brush reaches an area without cell record.") - .addValues(landeditOutsideCell); - declareEnum("outside-visible-landedit", "Handling terrain edit outside of visible cells", showAndLandEdit) - .setTooltip("Behavior of terrain editing, if land editing brush reaches an area that is not currently visible.") - .addValues(landeditOutsideVisibleCell); - declareInt("texturebrush-maximumsize", "Maximum texture brush size", 50).setMin(1); - declareInt("shapebrush-maximumsize", "Maximum height edit brush size", 100) + declareEnum(mValues->mSceneEditing.mOutsideDrop, "Handling drops outside of cells"); + declareEnum(mValues->mSceneEditing.mOutsideVisibleDrop, "Handling drops outside of visible cells"); + declareEnum(mValues->mSceneEditing.mOutsideLandedit, "Handling terrain edit outside of cells") + .setTooltip("Behavior of terrain editing, if land editing brush reaches an area without cell record."); + declareEnum(mValues->mSceneEditing.mOutsideVisibleLandedit, "Handling terrain edit outside of visible cells") + .setTooltip( + "Behavior of terrain editing, if land editing brush reaches an area that is not currently visible."); + declareInt(mValues->mSceneEditing.mTexturebrushMaximumsize, "Maximum texture brush size").setMin(1); + declareInt(mValues->mSceneEditing.mShapebrushMaximumsize, "Maximum height edit brush size") .setTooltip("Setting for the slider range of brush size in terrain height editing.") .setMin(1); - declareBool("landedit-post-smoothpainting", "Smooth land after painting height", false) + declareBool(mValues->mSceneEditing.mLandeditPostSmoothpainting, "Smooth land after painting height") .setTooltip("Raise and lower tools will leave bumpy finish without this option"); - declareDouble("landedit-post-smoothstrength", "Smoothing strength (post-edit)", 0.25) + declareDouble(mValues->mSceneEditing.mLandeditPostSmoothstrength, "Smoothing strength (post-edit)") .setTooltip( "If smoothing land after painting height is used, this is the percentage of smooth applied afterwards. " "Negative values may be used to roughen instead of smooth.") .setMin(-1) .setMax(1); - declareBool("open-list-view", "Open displays list view", false) + declareBool(mValues->mSceneEditing.mOpenListView, "Open displays list view") .setTooltip( "When opening a reference from the scene view, it will open the" " instance list view instead of the individual instance record view."); - declareEnum("primary-select-action", "Action for primary select", SelectOnly) + declareEnum(mValues->mSceneEditing.mPrimarySelectAction, "Action for primary select") .setTooltip( "Selection can be chosen between select only, add to selection, remove from selection and invert " - "selection.") - .addValues(primarySelectAction); - declareEnum("secondary-select-action", "Action for secondary select", SelectAdd) + "selection."); + declareEnum(mValues->mSceneEditing.mSecondarySelectAction, "Action for secondary select") .setTooltip( "Selection can be chosen between select only, add to selection, remove from selection and invert " - "selection.") - .addValues(secondarySelectAction); + "selection."); declareCategory("Key Bindings"); declareSubcategory("Document"); - declareShortcut("document-file-newgame", "New Game", QKeySequence(Qt::ControlModifier | Qt::Key_N)); - declareShortcut("document-file-newaddon", "New Addon", QKeySequence()); - declareShortcut("document-file-open", "Open", QKeySequence(Qt::ControlModifier | Qt::Key_O)); - declareShortcut("document-file-save", "Save", QKeySequence(Qt::ControlModifier | Qt::Key_S)); - declareShortcut("document-help-help", "Help", QKeySequence(Qt::Key_F1)); - declareShortcut("document-help-tutorial", "Tutorial", QKeySequence()); - declareShortcut("document-file-verify", "Verify", QKeySequence()); - declareShortcut("document-file-merge", "Merge", QKeySequence()); - declareShortcut("document-file-errorlog", "Open Load Error Log", QKeySequence()); - declareShortcut("document-file-metadata", "Meta Data", QKeySequence()); - declareShortcut("document-file-close", "Close Document", QKeySequence(Qt::ControlModifier | Qt::Key_W)); - declareShortcut("document-file-exit", "Exit Application", QKeySequence(Qt::ControlModifier | Qt::Key_Q)); - declareShortcut("document-edit-undo", "Undo", QKeySequence(Qt::ControlModifier | Qt::Key_Z)); - declareShortcut("document-edit-redo", "Redo", QKeySequence(Qt::ControlModifier | Qt::ShiftModifier | Qt::Key_Z)); - declareShortcut("document-edit-preferences", "Open Preferences", QKeySequence()); - declareShortcut("document-edit-search", "Search", QKeySequence(Qt::ControlModifier | Qt::Key_F)); - declareShortcut("document-view-newview", "New View", QKeySequence()); - declareShortcut("document-view-statusbar", "Toggle Status Bar", QKeySequence()); - declareShortcut("document-view-filters", "Open Filter List", QKeySequence()); - declareShortcut("document-world-regions", "Open Region List", QKeySequence()); - declareShortcut("document-world-cells", "Open Cell List", QKeySequence()); - declareShortcut("document-world-referencables", "Open Object List", QKeySequence()); - declareShortcut("document-world-references", "Open Instance List", QKeySequence()); - declareShortcut("document-world-lands", "Open Lands List", QKeySequence()); - declareShortcut("document-world-landtextures", "Open Land Textures List", QKeySequence()); - declareShortcut("document-world-pathgrid", "Open Pathgrid List", QKeySequence()); - declareShortcut("document-world-regionmap", "Open Region Map", QKeySequence()); - declareShortcut("document-mechanics-globals", "Open Global List", QKeySequence()); - declareShortcut("document-mechanics-gamesettings", "Open Game Settings", QKeySequence()); - declareShortcut("document-mechanics-scripts", "Open Script List", QKeySequence()); - declareShortcut("document-mechanics-spells", "Open Spell List", QKeySequence()); - declareShortcut("document-mechanics-enchantments", "Open Enchantment List", QKeySequence()); - declareShortcut("document-mechanics-magiceffects", "Open Magic Effect List", QKeySequence()); - declareShortcut("document-mechanics-startscripts", "Open Start Script List", QKeySequence()); - declareShortcut("document-character-skills", "Open Skill List", QKeySequence()); - declareShortcut("document-character-classes", "Open Class List", QKeySequence()); - declareShortcut("document-character-factions", "Open Faction List", QKeySequence()); - declareShortcut("document-character-races", "Open Race List", QKeySequence()); - declareShortcut("document-character-birthsigns", "Open Birthsign List", QKeySequence()); - declareShortcut("document-character-topics", "Open Topic List", QKeySequence()); - declareShortcut("document-character-journals", "Open Journal List", QKeySequence()); - declareShortcut("document-character-topicinfos", "Open Topic Info List", QKeySequence()); - declareShortcut("document-character-journalinfos", "Open Journal Info List", QKeySequence()); - declareShortcut("document-character-bodyparts", "Open Body Part List", QKeySequence()); - declareShortcut("document-assets-reload", "Reload Assets", QKeySequence(Qt::Key_F5)); - declareShortcut("document-assets-sounds", "Open Sound Asset List", QKeySequence()); - declareShortcut("document-assets-soundgens", "Open Sound Generator List", QKeySequence()); - declareShortcut("document-assets-meshes", "Open Mesh Asset List", QKeySequence()); - declareShortcut("document-assets-icons", "Open Icon Asset List", QKeySequence()); - declareShortcut("document-assets-music", "Open Music Asset List", QKeySequence()); - declareShortcut("document-assets-soundres", "Open Sound File List", QKeySequence()); - declareShortcut("document-assets-textures", "Open Texture Asset List", QKeySequence()); - declareShortcut("document-assets-videos", "Open Video Asset List", QKeySequence()); - declareShortcut("document-debug-run", "Run Debug", QKeySequence()); - declareShortcut("document-debug-shutdown", "Stop Debug", QKeySequence()); - declareShortcut("document-debug-profiles", "Debug Profiles", QKeySequence()); - declareShortcut("document-debug-runlog", "Open Run Log", QKeySequence()); + declareShortcut(mValues->mKeyBindings.mDocumentFileNewgame, "New Game"); + declareShortcut(mValues->mKeyBindings.mDocumentFileNewaddon, "New Addon"); + declareShortcut(mValues->mKeyBindings.mDocumentFileOpen, "Open"); + declareShortcut(mValues->mKeyBindings.mDocumentFileSave, "Save"); + declareShortcut(mValues->mKeyBindings.mDocumentHelpHelp, "Help"); + declareShortcut(mValues->mKeyBindings.mDocumentHelpTutorial, "Tutorial"); + declareShortcut(mValues->mKeyBindings.mDocumentFileVerify, "Verify"); + declareShortcut(mValues->mKeyBindings.mDocumentFileMerge, "Merge"); + declareShortcut(mValues->mKeyBindings.mDocumentFileErrorlog, "Open Load Error Log"); + declareShortcut(mValues->mKeyBindings.mDocumentFileMetadata, "Meta Data"); + declareShortcut(mValues->mKeyBindings.mDocumentFileClose, "Close Document"); + declareShortcut(mValues->mKeyBindings.mDocumentFileExit, "Exit Application"); + declareShortcut(mValues->mKeyBindings.mDocumentEditUndo, "Undo"); + declareShortcut(mValues->mKeyBindings.mDocumentEditRedo, "Redo"); + declareShortcut(mValues->mKeyBindings.mDocumentEditPreferences, "Open Preferences"); + declareShortcut(mValues->mKeyBindings.mDocumentEditSearch, "Search"); + declareShortcut(mValues->mKeyBindings.mDocumentViewNewview, "New View"); + declareShortcut(mValues->mKeyBindings.mDocumentViewStatusbar, "Toggle Status Bar"); + declareShortcut(mValues->mKeyBindings.mDocumentViewFilters, "Open Filter List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldRegions, "Open Region List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldCells, "Open Cell List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldReferencables, "Open Object List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldReferences, "Open Instance List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldLands, "Open Lands List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldLandtextures, "Open Land Textures List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldPathgrid, "Open Pathgrid List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldRegionmap, "Open Region Map"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsGlobals, "Open Global List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsGamesettings, "Open Game Settings"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsScripts, "Open Script List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsSpells, "Open Spell List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsEnchantments, "Open Enchantment List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsMagiceffects, "Open Magic Effect List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsStartscripts, "Open Start Script List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterSkills, "Open Skill List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterClasses, "Open Class List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterFactions, "Open Faction List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterRaces, "Open Race List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterBirthsigns, "Open Birthsign List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterTopics, "Open Topic List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterJournals, "Open Journal List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterTopicinfos, "Open Topic Info List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterJournalinfos, "Open Journal Info List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterBodyparts, "Open Body Part List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsReload, "Reload Assets"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsSounds, "Open Sound Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsSoundgens, "Open Sound Generator List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsMeshes, "Open Mesh Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsIcons, "Open Icon Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsMusic, "Open Music Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsSoundres, "Open Sound File List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsTextures, "Open Texture Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsVideos, "Open Video Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentDebugRun, "Run Debug"); + declareShortcut(mValues->mKeyBindings.mDocumentDebugShutdown, "Stop Debug"); + declareShortcut(mValues->mKeyBindings.mDocumentDebugProfiles, "Debug Profiles"); + declareShortcut(mValues->mKeyBindings.mDocumentDebugRunlog, "Open Run Log"); declareSubcategory("Table"); - declareShortcut("table-edit", "Edit Record", QKeySequence()); - declareShortcut("table-add", "Add Row/Record", QKeySequence(Qt::ShiftModifier | Qt::Key_A)); - declareShortcut("table-clone", "Clone Record", QKeySequence(Qt::ShiftModifier | Qt::Key_D)); - declareShortcut("touch-record", "Touch Record", QKeySequence()); - declareShortcut("table-revert", "Revert Record", QKeySequence()); - declareShortcut("table-remove", "Remove Row/Record", QKeySequence(Qt::Key_Delete)); - declareShortcut("table-moveup", "Move Record Up", QKeySequence()); - declareShortcut("table-movedown", "Move Record Down", QKeySequence()); - declareShortcut("table-view", "View Record", QKeySequence(Qt::ShiftModifier | Qt::Key_C)); - declareShortcut("table-preview", "Preview Record", QKeySequence(Qt::ShiftModifier | Qt::Key_V)); - declareShortcut("table-extendeddelete", "Extended Record Deletion", QKeySequence()); - declareShortcut("table-extendedrevert", "Extended Record Revertion", QKeySequence()); + declareShortcut(mValues->mKeyBindings.mTableEdit, "Edit Record"); + declareShortcut(mValues->mKeyBindings.mTableAdd, "Add Row/Record"); + declareShortcut(mValues->mKeyBindings.mTableClone, "Clone Record"); + declareShortcut(mValues->mKeyBindings.mTouchRecord, "Touch Record"); + declareShortcut(mValues->mKeyBindings.mTableRevert, "Revert Record"); + declareShortcut(mValues->mKeyBindings.mTableRemove, "Remove Row/Record"); + declareShortcut(mValues->mKeyBindings.mTableMoveup, "Move Record Up"); + declareShortcut(mValues->mKeyBindings.mTableMovedown, "Move Record Down"); + declareShortcut(mValues->mKeyBindings.mTableView, "View Record"); + declareShortcut(mValues->mKeyBindings.mTablePreview, "Preview Record"); + declareShortcut(mValues->mKeyBindings.mTableExtendeddelete, "Extended Record Deletion"); + declareShortcut(mValues->mKeyBindings.mTableExtendedrevert, "Extended Record Revertion"); declareSubcategory("Report Table"); - declareShortcut("reporttable-show", "Show Report", QKeySequence()); - declareShortcut("reporttable-remove", "Remove Report", QKeySequence(Qt::Key_Delete)); - declareShortcut("reporttable-replace", "Replace Report", QKeySequence()); - declareShortcut("reporttable-refresh", "Refresh Report", QKeySequence()); + declareShortcut(mValues->mKeyBindings.mReporttableShow, "Show Report"); + declareShortcut(mValues->mKeyBindings.mReporttableRemove, "Remove Report"); + declareShortcut(mValues->mKeyBindings.mReporttableReplace, "Replace Report"); + declareShortcut(mValues->mKeyBindings.mReporttableRefresh, "Refresh Report"); declareSubcategory("Scene"); - declareShortcut("scene-navi-primary", "Camera Rotation From Mouse Movement", QKeySequence(Qt::LeftButton)); - declareShortcut("scene-navi-secondary", "Camera Translation From Mouse Movement", - QKeySequence(Qt::ControlModifier | (int)Qt::LeftButton)); - declareShortcut("scene-open-primary", "Primary Open", QKeySequence(Qt::ShiftModifier | (int)Qt::LeftButton)); - declareShortcut("scene-edit-primary", "Primary Edit", QKeySequence(Qt::RightButton)); - declareShortcut("scene-edit-secondary", "Secondary Edit", QKeySequence(Qt::ControlModifier | (int)Qt::RightButton)); - declareShortcut("scene-select-primary", "Primary Select", QKeySequence(Qt::MiddleButton)); - declareShortcut( - "scene-select-secondary", "Secondary Select", QKeySequence(Qt::ControlModifier | (int)Qt::MiddleButton)); - declareShortcut( - "scene-select-tertiary", "Tertiary Select", QKeySequence(Qt::ShiftModifier | (int)Qt::MiddleButton)); - declareModifier("scene-speed-modifier", "Speed Modifier", Qt::Key_Shift); - declareShortcut("scene-delete", "Delete Instance", QKeySequence(Qt::Key_Delete)); - declareShortcut("scene-instance-drop-terrain", "Drop to terrain level", QKeySequence(Qt::Key_G)); - declareShortcut("scene-instance-drop-collision", "Drop to collision", QKeySequence(Qt::Key_H)); - declareShortcut("scene-instance-drop-terrain-separately", "Drop to terrain level separately", QKeySequence()); - declareShortcut("scene-instance-drop-collision-separately", "Drop to collision separately", QKeySequence()); - declareShortcut("scene-load-cam-cell", "Load Camera Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_5)); - declareShortcut("scene-load-cam-eastcell", "Load East Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_6)); - declareShortcut("scene-load-cam-northcell", "Load North Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_8)); - declareShortcut("scene-load-cam-westcell", "Load West Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_4)); - declareShortcut("scene-load-cam-southcell", "Load South Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_2)); - declareShortcut("scene-edit-abort", "Abort", QKeySequence(Qt::Key_Escape)); - declareShortcut("scene-focus-toolbar", "Toggle Toolbar Focus", QKeySequence(Qt::Key_T)); - declareShortcut("scene-render-stats", "Debug Rendering Stats", QKeySequence(Qt::Key_F3)); + declareShortcut(mValues->mKeyBindings.mSceneNaviPrimary, "Camera Rotation From Mouse Movement"); + declareShortcut(mValues->mKeyBindings.mSceneNaviSecondary, "Camera Translation From Mouse Movement"); + declareShortcut(mValues->mKeyBindings.mSceneOpenPrimary, "Primary Open"); + declareShortcut(mValues->mKeyBindings.mSceneEditPrimary, "Primary Edit"); + declareShortcut(mValues->mKeyBindings.mSceneEditSecondary, "Secondary Edit"); + declareShortcut(mValues->mKeyBindings.mSceneSelectPrimary, "Primary Select"); + declareShortcut(mValues->mKeyBindings.mSceneSelectSecondary, "Secondary Select"); + declareShortcut(mValues->mKeyBindings.mSceneSelectTertiary, "Tertiary Select"); + declareModifier(mValues->mKeyBindings.mSceneSpeedModifier, "Speed Modifier"); + declareShortcut(mValues->mKeyBindings.mSceneDelete, "Delete Instance"); + declareShortcut(mValues->mKeyBindings.mSceneInstanceDropTerrain, "Drop to terrain level"); + declareShortcut(mValues->mKeyBindings.mSceneInstanceDropCollision, "Drop to collision"); + declareShortcut(mValues->mKeyBindings.mSceneInstanceDropTerrainSeparately, "Drop to terrain level separately"); + declareShortcut(mValues->mKeyBindings.mSceneInstanceDropCollisionSeparately, "Drop to collision separately"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamCell, "Load Camera Cell"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamEastcell, "Load East Cell"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamNorthcell, "Load North Cell"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamWestcell, "Load West Cell"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamSouthcell, "Load South Cell"); + declareShortcut(mValues->mKeyBindings.mSceneEditAbort, "Abort"); + declareShortcut(mValues->mKeyBindings.mSceneFocusToolbar, "Toggle Toolbar Focus"); + declareShortcut(mValues->mKeyBindings.mSceneRenderStats, "Debug Rendering Stats"); + declareShortcut(mValues->mKeyBindings.mSceneDuplicate, "Duplicate Instance"); + declareShortcut(mValues->mKeyBindings.mSceneClearSelection, "Clear Selection"); + declareShortcut(mValues->mKeyBindings.mSceneUnhideAll, "Unhide All Objects"); + declareShortcut(mValues->mKeyBindings.mSceneToggleVisibility, "Toggle Selection Visibility"); + declareShortcut(mValues->mKeyBindings.mSceneGroup0, "Selection Group 0"); + declareShortcut(mValues->mKeyBindings.mSceneSave0, "Save Group 0"); + declareShortcut(mValues->mKeyBindings.mSceneGroup1, "Select Group 1"); + declareShortcut(mValues->mKeyBindings.mSceneSave1, "Save Group 1"); + declareShortcut(mValues->mKeyBindings.mSceneGroup2, "Select Group 2"); + declareShortcut(mValues->mKeyBindings.mSceneSave2, "Save Group 2"); + declareShortcut(mValues->mKeyBindings.mSceneGroup3, "Select Group 3"); + declareShortcut(mValues->mKeyBindings.mSceneSave3, "Save Group 3"); + declareShortcut(mValues->mKeyBindings.mSceneGroup4, "Select Group 4"); + declareShortcut(mValues->mKeyBindings.mSceneSave4, "Save Group 4"); + declareShortcut(mValues->mKeyBindings.mSceneGroup5, "Selection Group 5"); + declareShortcut(mValues->mKeyBindings.mSceneSave5, "Save Group 5"); + declareShortcut(mValues->mKeyBindings.mSceneGroup6, "Selection Group 6"); + declareShortcut(mValues->mKeyBindings.mSceneSave6, "Save Group 6"); + declareShortcut(mValues->mKeyBindings.mSceneGroup7, "Selection Group 7"); + declareShortcut(mValues->mKeyBindings.mSceneSave7, "Save Group 7"); + declareShortcut(mValues->mKeyBindings.mSceneGroup8, "Selection Group 8"); + declareShortcut(mValues->mKeyBindings.mSceneSave8, "Save Group 8"); + declareShortcut(mValues->mKeyBindings.mSceneGroup9, "Selection Group 9"); + declareShortcut(mValues->mKeyBindings.mSceneSave9, "Save Group 9"); declareSubcategory("1st/Free Camera"); - declareShortcut("free-forward", "Forward", QKeySequence(Qt::Key_W)); - declareShortcut("free-backward", "Backward", QKeySequence(Qt::Key_S)); - declareShortcut("free-left", "Left", QKeySequence(Qt::Key_A)); - declareShortcut("free-right", "Right", QKeySequence(Qt::Key_D)); - declareShortcut("free-roll-left", "Roll Left", QKeySequence(Qt::Key_Q)); - declareShortcut("free-roll-right", "Roll Right", QKeySequence(Qt::Key_E)); - declareShortcut("free-speed-mode", "Toggle Speed Mode", QKeySequence(Qt::Key_F)); + declareShortcut(mValues->mKeyBindings.mFreeForward, "Forward"); + declareShortcut(mValues->mKeyBindings.mFreeBackward, "Backward"); + declareShortcut(mValues->mKeyBindings.mFreeLeft, "Left"); + declareShortcut(mValues->mKeyBindings.mFreeRight, "Right"); + declareShortcut(mValues->mKeyBindings.mFreeRollLeft, "Roll Left"); + declareShortcut(mValues->mKeyBindings.mFreeRollRight, "Roll Right"); + declareShortcut(mValues->mKeyBindings.mFreeSpeedMode, "Toggle Speed Mode"); declareSubcategory("Orbit Camera"); - declareShortcut("orbit-up", "Up", QKeySequence(Qt::Key_W)); - declareShortcut("orbit-down", "Down", QKeySequence(Qt::Key_S)); - declareShortcut("orbit-left", "Left", QKeySequence(Qt::Key_A)); - declareShortcut("orbit-right", "Right", QKeySequence(Qt::Key_D)); - declareShortcut("orbit-roll-left", "Roll Left", QKeySequence(Qt::Key_Q)); - declareShortcut("orbit-roll-right", "Roll Right", QKeySequence(Qt::Key_E)); - declareShortcut("orbit-speed-mode", "Toggle Speed Mode", QKeySequence(Qt::Key_F)); - declareShortcut("orbit-center-selection", "Center On Selected", QKeySequence(Qt::Key_C)); + declareShortcut(mValues->mKeyBindings.mOrbitUp, "Up"); + declareShortcut(mValues->mKeyBindings.mOrbitDown, "Down"); + declareShortcut(mValues->mKeyBindings.mOrbitLeft, "Left"); + declareShortcut(mValues->mKeyBindings.mOrbitRight, "Right"); + declareShortcut(mValues->mKeyBindings.mOrbitRollLeft, "Roll Left"); + declareShortcut(mValues->mKeyBindings.mOrbitRollRight, "Roll Right"); + declareShortcut(mValues->mKeyBindings.mOrbitSpeedMode, "Toggle Speed Mode"); + declareShortcut(mValues->mKeyBindings.mOrbitCenterSelection, "Center On Selected"); declareSubcategory("Script Editor"); - declareShortcut("script-editor-comment", "Comment Selection", QKeySequence()); - declareShortcut("script-editor-uncomment", "Uncomment Selection", QKeySequence()); + declareShortcut(mValues->mKeyBindings.mScriptEditorComment, "Comment Selection"); + declareShortcut(mValues->mKeyBindings.mScriptEditorUncomment, "Uncomment Selection"); declareCategory("Models"); - declareString("baseanim", "base animations", "meshes/base_anim.nif") - .setTooltip("3rd person base model with textkeys-data"); - declareString("baseanimkna", "base animations, kna", "meshes/base_animkna.nif") + declareString(mValues->mModels.mBaseanim, "base animations").setTooltip("3rd person base model with textkeys-data"); + declareString(mValues->mModels.mBaseanimkna, "base animations, kna") .setTooltip("3rd person beast race base model with textkeys-data"); - declareString("baseanimfemale", "base animations, female", "meshes/base_anim_female.nif") + declareString(mValues->mModels.mBaseanimfemale, "base animations, female") .setTooltip("3rd person female base model with textkeys-data"); - declareString("wolfskin", "base animations, wolf", "meshes/wolf/skin.nif").setTooltip("3rd person werewolf skin"); + declareString(mValues->mModels.mWolfskin, "base animations, wolf").setTooltip("3rd person werewolf skin"); } void CSMPrefs::State::declareCategory(const std::string& key) @@ -463,71 +422,52 @@ void CSMPrefs::State::declareCategory(const std::string& key) } } -CSMPrefs::IntSetting& CSMPrefs::State::declareInt(const std::string& key, const std::string& label, int default_) +CSMPrefs::IntSetting& CSMPrefs::State::declareInt(Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - setDefault(key, std::to_string(default_)); - - default_ = Settings::Manager::getInt(key, mCurrentCategory->second.getKey()); - - CSMPrefs::IntSetting* setting = new CSMPrefs::IntSetting(&mCurrentCategory->second, &mMutex, key, label, default_); + CSMPrefs::IntSetting* setting + = new CSMPrefs::IntSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } -CSMPrefs::DoubleSetting& CSMPrefs::State::declareDouble( - const std::string& key, const std::string& label, double default_) +CSMPrefs::DoubleSetting& CSMPrefs::State::declareDouble(Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - std::ostringstream stream; - stream << default_; - setDefault(key, stream.str()); - - default_ = Settings::Manager::getFloat(key, mCurrentCategory->second.getKey()); - CSMPrefs::DoubleSetting* setting - = new CSMPrefs::DoubleSetting(&mCurrentCategory->second, &mMutex, key, label, default_); + = new CSMPrefs::DoubleSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } -CSMPrefs::BoolSetting& CSMPrefs::State::declareBool(const std::string& key, const std::string& label, bool default_) +CSMPrefs::BoolSetting& CSMPrefs::State::declareBool(Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - setDefault(key, default_ ? "true" : "false"); - - default_ = Settings::Manager::getBool(key, mCurrentCategory->second.getKey()); - CSMPrefs::BoolSetting* setting - = new CSMPrefs::BoolSetting(&mCurrentCategory->second, &mMutex, key, label, default_); + = new CSMPrefs::BoolSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } -CSMPrefs::EnumSetting& CSMPrefs::State::declareEnum( - const std::string& key, const std::string& label, EnumValue default_) +CSMPrefs::EnumSetting& CSMPrefs::State::declareEnum(EnumSettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - setDefault(key, default_.mValue); - - default_.mValue = Settings::Manager::getString(key, mCurrentCategory->second.getKey()); - - CSMPrefs::EnumSetting* setting - = new CSMPrefs::EnumSetting(&mCurrentCategory->second, &mMutex, key, label, default_); + CSMPrefs::EnumSetting* setting = new CSMPrefs::EnumSetting( + &mCurrentCategory->second, &mMutex, value.getValue().mName, label, value.getEnumValues(), *mIndex); mCurrentCategory->second.addSetting(setting); @@ -535,18 +475,13 @@ CSMPrefs::EnumSetting& CSMPrefs::State::declareEnum( } CSMPrefs::ColourSetting& CSMPrefs::State::declareColour( - const std::string& key, const std::string& label, QColor default_) + Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - setDefault(key, default_.name().toUtf8().data()); - - default_.setNamedColor( - QString::fromUtf8(Settings::Manager::getString(key, mCurrentCategory->second.getKey()).c_str())); - CSMPrefs::ColourSetting* setting - = new CSMPrefs::ColourSetting(&mCurrentCategory->second, &mMutex, key, label, default_); + = new CSMPrefs::ColourSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); @@ -554,39 +489,32 @@ CSMPrefs::ColourSetting& CSMPrefs::State::declareColour( } CSMPrefs::ShortcutSetting& CSMPrefs::State::declareShortcut( - const std::string& key, const std::string& label, const QKeySequence& default_) + Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - std::string seqStr = getShortcutManager().convertToString(default_); - setDefault(key, seqStr); - // Setup with actual data QKeySequence sequence; - getShortcutManager().convertFromString( - Settings::Manager::getString(key, mCurrentCategory->second.getKey()), sequence); - getShortcutManager().setSequence(key, sequence); + getShortcutManager().convertFromString(value, sequence); + getShortcutManager().setSequence(value.mName, sequence); - CSMPrefs::ShortcutSetting* setting = new CSMPrefs::ShortcutSetting(&mCurrentCategory->second, &mMutex, key, label); + CSMPrefs::ShortcutSetting* setting + = new CSMPrefs::ShortcutSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } CSMPrefs::StringSetting& CSMPrefs::State::declareString( - const std::string& key, const std::string& label, std::string default_) + Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - setDefault(key, default_); - - default_ = Settings::Manager::getString(key, mCurrentCategory->second.getKey()); - CSMPrefs::StringSetting* setting - = new CSMPrefs::StringSetting(&mCurrentCategory->second, &mMutex, key, label, default_); + = new CSMPrefs::StringSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); @@ -594,55 +522,31 @@ CSMPrefs::StringSetting& CSMPrefs::State::declareString( } CSMPrefs::ModifierSetting& CSMPrefs::State::declareModifier( - const std::string& key, const std::string& label, int default_) + Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - std::string modStr = getShortcutManager().convertToString(default_); - setDefault(key, modStr); - // Setup with actual data int modifier; - getShortcutManager().convertFromString( - Settings::Manager::getString(key, mCurrentCategory->second.getKey()), modifier); - getShortcutManager().setModifier(key, modifier); + getShortcutManager().convertFromString(value.get(), modifier); + getShortcutManager().setModifier(value.mName, modifier); - CSMPrefs::ModifierSetting* setting = new CSMPrefs::ModifierSetting(&mCurrentCategory->second, &mMutex, key, label); + CSMPrefs::ModifierSetting* setting + = new CSMPrefs::ModifierSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } -void CSMPrefs::State::declareSeparator() +void CSMPrefs::State::declareSubcategory(const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - CSMPrefs::Setting* setting = new CSMPrefs::Setting(&mCurrentCategory->second, &mMutex, "", ""); - - mCurrentCategory->second.addSetting(setting); -} - -void CSMPrefs::State::declareSubcategory(const std::string& label) -{ - if (mCurrentCategory == mCategories.end()) - throw std::logic_error("no category for setting"); - - CSMPrefs::Setting* setting = new CSMPrefs::Setting(&mCurrentCategory->second, &mMutex, "", label); - - mCurrentCategory->second.addSetting(setting); -} - -void CSMPrefs::State::setDefault(const std::string& key, const std::string& default_) -{ - Settings::CategorySetting fullKey(mCurrentCategory->second.getKey(), key); - - Settings::CategorySettingValueMap::iterator iter = Settings::Manager::mDefaultSettings.find(fullKey); - - if (iter == Settings::Manager::mDefaultSettings.end()) - Settings::Manager::mDefaultSettings.insert(std::make_pair(fullKey, default_)); + mCurrentCategory->second.addSubcategory( + new CSMPrefs::Subcategory(&mCurrentCategory->second, &mMutex, label, *mIndex)); } CSMPrefs::State::State(const Files::ConfigurationManager& configurationManager) @@ -650,6 +554,8 @@ CSMPrefs::State::State(const Files::ConfigurationManager& configurationManager) , mDefaultConfigFile("defaults-cs.bin") , mConfigurationManager(configurationManager) , mCurrentCategory(mCategories.end()) + , mIndex(std::make_unique()) + , mValues(std::make_unique(*mIndex)) { if (sThis) throw std::logic_error("An instance of CSMPRefs::State already exists"); @@ -709,27 +615,13 @@ CSMPrefs::State& CSMPrefs::State::get() void CSMPrefs::State::resetCategory(const std::string& category) { - for (Settings::CategorySettingValueMap::iterator i = Settings::Manager::mUserSettings.begin(); - i != Settings::Manager::mUserSettings.end(); ++i) - { - // if the category matches - if (i->first.first == category) - { - // mark the setting as changed - Settings::Manager::mChangedSettings.insert(std::make_pair(i->first.first, i->first.second)); - // reset the value to the default - i->second = Settings::Manager::mDefaultSettings[i->first]; - } - } - Collection::iterator container = mCategories.find(category); if (container != mCategories.end()) { - Category settings = container->second; - for (Category::Iterator i = settings.begin(); i != settings.end(); ++i) + for (Setting* setting : container->second) { - (*i)->updateWidget(); - update(**i); + setting->reset(); + update(*setting); } } } diff --git a/apps/opencs/model/prefs/state.hpp b/apps/opencs/model/prefs/state.hpp index 354f4552e3..821322d586 100644 --- a/apps/opencs/model/prefs/state.hpp +++ b/apps/opencs/model/prefs/state.hpp @@ -17,6 +17,11 @@ class QColor; +namespace Settings +{ + class Index; +} + namespace CSMPrefs { class IntSetting; @@ -27,6 +32,8 @@ namespace CSMPrefs class ModifierSetting; class Setting; class StringSetting; + class EnumSettingValue; + struct Values; /// \brief User settings state /// @@ -50,43 +57,40 @@ namespace CSMPrefs Collection mCategories; Iterator mCurrentCategory; QMutex mMutex; + std::unique_ptr mIndex; + std::unique_ptr mValues; - // not implemented - State(const State&); - State& operator=(const State&); - - private: void declare(); void declareCategory(const std::string& key); - IntSetting& declareInt(const std::string& key, const std::string& label, int default_); - DoubleSetting& declareDouble(const std::string& key, const std::string& label, double default_); + IntSetting& declareInt(Settings::SettingValue& value, const QString& label); - BoolSetting& declareBool(const std::string& key, const std::string& label, bool default_); + DoubleSetting& declareDouble(Settings::SettingValue& value, const QString& label); - EnumSetting& declareEnum(const std::string& key, const std::string& label, EnumValue default_); + BoolSetting& declareBool(Settings::SettingValue& value, const QString& label); - ColourSetting& declareColour(const std::string& key, const std::string& label, QColor default_); + EnumSetting& declareEnum(EnumSettingValue& value, const QString& label); - ShortcutSetting& declareShortcut( - const std::string& key, const std::string& label, const QKeySequence& default_); + ColourSetting& declareColour(Settings::SettingValue& value, const QString& label); - StringSetting& declareString(const std::string& key, const std::string& label, std::string default_); + ShortcutSetting& declareShortcut(Settings::SettingValue& value, const QString& label); - ModifierSetting& declareModifier(const std::string& key, const std::string& label, int modifier_); + StringSetting& declareString(Settings::SettingValue& value, const QString& label); - void declareSeparator(); + ModifierSetting& declareModifier(Settings::SettingValue& value, const QString& label); - void declareSubcategory(const std::string& label); - - void setDefault(const std::string& key, const std::string& default_); + void declareSubcategory(const QString& label); public: State(const Files::ConfigurationManager& configurationManager); + State(const State&) = delete; + ~State(); + State& operator=(const State&) = delete; + void save(); Iterator begin(); diff --git a/apps/opencs/model/prefs/stringsetting.cpp b/apps/opencs/model/prefs/stringsetting.cpp index 2a8fdd587a..10bd8cb558 100644 --- a/apps/opencs/model/prefs/stringsetting.cpp +++ b/apps/opencs/model/prefs/stringsetting.cpp @@ -12,9 +12,8 @@ #include "state.hpp" CSMPrefs::StringSetting::StringSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, std::string_view default_) - : Setting(parent, mutex, key, label) - , mDefault(default_) + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mWidget(nullptr) { } @@ -25,9 +24,9 @@ CSMPrefs::StringSetting& CSMPrefs::StringSetting::setTooltip(const std::string& return *this; } -std::pair CSMPrefs::StringSetting::makeWidgets(QWidget* parent) +CSMPrefs::SettingWidgets CSMPrefs::StringSetting::makeWidgets(QWidget* parent) { - mWidget = new QLineEdit(QString::fromUtf8(mDefault.c_str()), parent); + mWidget = new QLineEdit(QString::fromStdString(getValue()), parent); if (!mTooltip.empty()) { @@ -37,23 +36,17 @@ std::pair CSMPrefs::StringSetting::makeWidgets(QWidget* pare connect(mWidget, &QLineEdit::textChanged, this, &StringSetting::textChanged); - return std::make_pair(static_cast(nullptr), mWidget); + return SettingWidgets{ .mLabel = nullptr, .mInput = mWidget }; } void CSMPrefs::StringSetting::updateWidget() { if (mWidget) - { - mWidget->setText(QString::fromStdString(Settings::Manager::getString(getKey(), getParent()->getKey()))); - } + mWidget->setText(QString::fromStdString(getValue())); } void CSMPrefs::StringSetting::textChanged(const QString& text) { - { - QMutexLocker lock(getMutex()); - Settings::Manager::setString(getKey(), getParent()->getKey(), text.toStdString()); - } - + setValue(text.toStdString()); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/stringsetting.hpp b/apps/opencs/model/prefs/stringsetting.hpp index 4b5499ef86..0a7d2a4935 100644 --- a/apps/opencs/model/prefs/stringsetting.hpp +++ b/apps/opencs/model/prefs/stringsetting.hpp @@ -14,22 +14,22 @@ class QWidget; namespace CSMPrefs { class Category; - class StringSetting : public Setting + + class StringSetting final : public TypedSetting { Q_OBJECT std::string mTooltip; - std::string mDefault; QLineEdit* mWidget; public: - StringSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label, - std::string_view default_); + explicit StringSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); StringSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/subcategory.cpp b/apps/opencs/model/prefs/subcategory.cpp new file mode 100644 index 0000000000..815025daec --- /dev/null +++ b/apps/opencs/model/prefs/subcategory.cpp @@ -0,0 +1,18 @@ +#include "subcategory.hpp" + +#include + +namespace CSMPrefs +{ + class Category; + + Subcategory::Subcategory(Category* parent, QMutex* mutex, const QString& label, Settings::Index& index) + : Setting(parent, mutex, "", label, index) + { + } + + SettingWidgets Subcategory::makeWidgets(QWidget* /*parent*/) + { + return SettingWidgets{ .mLabel = nullptr, .mInput = nullptr }; + } +} diff --git a/apps/opencs/model/prefs/subcategory.hpp b/apps/opencs/model/prefs/subcategory.hpp new file mode 100644 index 0000000000..4c661ad0fa --- /dev/null +++ b/apps/opencs/model/prefs/subcategory.hpp @@ -0,0 +1,28 @@ +#ifndef OPENMW_APPS_OPENCS_MODEL_PREFS_SUBCATEGORY_H +#define OPENMW_APPS_OPENCS_MODEL_PREFS_SUBCATEGORY_H + +#include "setting.hpp" + +#include +#include + +namespace CSMPrefs +{ + class Category; + + class Subcategory final : public Setting + { + Q_OBJECT + + public: + explicit Subcategory(Category* parent, QMutex* mutex, const QString& label, Settings::Index& index); + + SettingWidgets makeWidgets(QWidget* parent) override; + + void updateWidget() override {} + + void reset() override {} + }; +} + +#endif diff --git a/apps/opencs/model/prefs/values.hpp b/apps/opencs/model/prefs/values.hpp new file mode 100644 index 0000000000..9899a239a9 --- /dev/null +++ b/apps/opencs/model/prefs/values.hpp @@ -0,0 +1,546 @@ +#ifndef OPENMW_APPS_OPENCS_MODEL_PREFS_VALUES_H +#define OPENMW_APPS_OPENCS_MODEL_PREFS_VALUES_H + +#include "enumvalueview.hpp" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace CSMPrefs +{ + class EnumSanitizer final : public Settings::Sanitizer + { + public: + explicit EnumSanitizer(std::span values) + : mValues(values) + { + } + + std::string apply(const std::string& value) const override + { + const auto hasValue = [&](const EnumValueView& v) { return v.mValue == value; }; + if (std::find_if(mValues.begin(), mValues.end(), hasValue) == mValues.end()) + { + std::ostringstream message; + message << "Invalid enum value: " << value; + throw std::runtime_error(message.str()); + } + return value; + } + + private: + std::span mValues; + }; + + inline std::unique_ptr> makeEnumSanitizerString( + std::span values) + { + return std::make_unique(values); + } + + class EnumSettingValue + { + public: + explicit EnumSettingValue(Settings::Index& index, std::string_view category, std::string_view name, + std::span values, std::size_t defaultValueIndex) + : mValue( + index, category, name, std::string(values[defaultValueIndex].mValue), makeEnumSanitizerString(values)) + , mEnumValues(values) + { + } + + Settings::SettingValue& getValue() { return mValue; } + + std::span getEnumValues() const { return mEnumValues; } + + private: + Settings::SettingValue mValue; + std::span mEnumValues; + }; + + struct WindowsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Windows"; + + static constexpr std::array sMainwindowScrollbarValues{ + EnumValueView{ + "Scrollbar Only", "Simple addition of scrollbars, the view window does not grow automatically." }, + EnumValueView{ "Grow Only", "The view window grows as subviews are added. No scrollbars." }, + EnumValueView{ + "Grow then Scroll", "The view window grows. The scrollbar appears once it cannot grow any further." }, + }; + + Settings::SettingValue mDefaultWidth{ mIndex, sName, "default-width", 800 }; + Settings::SettingValue mDefaultHeight{ mIndex, sName, "default-height", 600 }; + Settings::SettingValue mShowStatusbar{ mIndex, sName, "show-statusbar", true }; + Settings::SettingValue mReuse{ mIndex, sName, "reuse", true }; + Settings::SettingValue mMaxSubviews{ mIndex, sName, "max-subviews", 256 }; + Settings::SettingValue mHideSubview{ mIndex, sName, "hide-subview", false }; + Settings::SettingValue mMinimumWidth{ mIndex, sName, "minimum-width", 325 }; + EnumSettingValue mMainwindowScrollbar{ mIndex, sName, "mainwindow-scrollbar", sMainwindowScrollbarValues, 0 }; + Settings::SettingValue mGrowLimit{ mIndex, sName, "grow-limit", false }; + }; + + struct RecordsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Records"; + + static constexpr std::array sRecordValues{ + EnumValueView{ "Icon and Text", "" }, + EnumValueView{ "Icon Only", "" }, + EnumValueView{ "Text Only", "" }, + }; + + EnumSettingValue mStatusFormat{ mIndex, sName, "status-format", sRecordValues, 0 }; + EnumSettingValue mTypeFormat{ mIndex, sName, "type-format", sRecordValues, 0 }; + }; + + struct IdTablesCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "ID Tables"; + + static constexpr std::array sDoubleClickValues{ + EnumValueView{ "Edit in Place", "Edit the clicked cell" }, + EnumValueView{ "Edit Record", "Open a dialogue subview for the clicked record" }, + EnumValueView{ "View", "Open a scene subview for the clicked record (not available everywhere)" }, + EnumValueView{ "Revert", "" }, + EnumValueView{ "Delete", "" }, + EnumValueView{ "Edit Record and Close", "" }, + EnumValueView{ + "View and Close", "Open a scene subview for the clicked record and close the table subview" }, + }; + + static constexpr std::array sJumpAndSelectValues{ + EnumValueView{ "Jump and Select", "Scroll new record into view and make it the selection" }, + EnumValueView{ "Jump Only", "Scroll new record into view" }, + EnumValueView{ "No Jump", "No special action" }, + }; + + EnumSettingValue mDouble{ mIndex, sName, "double", sDoubleClickValues, 0 }; + EnumSettingValue mDoubleS{ mIndex, sName, "double-s", sDoubleClickValues, 1 }; + EnumSettingValue mDoubleC{ mIndex, sName, "double-c", sDoubleClickValues, 2 }; + EnumSettingValue mDoubleSc{ mIndex, sName, "double-sc", sDoubleClickValues, 5 }; + EnumSettingValue mJumpToAdded{ mIndex, sName, "jump-to-added", sJumpAndSelectValues, 0 }; + Settings::SettingValue mExtendedConfig{ mIndex, sName, "extended-config", false }; + Settings::SettingValue mSubviewNewWindow{ mIndex, sName, "subview-new-window", false }; + }; + + struct IdDialoguesCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "ID Dialogues"; + + Settings::SettingValue mToolbar{ mIndex, sName, "toolbar", true }; + }; + + struct ReportsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Reports"; + + static constexpr std::array sReportValues{ + EnumValueView{ "None", "" }, + EnumValueView{ "Edit", "Open a table or dialogue suitable for addressing the listed report" }, + EnumValueView{ "Remove", "Remove the report from the report table" }, + EnumValueView{ "Edit And Remove", + "Open a table or dialogue suitable for addressing the listed report, then remove the report from the " + "report table" }, + }; + + EnumSettingValue mDouble{ mIndex, sName, "double", sReportValues, 1 }; + EnumSettingValue mDoubleS{ mIndex, sName, "double-s", sReportValues, 2 }; + EnumSettingValue mDoubleC{ mIndex, sName, "double-c", sReportValues, 3 }; + EnumSettingValue mDoubleSc{ mIndex, sName, "double-sc", sReportValues, 0 }; + Settings::SettingValue mIgnoreBaseRecords{ mIndex, sName, "ignore-base-records", false }; + }; + + struct SearchAndReplaceCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Search & Replace"; + + Settings::SettingValue mCharBefore{ mIndex, sName, "char-before", 10 }; + Settings::SettingValue mCharAfter{ mIndex, sName, "char-after", 10 }; + Settings::SettingValue mAutoDelete{ mIndex, sName, "auto-delete", true }; + }; + + struct ScriptsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Scripts"; + + static constexpr std::array sWarningValues{ + EnumValueView{ "Ignore", "Do not report warning" }, + EnumValueView{ "Normal", "Report warnings as warning" }, + EnumValueView{ "Strict", "Promote warning to an error" }, + }; + + Settings::SettingValue mShowLinenum{ mIndex, sName, "show-linenum", true }; + Settings::SettingValue mWrapLines{ mIndex, sName, "wrap-lines", false }; + Settings::SettingValue mMonoFont{ mIndex, sName, "mono-font", true }; + Settings::SettingValue mTabWidth{ mIndex, sName, "tab-width", 4 }; + EnumSettingValue mWarnings{ mIndex, sName, "warnings", sWarningValues, 1 }; + Settings::SettingValue mToolbar{ mIndex, sName, "toolbar", true }; + Settings::SettingValue mCompileDelay{ mIndex, sName, "compile-delay", 100 }; + Settings::SettingValue mErrorHeight{ mIndex, sName, "error-height", 100 }; + Settings::SettingValue mHighlightOccurrences{ mIndex, sName, "highlight-occurrences", true }; + Settings::SettingValue mColourHighlight{ mIndex, sName, "colour-highlight", "lightcyan" }; + Settings::SettingValue mColourInt{ mIndex, sName, "colour-int", "darkmagenta" }; + Settings::SettingValue mColourFloat{ mIndex, sName, "colour-float", "magenta" }; + Settings::SettingValue mColourName{ mIndex, sName, "colour-name", "grey" }; + Settings::SettingValue mColourKeyword{ mIndex, sName, "colour-keyword", "red" }; + Settings::SettingValue mColourSpecial{ mIndex, sName, "colour-special", "darkorange" }; + Settings::SettingValue mColourComment{ mIndex, sName, "colour-comment", "green" }; + Settings::SettingValue mColourId{ mIndex, sName, "colour-id", "blue" }; + }; + + struct GeneralInputCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "General Input"; + + Settings::SettingValue mCycle{ mIndex, sName, "cycle", false }; + }; + + struct SceneInputCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "3D Scene Input"; + + Settings::SettingValue mNaviWheelFactor{ mIndex, sName, "navi-wheel-factor", 8 }; + Settings::SettingValue mSNaviSensitivity{ mIndex, sName, "s-navi-sensitivity", 50 }; + Settings::SettingValue mPNaviFreeSensitivity{ mIndex, sName, "p-navi-free-sensitivity", 1 / 650.0 }; + Settings::SettingValue mPNaviFreeInvert{ mIndex, sName, "p-navi-free-invert", false }; + Settings::SettingValue mNaviFreeLinSpeed{ mIndex, sName, "navi-free-lin-speed", 1000 }; + Settings::SettingValue mNaviFreeRotSpeed{ mIndex, sName, "navi-free-rot-speed", 3.14 / 2 }; + Settings::SettingValue mNaviFreeSpeedMult{ mIndex, sName, "navi-free-speed-mult", 8 }; + Settings::SettingValue mPNaviOrbitSensitivity{ mIndex, sName, "p-navi-orbit-sensitivity", 1 / 650.0 }; + Settings::SettingValue mPNaviOrbitInvert{ mIndex, sName, "p-navi-orbit-invert", false }; + Settings::SettingValue mNaviOrbitRotSpeed{ mIndex, sName, "navi-orbit-rot-speed", 3.14 / 4 }; + Settings::SettingValue mNaviOrbitSpeedMult{ mIndex, sName, "navi-orbit-speed-mult", 4 }; + Settings::SettingValue mNaviOrbitConstRoll{ mIndex, sName, "navi-orbit-const-roll", true }; + Settings::SettingValue mContextSelect{ mIndex, sName, "context-select", false }; + Settings::SettingValue mDragFactor{ mIndex, sName, "drag-factor", 1 }; + Settings::SettingValue mDragWheelFactor{ mIndex, sName, "drag-wheel-factor", 1 }; + Settings::SettingValue mDragShiftFactor{ mIndex, sName, "drag-shift-factor", 4 }; + Settings::SettingValue mRotateFactor{ mIndex, sName, "rotate-factor", 0.007 }; + }; + + struct RenderingCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Rendering"; + + Settings::SettingValue mFramerateLimit{ mIndex, sName, "framerate-limit", 60 }; + Settings::SettingValue mCameraFov{ mIndex, sName, "camera-fov", 90 }; + Settings::SettingValue mCameraOrtho{ mIndex, sName, "camera-ortho", false }; + Settings::SettingValue mCameraOrthoSize{ mIndex, sName, "camera-ortho-size", 100 }; + Settings::SettingValue mObjectMarkerAlpha{ mIndex, sName, "object-marker-alpha", 0.5 }; + Settings::SettingValue mSceneUseGradient{ mIndex, sName, "scene-use-gradient", true }; + Settings::SettingValue mSceneDayBackgroundColour{ mIndex, sName, "scene-day-background-colour", + "#6e7880" }; + Settings::SettingValue mSceneDayGradientColour{ mIndex, sName, "scene-day-gradient-colour", + "#2f3333" }; + Settings::SettingValue mSceneBrightBackgroundColour{ mIndex, sName, + "scene-bright-background-colour", "#4f575c" }; + Settings::SettingValue mSceneBrightGradientColour{ mIndex, sName, "scene-bright-gradient-colour", + "#2f3333" }; + Settings::SettingValue mSceneNightBackgroundColour{ mIndex, sName, "scene-night-background-colour", + "#404d4f" }; + Settings::SettingValue mSceneNightGradientColour{ mIndex, sName, "scene-night-gradient-colour", + "#2f3333" }; + Settings::SettingValue mSceneDayNightSwitchNodes{ mIndex, sName, "scene-day-night-switch-nodes", true }; + }; + + struct TooltipsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Tooltips"; + + Settings::SettingValue mScene{ mIndex, sName, "scene", true }; + Settings::SettingValue mSceneHideBasic{ mIndex, sName, "scene-hide-basic", false }; + Settings::SettingValue mSceneDelay{ mIndex, sName, "scene-delay", 500 }; + }; + + struct SceneEditingCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "3D Scene Editing"; + + static constexpr std::array sInsertOutsideCellValues{ + EnumValueView{ "Create cell and insert", "" }, + EnumValueView{ "Discard", "" }, + EnumValueView{ "Insert anyway", "" }, + }; + + static constexpr std::array sInsertOutsideVisibleCellValues{ + EnumValueView{ "Show cell and insert", "" }, + EnumValueView{ "Discard", "" }, + EnumValueView{ "Insert anyway", "" }, + }; + + static constexpr std::array sLandEditOutsideCellValues{ + EnumValueView{ "Create cell and land, then edit", "" }, + EnumValueView{ "Discard", "" }, + }; + + static constexpr std::array sLandEditOutsideVisibleCellValues{ + EnumValueView{ "Show cell and edit", "" }, + EnumValueView{ "Discard", "" }, + }; + + static constexpr std::array sSelectAction{ + EnumValueView{ "Select only", "" }, + EnumValueView{ "Add to selection", "" }, + EnumValueView{ "Remove from selection", "" }, + EnumValueView{ "Invert selection", "" }, + }; + + Settings::SettingValue mGridsnapMovement{ mIndex, sName, "gridsnap-movement", 16 }; + Settings::SettingValue mGridsnapRotation{ mIndex, sName, "gridsnap-rotation", 15 }; + Settings::SettingValue mGridsnapScale{ mIndex, sName, "gridsnap-scale", 0.25 }; + Settings::SettingValue mDistance{ mIndex, sName, "distance", 50 }; + EnumSettingValue mOutsideDrop{ mIndex, sName, "outside-drop", sInsertOutsideCellValues, 0 }; + EnumSettingValue mOutsideVisibleDrop{ mIndex, sName, "outside-visible-drop", sInsertOutsideVisibleCellValues, + 0 }; + EnumSettingValue mOutsideLandedit{ mIndex, sName, "outside-landedit", sLandEditOutsideCellValues, 0 }; + EnumSettingValue mOutsideVisibleLandedit{ mIndex, sName, "outside-visible-landedit", + sLandEditOutsideVisibleCellValues, 0 }; + Settings::SettingValue mTexturebrushMaximumsize{ mIndex, sName, "texturebrush-maximumsize", 50 }; + Settings::SettingValue mShapebrushMaximumsize{ mIndex, sName, "shapebrush-maximumsize", 100 }; + Settings::SettingValue mLandeditPostSmoothpainting{ mIndex, sName, "landedit-post-smoothpainting", + false }; + Settings::SettingValue mLandeditPostSmoothstrength{ mIndex, sName, "landedit-post-smoothstrength", + 0.25 }; + Settings::SettingValue mOpenListView{ mIndex, sName, "open-list-view", false }; + EnumSettingValue mPrimarySelectAction{ mIndex, sName, "primary-select-action", sSelectAction, 0 }; + EnumSettingValue mSecondarySelectAction{ mIndex, sName, "secondary-select-action", sSelectAction, 1 }; + }; + + struct KeyBindingsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Key Bindings"; + + Settings::SettingValue mDocumentFileNewgame{ mIndex, sName, "document-file-newgame", "Ctrl+N" }; + Settings::SettingValue mDocumentFileNewaddon{ mIndex, sName, "document-file-newaddon", "" }; + Settings::SettingValue mDocumentFileOpen{ mIndex, sName, "document-file-open", "Ctrl+O" }; + Settings::SettingValue mDocumentFileSave{ mIndex, sName, "document-file-save", "Ctrl+S" }; + Settings::SettingValue mDocumentHelpHelp{ mIndex, sName, "document-help-help", "F1" }; + Settings::SettingValue mDocumentHelpTutorial{ mIndex, sName, "document-help-tutorial", "" }; + Settings::SettingValue mDocumentFileVerify{ mIndex, sName, "document-file-verify", "" }; + Settings::SettingValue mDocumentFileMerge{ mIndex, sName, "document-file-merge", "" }; + Settings::SettingValue mDocumentFileErrorlog{ mIndex, sName, "document-file-errorlog", "" }; + Settings::SettingValue mDocumentFileMetadata{ mIndex, sName, "document-file-metadata", "" }; + Settings::SettingValue mDocumentFileClose{ mIndex, sName, "document-file-close", "Ctrl+W" }; + Settings::SettingValue mDocumentFileExit{ mIndex, sName, "document-file-exit", "Ctrl+Q" }; + Settings::SettingValue mDocumentEditUndo{ mIndex, sName, "document-edit-undo", "Ctrl+Z" }; + Settings::SettingValue mDocumentEditRedo{ mIndex, sName, "document-edit-redo", "Ctrl+Shift+Z" }; + Settings::SettingValue mDocumentEditPreferences{ mIndex, sName, "document-edit-preferences", "" }; + Settings::SettingValue mDocumentEditSearch{ mIndex, sName, "document-edit-search", "Ctrl+F" }; + Settings::SettingValue mDocumentViewNewview{ mIndex, sName, "document-view-newview", "" }; + Settings::SettingValue mDocumentViewStatusbar{ mIndex, sName, "document-view-statusbar", "" }; + Settings::SettingValue mDocumentViewFilters{ mIndex, sName, "document-view-filters", "" }; + Settings::SettingValue mDocumentWorldRegions{ mIndex, sName, "document-world-regions", "" }; + Settings::SettingValue mDocumentWorldCells{ mIndex, sName, "document-world-cells", "" }; + Settings::SettingValue mDocumentWorldReferencables{ mIndex, sName, "document-world-referencables", + "" }; + Settings::SettingValue mDocumentWorldReferences{ mIndex, sName, "document-world-references", "" }; + Settings::SettingValue mDocumentWorldLands{ mIndex, sName, "document-world-lands", "" }; + Settings::SettingValue mDocumentWorldLandtextures{ mIndex, sName, "document-world-landtextures", + "" }; + Settings::SettingValue mDocumentWorldPathgrid{ mIndex, sName, "document-world-pathgrid", "" }; + Settings::SettingValue mDocumentWorldRegionmap{ mIndex, sName, "document-world-regionmap", "" }; + Settings::SettingValue mDocumentMechanicsGlobals{ mIndex, sName, "document-mechanics-globals", + "" }; + Settings::SettingValue mDocumentMechanicsGamesettings{ mIndex, sName, + "document-mechanics-gamesettings", "" }; + Settings::SettingValue mDocumentMechanicsScripts{ mIndex, sName, "document-mechanics-scripts", + "" }; + Settings::SettingValue mDocumentMechanicsSpells{ mIndex, sName, "document-mechanics-spells", "" }; + Settings::SettingValue mDocumentMechanicsEnchantments{ mIndex, sName, + "document-mechanics-enchantments", "" }; + Settings::SettingValue mDocumentMechanicsMagiceffects{ mIndex, sName, + "document-mechanics-magiceffects", "" }; + Settings::SettingValue mDocumentMechanicsStartscripts{ mIndex, sName, + "document-mechanics-startscripts", "" }; + Settings::SettingValue mDocumentCharacterSkills{ mIndex, sName, "document-character-skills", "" }; + Settings::SettingValue mDocumentCharacterClasses{ mIndex, sName, "document-character-classes", + "" }; + Settings::SettingValue mDocumentCharacterFactions{ mIndex, sName, "document-character-factions", + "" }; + Settings::SettingValue mDocumentCharacterRaces{ mIndex, sName, "document-character-races", "" }; + Settings::SettingValue mDocumentCharacterBirthsigns{ mIndex, sName, + "document-character-birthsigns", "" }; + Settings::SettingValue mDocumentCharacterTopics{ mIndex, sName, "document-character-topics", "" }; + Settings::SettingValue mDocumentCharacterJournals{ mIndex, sName, "document-character-journals", + "" }; + Settings::SettingValue mDocumentCharacterTopicinfos{ mIndex, sName, + "document-character-topicinfos", "" }; + Settings::SettingValue mDocumentCharacterJournalinfos{ mIndex, sName, + "document-character-journalinfos", "" }; + Settings::SettingValue mDocumentCharacterBodyparts{ mIndex, sName, "document-character-bodyparts", + "" }; + Settings::SettingValue mDocumentAssetsReload{ mIndex, sName, "document-assets-reload", "F5" }; + Settings::SettingValue mDocumentAssetsSounds{ mIndex, sName, "document-assets-sounds", "" }; + Settings::SettingValue mDocumentAssetsSoundgens{ mIndex, sName, "document-assets-soundgens", "" }; + Settings::SettingValue mDocumentAssetsMeshes{ mIndex, sName, "document-assets-meshes", "" }; + Settings::SettingValue mDocumentAssetsIcons{ mIndex, sName, "document-assets-icons", "" }; + Settings::SettingValue mDocumentAssetsMusic{ mIndex, sName, "document-assets-music", "" }; + Settings::SettingValue mDocumentAssetsSoundres{ mIndex, sName, "document-assets-soundres", "" }; + Settings::SettingValue mDocumentAssetsTextures{ mIndex, sName, "document-assets-textures", "" }; + Settings::SettingValue mDocumentAssetsVideos{ mIndex, sName, "document-assets-videos", "" }; + Settings::SettingValue mDocumentDebugRun{ mIndex, sName, "document-debug-run", "" }; + Settings::SettingValue mDocumentDebugShutdown{ mIndex, sName, "document-debug-shutdown", "" }; + Settings::SettingValue mDocumentDebugProfiles{ mIndex, sName, "document-debug-profiles", "" }; + Settings::SettingValue mDocumentDebugRunlog{ mIndex, sName, "document-debug-runlog", "" }; + Settings::SettingValue mTableEdit{ mIndex, sName, "table-edit", "" }; + Settings::SettingValue mTableAdd{ mIndex, sName, "table-add", "Shift+A" }; + Settings::SettingValue mTableClone{ mIndex, sName, "table-clone", "Shift+D" }; + Settings::SettingValue mTouchRecord{ mIndex, sName, "touch-record", "" }; + Settings::SettingValue mTableRevert{ mIndex, sName, "table-revert", "" }; + Settings::SettingValue mTableRemove{ mIndex, sName, "table-remove", "Delete" }; + Settings::SettingValue mTableMoveup{ mIndex, sName, "table-moveup", "" }; + Settings::SettingValue mTableMovedown{ mIndex, sName, "table-movedown", "" }; + Settings::SettingValue mTableView{ mIndex, sName, "table-view", "Shift+C" }; + Settings::SettingValue mTablePreview{ mIndex, sName, "table-preview", "Shift+V" }; + Settings::SettingValue mTableExtendeddelete{ mIndex, sName, "table-extendeddelete", "" }; + Settings::SettingValue mTableExtendedrevert{ mIndex, sName, "table-extendedrevert", "" }; + Settings::SettingValue mReporttableShow{ mIndex, sName, "reporttable-show", "" }; + Settings::SettingValue mReporttableRemove{ mIndex, sName, "reporttable-remove", "Delete" }; + Settings::SettingValue mReporttableReplace{ mIndex, sName, "reporttable-replace", "" }; + Settings::SettingValue mReporttableRefresh{ mIndex, sName, "reporttable-refresh", "" }; + Settings::SettingValue mSceneNaviPrimary{ mIndex, sName, "scene-navi-primary", "LMB" }; + Settings::SettingValue mSceneNaviSecondary{ mIndex, sName, "scene-navi-secondary", "Ctrl+LMB" }; + Settings::SettingValue mSceneOpenPrimary{ mIndex, sName, "scene-open-primary", "Shift+LMB" }; + Settings::SettingValue mSceneEditPrimary{ mIndex, sName, "scene-edit-primary", "RMB" }; + Settings::SettingValue mSceneEditSecondary{ mIndex, sName, "scene-edit-secondary", "Ctrl+RMB" }; + Settings::SettingValue mSceneSelectPrimary{ mIndex, sName, "scene-select-primary", "MMB" }; + Settings::SettingValue mSceneSelectSecondary{ mIndex, sName, "scene-select-secondary", + "Ctrl+MMB" }; + Settings::SettingValue mSceneSelectTertiary{ mIndex, sName, "scene-select-tertiary", "Shift+MMB" }; + Settings::SettingValue mSceneSpeedModifier{ mIndex, sName, "scene-speed-modifier", "Shift" }; + Settings::SettingValue mSceneDelete{ mIndex, sName, "scene-delete", "Delete" }; + Settings::SettingValue mSceneInstanceDropTerrain{ mIndex, sName, "scene-instance-drop-terrain", + "G" }; + Settings::SettingValue mSceneInstanceDropCollision{ mIndex, sName, "scene-instance-drop-collision", + "H" }; + Settings::SettingValue mSceneInstanceDropTerrainSeparately{ mIndex, sName, + "scene-instance-drop-terrain-separately", "" }; + Settings::SettingValue mSceneInstanceDropCollisionSeparately{ mIndex, sName, + "scene-instance-drop-collision-separately", "" }; + Settings::SettingValue mSceneDuplicate{ mIndex, sName, "scene-duplicate", "Shift+C" }; + Settings::SettingValue mSceneLoadCamCell{ mIndex, sName, "scene-load-cam-cell", "Keypad+5" }; + Settings::SettingValue mSceneLoadCamEastcell{ mIndex, sName, "scene-load-cam-eastcell", + "Keypad+6" }; + Settings::SettingValue mSceneLoadCamNorthcell{ mIndex, sName, "scene-load-cam-northcell", + "Keypad+8" }; + Settings::SettingValue mSceneLoadCamWestcell{ mIndex, sName, "scene-load-cam-westcell", + "Keypad+4" }; + Settings::SettingValue mSceneLoadCamSouthcell{ mIndex, sName, "scene-load-cam-southcell", + "Keypad+2" }; + Settings::SettingValue mSceneEditAbort{ mIndex, sName, "scene-edit-abort", "Escape" }; + Settings::SettingValue mSceneFocusToolbar{ mIndex, sName, "scene-focus-toolbar", "T" }; + Settings::SettingValue mSceneRenderStats{ mIndex, sName, "scene-render-stats", "F3" }; + Settings::SettingValue mSceneClearSelection{ mIndex, sName, "scene-clear-selection", "Space" }; + Settings::SettingValue mSceneUnhideAll{ mIndex, sName, "scene-unhide-all", "Alt+H" }; + Settings::SettingValue mSceneToggleVisibility{ mIndex, sName, "scene-toggle-visibility", "H" }; + Settings::SettingValue mSceneGroup0{ mIndex, sName, "scene-group-0", "0" }; + Settings::SettingValue mSceneSave0{ mIndex, sName, "scene-save-0", "Ctrl+0" }; + Settings::SettingValue mSceneGroup1{ mIndex, sName, "scene-group-1", "1" }; + Settings::SettingValue mSceneSave1{ mIndex, sName, "scene-save-1", "Ctrl+1" }; + Settings::SettingValue mSceneGroup2{ mIndex, sName, "scene-group-2", "2" }; + Settings::SettingValue mSceneSave2{ mIndex, sName, "scene-save-2", "Ctrl+2" }; + Settings::SettingValue mSceneGroup3{ mIndex, sName, "scene-group-3", "3" }; + Settings::SettingValue mSceneSave3{ mIndex, sName, "scene-save-3", "Ctrl+3" }; + Settings::SettingValue mSceneGroup4{ mIndex, sName, "scene-group-4", "4" }; + Settings::SettingValue mSceneSave4{ mIndex, sName, "scene-save-4", "Ctrl+4" }; + Settings::SettingValue mSceneGroup5{ mIndex, sName, "scene-group-5", "5" }; + Settings::SettingValue mSceneSave5{ mIndex, sName, "scene-save-5", "Ctrl+5" }; + Settings::SettingValue mSceneGroup6{ mIndex, sName, "scene-group-6", "6" }; + Settings::SettingValue mSceneSave6{ mIndex, sName, "scene-save-6", "Ctrl+6" }; + Settings::SettingValue mSceneGroup7{ mIndex, sName, "scene-group-7", "7" }; + Settings::SettingValue mSceneSave7{ mIndex, sName, "scene-save-7", "Ctrl+7" }; + Settings::SettingValue mSceneGroup8{ mIndex, sName, "scene-group-8", "8" }; + Settings::SettingValue mSceneSave8{ mIndex, sName, "scene-save-8", "Ctrl+8" }; + Settings::SettingValue mSceneGroup9{ mIndex, sName, "scene-group-9", "9" }; + Settings::SettingValue mSceneSave9{ mIndex, sName, "scene-save-9", "Ctrl+9" }; + Settings::SettingValue mFreeForward{ mIndex, sName, "free-forward", "W" }; + Settings::SettingValue mFreeBackward{ mIndex, sName, "free-backward", "S" }; + Settings::SettingValue mFreeLeft{ mIndex, sName, "free-left", "A" }; + Settings::SettingValue mFreeRight{ mIndex, sName, "free-right", "D" }; + Settings::SettingValue mFreeRollLeft{ mIndex, sName, "free-roll-left", "Q" }; + Settings::SettingValue mFreeRollRight{ mIndex, sName, "free-roll-right", "E" }; + Settings::SettingValue mFreeSpeedMode{ mIndex, sName, "free-speed-mode", "F" }; + Settings::SettingValue mOrbitUp{ mIndex, sName, "orbit-up", "W" }; + Settings::SettingValue mOrbitDown{ mIndex, sName, "orbit-down", "S" }; + Settings::SettingValue mOrbitLeft{ mIndex, sName, "orbit-left", "A" }; + Settings::SettingValue mOrbitRight{ mIndex, sName, "orbit-right", "D" }; + Settings::SettingValue mOrbitRollLeft{ mIndex, sName, "orbit-roll-left", "Q" }; + Settings::SettingValue mOrbitRollRight{ mIndex, sName, "orbit-roll-right", "E" }; + Settings::SettingValue mOrbitSpeedMode{ mIndex, sName, "orbit-speed-mode", "F" }; + Settings::SettingValue mOrbitCenterSelection{ mIndex, sName, "orbit-center-selection", "C" }; + Settings::SettingValue mScriptEditorComment{ mIndex, sName, "script-editor-comment", "" }; + Settings::SettingValue mScriptEditorUncomment{ mIndex, sName, "script-editor-uncomment", "" }; + }; + + struct ModelsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Models"; + + Settings::SettingValue mBaseanim{ mIndex, sName, "baseanim", "meshes/base_anim.nif" }; + Settings::SettingValue mBaseanimkna{ mIndex, sName, "baseanimkna", "meshes/base_animkna.nif" }; + Settings::SettingValue mBaseanimfemale{ mIndex, sName, "baseanimfemale", + "meshes/base_anim_female.nif" }; + Settings::SettingValue mWolfskin{ mIndex, sName, "wolfskin", "meshes/wolf/skin.nif" }; + }; + + struct Values : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + WindowsCategory mWindows{ mIndex }; + RecordsCategory mRecords{ mIndex }; + IdTablesCategory mIdTables{ mIndex }; + IdDialoguesCategory mIdDialogues{ mIndex }; + ReportsCategory mReports{ mIndex }; + SearchAndReplaceCategory mSearchAndReplace{ mIndex }; + ScriptsCategory mScripts{ mIndex }; + GeneralInputCategory mGeneralInput{ mIndex }; + SceneInputCategory mSceneInput{ mIndex }; + RenderingCategory mRendering{ mIndex }; + TooltipsCategory mTooltips{ mIndex }; + SceneEditingCategory mSceneEditing{ mIndex }; + KeyBindingsCategory mKeyBindings{ mIndex }; + ModelsCategory mModels{ mIndex }; + }; +} + +#endif diff --git a/apps/opencs/model/tools/racecheck.cpp b/apps/opencs/model/tools/racecheck.cpp index 78f72f44c5..8f0df823c3 100644 --- a/apps/opencs/model/tools/racecheck.cpp +++ b/apps/opencs/model/tools/racecheck.cpp @@ -41,17 +41,17 @@ void CSMTools::RaceCheckStage::performPerRecord(int stage, CSMDoc::Messages& mes messages.add(id, "Description is missing", "", CSMDoc::Message::Severity_Warning); // test for positive height - if (race.mData.mHeight.mMale <= 0) + if (race.mData.mMaleHeight <= 0) messages.add(id, "Male height is non-positive", "", CSMDoc::Message::Severity_Error); - if (race.mData.mHeight.mFemale <= 0) + if (race.mData.mFemaleHeight <= 0) messages.add(id, "Female height is non-positive", "", CSMDoc::Message::Severity_Error); // test for non-negative weight - if (race.mData.mWeight.mMale < 0) + if (race.mData.mMaleWeight < 0) messages.add(id, "Male weight is negative", "", CSMDoc::Message::Severity_Error); - if (race.mData.mWeight.mFemale < 0) + if (race.mData.mFemaleWeight < 0) messages.add(id, "Female weight is negative", "", CSMDoc::Message::Severity_Error); /// \todo check data members that can't be edited in the table view diff --git a/apps/opencs/model/tools/referenceablecheck.cpp b/apps/opencs/model/tools/referenceablecheck.cpp index e2e178f90a..d25568fd0a 100644 --- a/apps/opencs/model/tools/referenceablecheck.cpp +++ b/apps/opencs/model/tools/referenceablecheck.cpp @@ -693,22 +693,12 @@ void CSMTools::ReferenceableCheckStage::npcCheck( } else if (npc.mNpdt.mHealth != 0) { - if (npc.mNpdt.mStrength == 0) - messages.add(id, "Strength is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mIntelligence == 0) - messages.add(id, "Intelligence is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mWillpower == 0) - messages.add(id, "Willpower is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mAgility == 0) - messages.add(id, "Agility is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mSpeed == 0) - messages.add(id, "Speed is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mEndurance == 0) - messages.add(id, "Endurance is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mPersonality == 0) - messages.add(id, "Personality is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mLuck == 0) - messages.add(id, "Luck is equal to zero", "", CSMDoc::Message::Severity_Warning); + for (size_t i = 0; i < npc.mNpdt.mAttributes.size(); ++i) + { + if (npc.mNpdt.mAttributes[i] == 0) + messages.add(id, ESM::Attribute::indexToRefId(i).getRefIdString() + " is equal to zero", {}, + CSMDoc::Message::Severity_Warning); + } } if (level <= 0) diff --git a/apps/opencs/model/tools/referencecheck.cpp b/apps/opencs/model/tools/referencecheck.cpp index 87d133a59e..511458b946 100644 --- a/apps/opencs/model/tools/referencecheck.cpp +++ b/apps/opencs/model/tools/referencecheck.cpp @@ -98,9 +98,8 @@ void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages& message if (cellRef.mEnchantmentCharge < -1) messages.add(id, "Negative number of enchantment points", "", CSMDoc::Message::Severity_Error); - // Check if gold value isn't negative - if (cellRef.mGoldValue < 0) - messages.add(id, "Negative gold value", "", CSMDoc::Message::Severity_Error); + if (cellRef.mCount < 1) + messages.add(id, "Reference without count", {}, CSMDoc::Message::Severity_Error); } int CSMTools::ReferenceCheckStage::setup() diff --git a/apps/opencs/model/world/actoradapter.cpp b/apps/opencs/model/world/actoradapter.cpp index e68d3e833f..0e3725bbb7 100644 --- a/apps/opencs/model/world/actoradapter.cpp +++ b/apps/opencs/model/world/actoradapter.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -132,11 +133,11 @@ namespace CSMWorld bool beast = mRaceData ? mRaceData->isBeast() : false; if (beast) - return Settings::Manager::getString("baseanimkna", "Models"); + return CSMPrefs::get()["Models"]["baseanimkna"].toString(); else if (mFemale) - return Settings::Manager::getString("baseanimfemale", "Models"); + return CSMPrefs::get()["Models"]["baseanimfemale"].toString(); else - return Settings::Manager::getString("baseanim", "Models"); + return CSMPrefs::get()["Models"]["baseanim"].toString(); } ESM::RefId ActorAdapter::ActorData::getPart(ESM::PartReferenceType index) const diff --git a/apps/opencs/model/world/columnimp.cpp b/apps/opencs/model/world/columnimp.cpp index 981ec5278d..215e4c3dfc 100644 --- a/apps/opencs/model/world/columnimp.cpp +++ b/apps/opencs/model/world/columnimp.cpp @@ -333,6 +333,37 @@ namespace CSMWorld return true; } + SelectionGroupColumn::SelectionGroupColumn() + : Column(Columns::ColumnId_SelectionGroupObjects, ColumnBase::Display_None) + { + } + + QVariant SelectionGroupColumn::get(const Record& record) const + { + QVariant data; + QStringList selectionInfo; + const std::vector& instances = record.get().selectedInstances; + + for (const std::string& instance : instances) + selectionInfo << QString::fromStdString(instance); + data.setValue(selectionInfo); + + return data; + } + + void SelectionGroupColumn::set(Record& record, const QVariant& data) + { + ESM::SelectionGroup record2 = record.get(); + for (const auto& item : data.toStringList()) + record2.selectedInstances.push_back(item.toStdString()); + record.setModified(record2); + } + + bool SelectionGroupColumn::isEditable() const + { + return false; + } + std::optional getSkillIndex(std::string_view value) { int index = ESM::Skill::refIdToIndex(ESM::RefId::stringRefId(value)); diff --git a/apps/opencs/model/world/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index 5e5ff83fcf..eba09bd8b1 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -570,19 +571,34 @@ namespace CSMWorld QVariant get(const Record& record) const override { - const ESM::Race::MaleFemaleF& value = mWeight ? record.get().mData.mWeight : record.get().mData.mHeight; - - return mMale ? value.mMale : value.mFemale; + if (mWeight) + { + if (mMale) + return record.get().mData.mMaleWeight; + return record.get().mData.mFemaleWeight; + } + if (mMale) + return record.get().mData.mMaleHeight; + return record.get().mData.mFemaleHeight; } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - - ESM::Race::MaleFemaleF& value = mWeight ? record2.mData.mWeight : record2.mData.mHeight; - - (mMale ? value.mMale : value.mFemale) = data.toFloat(); - + if (mWeight) + { + if (mMale) + record2.mData.mMaleWeight = data.toFloat(); + else + record2.mData.mFemaleWeight = data.toFloat(); + } + else + { + if (mMale) + record2.mData.mMaleHeight = data.toFloat(); + else + record2.mData.mFemaleHeight = data.toFloat(); + } record.setModified(record2); } @@ -1095,19 +1111,19 @@ namespace CSMWorld }; template - struct GoldValueColumn : public Column + struct StackSizeColumn : public Column { - GoldValueColumn() - : Column(Columns::ColumnId_CoinValue, ColumnBase::Display_Integer) + StackSizeColumn() + : Column(Columns::ColumnId_StackCount, ColumnBase::Display_Integer) { } - QVariant get(const Record& record) const override { return record.get().mGoldValue; } + QVariant get(const Record& record) const override { return record.get().mCount; } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mGoldValue = data.toInt(); + record2.mCount = data.toInt(); record.setModified(record2); } @@ -2376,6 +2392,17 @@ namespace CSMWorld void set(Record& record, const QVariant& data) override; bool isEditable() const override; }; + + struct SelectionGroupColumn : public Column + { + SelectionGroupColumn(); + + QVariant get(const Record& record) const override; + + void set(Record& record, const QVariant& data) override; + + bool isEditable() const override; + }; } // This is required to access the type as a QVariant. diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index 4a476b52f3..45759cd234 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -56,7 +56,7 @@ namespace CSMWorld { ColumnId_FactionIndex, "Faction Index" }, { ColumnId_Charges, "Charges" }, { ColumnId_Enchantment, "Enchantment" }, - { ColumnId_CoinValue, "Coin Value" }, + { ColumnId_StackCount, "Count" }, { ColumnId_Teleport, "Teleport" }, { ColumnId_TeleportCell, "Teleport Cell" }, { ColumnId_LockLevel, "Lock Level" }, diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index 92f41a2f20..74e5bdd006 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -45,7 +45,7 @@ namespace CSMWorld ColumnId_FactionIndex = 31, ColumnId_Charges = 32, ColumnId_Enchantment = 33, - ColumnId_CoinValue = 34, + ColumnId_StackCount = 34, ColumnId_Teleport = 35, ColumnId_TeleportCell = 36, ColumnId_LockLevel = 37, @@ -347,6 +347,8 @@ namespace CSMWorld ColumnId_LevelledCreatureId = 315, + ColumnId_SelectionGroupObjects = 316, + // Allocated to a separate value range, so we don't get a collision should we ever need // to extend the number of use values. ColumnId_UseValue1 = 0x10000, diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index e8fd138ab4..6322a77e66 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -587,7 +587,7 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data mRefs.addColumn(new FactionIndexColumn); mRefs.addColumn(new ChargesColumn); mRefs.addColumn(new EnchantmentChargesColumn); - mRefs.addColumn(new GoldValueColumn); + mRefs.addColumn(new StackSizeColumn); mRefs.addColumn(new TeleportColumn); mRefs.addColumn(new TeleportCellColumn); mRefs.addColumn(new PosColumn(&CellRef::mDoorDest, 0, true)); @@ -620,6 +620,11 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data mDebugProfiles.addColumn(new DescriptionColumn); mDebugProfiles.addColumn(new ScriptColumn(ScriptColumn::Type_Lines)); + mSelectionGroups.addColumn(new StringIdColumn); + mSelectionGroups.addColumn(new RecordStateColumn); + mSelectionGroups.addColumn(new FixedRecordTypeColumn(UniversalId::Type_SelectionGroup)); + mSelectionGroups.addColumn(new SelectionGroupColumn); + mMetaData.appendBlankRecord(ESM::RefId::stringRefId("sys::meta")); mMetaData.addColumn(new StringIdColumn(true)); @@ -664,6 +669,7 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data addModel(new ResourceTable(&mResourcesManager.get(UniversalId::Type_Textures)), UniversalId::Type_Texture); addModel(new ResourceTable(&mResourcesManager.get(UniversalId::Type_Videos)), UniversalId::Type_Video); addModel(new IdTable(&mMetaData), UniversalId::Type_MetaData); + addModel(new IdTable(&mSelectionGroups), UniversalId::Type_SelectionGroup); mActorAdapter = std::make_unique(*this); @@ -908,6 +914,16 @@ CSMWorld::IdCollection& CSMWorld::Data::getDebugProfiles() return mDebugProfiles; } +CSMWorld::IdCollection& CSMWorld::Data::getSelectionGroups() +{ + return mSelectionGroups; +} + +const CSMWorld::IdCollection& CSMWorld::Data::getSelectionGroups() const +{ + return mSelectionGroups; +} + const CSMWorld::IdCollection& CSMWorld::Data::getLand() const { return mLand; @@ -1369,6 +1385,17 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages) mDebugProfiles.load(*mReader, mBase); break; + case ESM::REC_SELG: + + if (!mProject) + { + unhandledRecord = true; + break; + } + + mSelectionGroups.load(*mReader, mBase); + break; + default: unhandledRecord = true; diff --git a/apps/opencs/model/world/data.hpp b/apps/opencs/model/world/data.hpp index 1b63986eac..8e01452f3b 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -105,6 +106,7 @@ namespace CSMWorld IdCollection mBodyParts; IdCollection mMagicEffects; IdCollection mDebugProfiles; + IdCollection mSelectionGroups; IdCollection mSoundGens; IdCollection mStartScripts; NestedInfoCollection mTopicInfos; @@ -251,6 +253,10 @@ namespace CSMWorld IdCollection& getDebugProfiles(); + const IdCollection& getSelectionGroups() const; + + IdCollection& getSelectionGroups(); + const IdCollection& getLand() const; IdCollection& getLand(); diff --git a/apps/opencs/model/world/nestedcoladapterimp.cpp b/apps/opencs/model/world/nestedcoladapterimp.cpp index b96cf46465..13ae821a77 100644 --- a/apps/opencs/model/world/nestedcoladapterimp.cpp +++ b/apps/opencs/model/world/nestedcoladapterimp.cpp @@ -741,8 +741,8 @@ namespace CSMWorld QVariant RaceAttributeAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); - - if (subRowIndex < 0 || subRowIndex >= ESM::Attribute::Length) + ESM::RefId attribute = ESM::Attribute::indexToRefId(subRowIndex); + if (attribute.empty()) throw std::runtime_error("index out of range"); switch (subColIndex) @@ -750,9 +750,9 @@ namespace CSMWorld case 0: return subRowIndex; case 1: - return race.mData.mAttributeValues[subRowIndex].mMale; + return race.mData.getAttribute(attribute, true); case 2: - return race.mData.mAttributeValues[subRowIndex].mFemale; + return race.mData.getAttribute(attribute, false); default: throw std::runtime_error("Race Attribute subcolumn index out of range"); } @@ -762,8 +762,8 @@ namespace CSMWorld Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); - - if (subRowIndex < 0 || subRowIndex >= ESM::Attribute::Length) + ESM::RefId attribute = ESM::Attribute::indexToRefId(subRowIndex); + if (attribute.empty()) throw std::runtime_error("index out of range"); switch (subColIndex) @@ -771,10 +771,10 @@ namespace CSMWorld case 0: return; // throw an exception here? case 1: - race.mData.mAttributeValues[subRowIndex].mMale = value.toInt(); + race.mData.setAttribute(attribute, true, value.toInt()); break; case 2: - race.mData.mAttributeValues[subRowIndex].mFemale = value.toInt(); + race.mData.setAttribute(attribute, false, value.toInt()); break; default: throw std::runtime_error("Race Attribute subcolumn index out of range"); diff --git a/apps/opencs/model/world/refcollection.cpp b/apps/opencs/model/world/refcollection.cpp index 1afa9027a9..642edcfb64 100644 --- a/apps/opencs/model/world/refcollection.cpp +++ b/apps/opencs/model/world/refcollection.cpp @@ -297,6 +297,7 @@ void CSMWorld::RefCollection::cloneRecord( const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) { auto copy = std::make_unique>(); + int index = getAppendIndex(ESM::RefId(), type); copy->mModified = getRecord(origin).get(); copy->mState = RecordBase::State_ModifiedOnly; @@ -304,6 +305,15 @@ void CSMWorld::RefCollection::cloneRecord( copy->get().mId = destination; copy->get().mIdNum = extractIdNum(destination.getRefIdString()); + if (copy->get().mRefNum.mContentFile != 0) + { + mRefIndex.insert(std::make_pair(static_cast*>(copy.get())->get().mIdNum, index)); + copy->get().mRefNum.mContentFile = 0; + copy->get().mRefNum.mIndex = index; + } + else + copy->get().mRefNum.mIndex = copy->get().mIdNum; + insertRecord(std::move(copy), getAppendIndex(destination, type)); // call RefCollection::insertRecord() } diff --git a/apps/opencs/model/world/refidadapterimp.cpp b/apps/opencs/model/world/refidadapterimp.cpp index 0ddfbbb051..c6179facb8 100644 --- a/apps/opencs/model/world/refidadapterimp.cpp +++ b/apps/opencs/model/world/refidadapterimp.cpp @@ -938,30 +938,9 @@ QVariant CSMWorld::NpcAttributesRefIdAdapter::getNestedData( if (subColIndex == 0) return subRowIndex; - else if (subColIndex == 1) - switch (subRowIndex) - { - case 0: - return static_cast(npcStruct.mStrength); - case 1: - return static_cast(npcStruct.mIntelligence); - case 2: - return static_cast(npcStruct.mWillpower); - case 3: - return static_cast(npcStruct.mAgility); - case 4: - return static_cast(npcStruct.mSpeed); - case 5: - return static_cast(npcStruct.mEndurance); - case 6: - return static_cast(npcStruct.mPersonality); - case 7: - return static_cast(npcStruct.mLuck); - default: - return QVariant(); // throw an exception here? - } - else - return QVariant(); // throw an exception here? + else if (subColIndex == 1 && subRowIndex >= 0 && subRowIndex < ESM::Attribute::Length) + return static_cast(npcStruct.mAttributes[subRowIndex]); + return QVariant(); // throw an exception here? } void CSMWorld::NpcAttributesRefIdAdapter::setNestedData( @@ -972,36 +951,8 @@ void CSMWorld::NpcAttributesRefIdAdapter::setNestedData( ESM::NPC npc = record.get(); ESM::NPC::NPDTstruct52& npcStruct = npc.mNpdt; - if (subColIndex == 1) - switch (subRowIndex) - { - case 0: - npcStruct.mStrength = static_cast(value.toInt()); - break; - case 1: - npcStruct.mIntelligence = static_cast(value.toInt()); - break; - case 2: - npcStruct.mWillpower = static_cast(value.toInt()); - break; - case 3: - npcStruct.mAgility = static_cast(value.toInt()); - break; - case 4: - npcStruct.mSpeed = static_cast(value.toInt()); - break; - case 5: - npcStruct.mEndurance = static_cast(value.toInt()); - break; - case 6: - npcStruct.mPersonality = static_cast(value.toInt()); - break; - case 7: - npcStruct.mLuck = static_cast(value.toInt()); - break; - default: - return; // throw an exception here? - } + if (subColIndex == 1 && subRowIndex >= 0 && subRowIndex < ESM::Attribute::Length) + npcStruct.mAttributes[subRowIndex] = static_cast(value.toInt()); else return; // throw an exception here? diff --git a/apps/opencs/model/world/refidcollection.cpp b/apps/opencs/model/world/refidcollection.cpp index b8c3974d75..694f67e445 100644 --- a/apps/opencs/model/world/refidcollection.cpp +++ b/apps/opencs/model/world/refidcollection.cpp @@ -97,7 +97,7 @@ CSMWorld::RefIdCollection::RefIdCollection() inventoryColumns.mIcon = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Weight, ColumnBase::Display_Float); inventoryColumns.mWeight = &mColumns.back(); - mColumns.emplace_back(Columns::ColumnId_CoinValue, ColumnBase::Display_Integer); + mColumns.emplace_back(Columns::ColumnId_StackCount, ColumnBase::Display_Integer); inventoryColumns.mValue = &mColumns.back(); IngredientColumns ingredientColumns(inventoryColumns); diff --git a/apps/opencs/model/world/universalid.cpp b/apps/opencs/model/world/universalid.cpp index dec533b015..9daf87e20a 100644 --- a/apps/opencs/model/world/universalid.cpp +++ b/apps/opencs/model/world/universalid.cpp @@ -68,6 +68,7 @@ namespace ":./resources-video" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_DebugProfiles, "Debug Profiles", ":./debug-profile.png" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_SelectionGroup, "Selection Groups", "" }, { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_RunLog, "Run Log", ":./run-log.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_SoundGens, "Sound Generators", ":./sound-generator.png" }, @@ -187,6 +188,8 @@ namespace { mStream << ": " << value; } + + void operator()(const ESM::RefId& value) const { mStream << ": " << value.toString(); } }; struct GetTypeData @@ -218,6 +221,23 @@ namespace return std::to_string(value); } + + CSMWorld::UniversalId::Class getClassByType(CSMWorld::UniversalId::Type type) + { + if (const auto it + = std::find_if(std::begin(sIdArg), std::end(sIdArg), [&](const TypeData& v) { return v.mType == type; }); + it != std::end(sIdArg)) + return it->mClass; + if (const auto it = std::find_if( + std::begin(sIndexArg), std::end(sIndexArg), [&](const TypeData& v) { return v.mType == type; }); + it != std::end(sIndexArg)) + return it->mClass; + if (const auto it + = std::find_if(std::begin(sNoArg), std::end(sNoArg), [&](const TypeData& v) { return v.mType == type; }); + it != std::end(sNoArg)) + return it->mClass; + throw std::logic_error("invalid UniversalId type: " + std::to_string(type)); + } } CSMWorld::UniversalId::UniversalId(const std::string& universalId) @@ -326,6 +346,13 @@ CSMWorld::UniversalId::UniversalId(Type type, ESM::RefId id) throw std::logic_error("invalid RefId argument UniversalId type: " + std::to_string(type)); } +CSMWorld::UniversalId::UniversalId(Type type, const UniversalId& id) + : mClass(getClassByType(type)) + , mType(type) + , mValue(id.mValue) +{ +} + CSMWorld::UniversalId::UniversalId(Type type, int index) : mType(type) , mValue(index) @@ -360,6 +387,10 @@ const std::string& CSMWorld::UniversalId::getId() const if (const std::string* result = std::get_if(&mValue)) return *result; + if (const ESM::RefId* refId = std::get_if(&mValue)) + if (const ESM::StringRefId* result = refId->getIf()) + return result->getValue(); + throw std::logic_error("invalid access to ID of " + ::toString(getArgumentType()) + " UniversalId"); } diff --git a/apps/opencs/model/world/universalid.hpp b/apps/opencs/model/world/universalid.hpp index 2d3385bcb4..34ef480fa5 100644 --- a/apps/opencs/model/world/universalid.hpp +++ b/apps/opencs/model/world/universalid.hpp @@ -133,6 +133,7 @@ namespace CSMWorld Type_LandTexture, Type_Pathgrids, Type_Pathgrid, + Type_SelectionGroup, Type_StartScripts, Type_StartScript, Type_Search, @@ -158,6 +159,8 @@ namespace CSMWorld UniversalId(Type type, int index); ///< Using a type for a non-index-argument UniversalId will throw an exception. + UniversalId(Type type, const UniversalId& id); + Class getClass() const; ArgumentType getArgumentType() const; diff --git a/files/ui/filedialog.ui b/apps/opencs/ui/filedialog.ui similarity index 100% rename from files/ui/filedialog.ui rename to apps/opencs/ui/filedialog.ui diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index 4f4e687d8f..e1bf7e6ac6 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -629,7 +629,7 @@ void CSVDoc::View::addSubView(const CSMWorld::UniversalId& id, const std::string if (isReferenceable) { view = mSubViewFactory.makeSubView( - CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, id.getId()), *mDocument); + CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, id), *mDocument); } else { diff --git a/apps/opencs/view/prefs/keybindingpage.cpp b/apps/opencs/view/prefs/keybindingpage.cpp index d3cc1ff889..f292fa4cf5 100644 --- a/apps/opencs/view/prefs/keybindingpage.cpp +++ b/apps/opencs/view/prefs/keybindingpage.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -61,46 +62,35 @@ namespace CSVPrefs void KeyBindingPage::addSetting(CSMPrefs::Setting* setting) { - std::pair widgets = setting->makeWidgets(this); + const CSMPrefs::SettingWidgets widgets = setting->makeWidgets(this); - if (widgets.first) + if (widgets.mLabel != nullptr && widgets.mInput != nullptr) { // Label, Option widgets assert(mPageLayout); int next = mPageLayout->rowCount(); - mPageLayout->addWidget(widgets.first, next, 0); - mPageLayout->addWidget(widgets.second, next, 1); + mPageLayout->addWidget(widgets.mLabel, next, 0); + mPageLayout->addWidget(widgets.mInput, next, 1); } - else if (widgets.second) + else if (widgets.mInput != nullptr) { // Wide single widget assert(mPageLayout); int next = mPageLayout->rowCount(); - mPageLayout->addWidget(widgets.second, next, 0, 1, 2); + mPageLayout->addWidget(widgets.mInput, next, 0, 1, 2); } else { - if (setting->getLabel().empty()) - { - // Insert empty space - assert(mPageLayout); + // Create new page + QWidget* pageWidget = new QWidget(); + mPageLayout = new QGridLayout(pageWidget); + mPageLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); - int next = mPageLayout->rowCount(); - mPageLayout->addWidget(new QWidget(), next, 0); - } - else - { - // Create new page - QWidget* pageWidget = new QWidget(); - mPageLayout = new QGridLayout(pageWidget); - mPageLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); + mStackedLayout->addWidget(pageWidget); - mStackedLayout->addWidget(pageWidget); - - mPageSelector->addItem(QString::fromUtf8(setting->getLabel().c_str())); - } + mPageSelector->addItem(setting->getLabel()); } } diff --git a/apps/opencs/view/prefs/page.cpp b/apps/opencs/view/prefs/page.cpp index 4f04a39f00..cc74122782 100644 --- a/apps/opencs/view/prefs/page.cpp +++ b/apps/opencs/view/prefs/page.cpp @@ -6,6 +6,7 @@ #include #include +#include #include "../../model/prefs/category.hpp" #include "../../model/prefs/setting.hpp" @@ -24,21 +25,17 @@ CSVPrefs::Page::Page(CSMPrefs::Category& category, QWidget* parent) void CSVPrefs::Page::addSetting(CSMPrefs::Setting* setting) { - std::pair widgets = setting->makeWidgets(this); + const CSMPrefs::SettingWidgets widgets = setting->makeWidgets(this); int next = mGrid->rowCount(); - if (widgets.first) + if (widgets.mLabel != nullptr && widgets.mInput != nullptr) { - mGrid->addWidget(widgets.first, next, 0); - mGrid->addWidget(widgets.second, next, 1); + mGrid->addWidget(widgets.mLabel, next, 0); + mGrid->addWidget(widgets.mInput, next, 1); } - else if (widgets.second) + else if (widgets.mInput != nullptr) { - mGrid->addWidget(widgets.second, next, 0, 1, 2); - } - else - { - mGrid->addWidget(new QWidget(this), next, 0); + mGrid->addWidget(widgets.mInput, next, 0, 1, 2); } } diff --git a/apps/opencs/view/render/cell.cpp b/apps/opencs/view/render/cell.cpp index c74782f2d1..f2c851cc72 100644 --- a/apps/opencs/view/render/cell.cpp +++ b/apps/opencs/view/render/cell.cpp @@ -612,6 +612,30 @@ osg::ref_ptr CSVRender::Cell::getSnapTarget(unsigned int ele return result; } +void CSVRender::Cell::selectFromGroup(const std::vector& group) +{ + for (const auto& [_, object] : mObjects) + { + for (const auto& objectName : group) + { + if (objectName == object->getReferenceId()) + { + object->setSelected(true, osg::Vec4f(1, 0, 1, 1)); + } + } + } +} + +void CSVRender::Cell::unhideAll() +{ + for (const auto& [_, object] : mObjects) + { + osg::ref_ptr rootNode = object->getRootNode(); + if (rootNode->getNodeMask() == Mask_Hidden) + rootNode->setNodeMask(Mask_Reference); + } +} + std::vector> CSVRender::Cell::getSelection(unsigned int elementMask) const { std::vector> result; diff --git a/apps/opencs/view/render/cell.hpp b/apps/opencs/view/render/cell.hpp index cf50604c29..5bfce47904 100644 --- a/apps/opencs/view/render/cell.hpp +++ b/apps/opencs/view/render/cell.hpp @@ -148,6 +148,10 @@ namespace CSVRender // already selected void selectAllWithSameParentId(int elementMask); + void selectFromGroup(const std::vector& group); + + void unhideAll(); + void handleSelectDrag(Object* object, DragMode dragMode); void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode); diff --git a/apps/opencs/view/render/instancemode.cpp b/apps/opencs/view/render/instancemode.cpp index df5bb02332..2d79c912f6 100644 --- a/apps/opencs/view/render/instancemode.cpp +++ b/apps/opencs/view/render/instancemode.cpp @@ -186,6 +186,71 @@ osg::Vec3f CSVRender::InstanceMode::getMousePlaneCoords(const QPoint& point, con return mousePlanePoint; } +void CSVRender::InstanceMode::saveSelectionGroup(const int group) +{ + QStringList strings; + QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); + QVariant selectionObjects; + CSMWorld::CommandMacro macro(undoStack, "Replace Selection Group"); + std::string groupName = "project::" + std::to_string(group); + + const auto& selection = getWorldspaceWidget().getSelection(Mask_Reference); + const int selectionObjectsIndex + = mSelectionGroups->findColumnIndex(CSMWorld::Columns::ColumnId_SelectionGroupObjects); + + if (dynamic_cast(&getWorldspaceWidget())) + groupName += "-ext"; + else + groupName += "-" + getWorldspaceWidget().getCellId(osg::Vec3f(0, 0, 0)); + + CSMWorld::CreateCommand* newGroup = new CSMWorld::CreateCommand(*mSelectionGroups, groupName); + + newGroup->setType(CSMWorld::UniversalId::Type_SelectionGroup); + + for (const auto& object : selection) + if (const CSVRender::ObjectTag* objectTag = dynamic_cast(object.get())) + strings << QString::fromStdString(objectTag->mObject->getReferenceId()); + + selectionObjects.setValue(strings); + + newGroup->addValue(selectionObjectsIndex, selectionObjects); + + if (mSelectionGroups->getModelIndex(groupName, 0).row() != -1) + macro.push(new CSMWorld::DeleteCommand(*mSelectionGroups, groupName)); + + macro.push(newGroup); + + getWorldspaceWidget().clearSelection(Mask_Reference); +} + +void CSVRender::InstanceMode::getSelectionGroup(const int group) +{ + std::string groupName = "project::" + std::to_string(group); + std::vector targets; + + const auto& selection = getWorldspaceWidget().getSelection(Mask_Reference); + const int selectionObjectsIndex + = mSelectionGroups->findColumnIndex(CSMWorld::Columns::ColumnId_SelectionGroupObjects); + + if (dynamic_cast(&getWorldspaceWidget())) + groupName += "-ext"; + else + groupName += "-" + getWorldspaceWidget().getCellId(osg::Vec3f(0, 0, 0)); + + const QModelIndex groupSearch = mSelectionGroups->getModelIndex(groupName, selectionObjectsIndex); + + if (groupSearch.row() == -1) + return; + + for (const QString& target : groupSearch.data().toStringList()) + targets.push_back(target.toStdString()); + + if (!selection.empty()) + getWorldspaceWidget().clearSelection(Mask_Reference); + + getWorldspaceWidget().selectGroup(targets); +} + CSVRender::InstanceMode::InstanceMode( WorldspaceWidget* worldspaceWidget, osg::ref_ptr parentNode, QWidget* parent) : EditMode(worldspaceWidget, QIcon(":scenetoolbar/editing-instance"), Mask_Reference | Mask_Terrain, @@ -199,18 +264,27 @@ CSVRender::InstanceMode::InstanceMode( , mUnitScaleDist(1) , mParentNode(std::move(parentNode)) { + mSelectionGroups = dynamic_cast( + worldspaceWidget->getDocument().getData().getTableModel(CSMWorld::UniversalId::Type_SelectionGroup)); + connect(this, &InstanceMode::requestFocus, worldspaceWidget, &WorldspaceWidget::requestFocus); CSMPrefs::Shortcut* deleteShortcut = new CSMPrefs::Shortcut("scene-delete", worldspaceWidget); + connect(deleteShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &InstanceMode::deleteSelectedInstances); + + CSMPrefs::Shortcut* duplicateShortcut = new CSMPrefs::Shortcut("scene-duplicate", worldspaceWidget); + connect( - deleteShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &InstanceMode::deleteSelectedInstances); + duplicateShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &InstanceMode::cloneSelectedInstances); // Following classes could be simplified by using QSignalMapper, which is obsolete in Qt5.10, but not in Qt4.8 and // Qt5.14 CSMPrefs::Shortcut* dropToCollisionShortcut = new CSMPrefs::Shortcut("scene-instance-drop-collision", worldspaceWidget); + connect(dropToCollisionShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &InstanceMode::dropSelectedInstancesToCollision); + CSMPrefs::Shortcut* dropToTerrainLevelShortcut = new CSMPrefs::Shortcut("scene-instance-drop-terrain", worldspaceWidget); connect(dropToTerrainLevelShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, @@ -223,6 +297,14 @@ CSVRender::InstanceMode::InstanceMode( = new CSMPrefs::Shortcut("scene-instance-drop-terrain-separately", worldspaceWidget); connect(dropToTerrainLevelShortcut2, qOverload<>(&CSMPrefs::Shortcut::activated), this, &InstanceMode::dropSelectedInstancesToTerrainSeparately); + + for (short i = 0; i <= 9; i++) + { + connect(new CSMPrefs::Shortcut("scene-group-" + std::to_string(i), worldspaceWidget), + qOverload<>(&CSMPrefs::Shortcut::activated), this, [this, i] { this->getSelectionGroup(i); }); + connect(new CSMPrefs::Shortcut("scene-save-" + std::to_string(i), worldspaceWidget), + qOverload<>(&CSMPrefs::Shortcut::activated), this, [this, i] { this->saveSelectionGroup(i); }); + } } void CSVRender::InstanceMode::activate(CSVWidget::SceneToolbar* toolbar) @@ -1068,7 +1150,7 @@ void CSVRender::InstanceMode::handleSelectDrag(const QPoint& pos) mDragMode = DragMode_None; } -void CSVRender::InstanceMode::deleteSelectedInstances(bool active) +void CSVRender::InstanceMode::deleteSelectedInstances() { std::vector> selection = getWorldspaceWidget().getSelection(Mask_Reference); if (selection.empty()) @@ -1087,6 +1169,27 @@ void CSVRender::InstanceMode::deleteSelectedInstances(bool active) getWorldspaceWidget().clearSelection(Mask_Reference); } +void CSVRender::InstanceMode::cloneSelectedInstances() +{ + std::vector> selection = getWorldspaceWidget().getSelection(Mask_Reference); + if (selection.empty()) + return; + + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& referencesTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_References)); + QUndoStack& undoStack = document.getUndoStack(); + + CSMWorld::CommandMacro macro(undoStack, "Clone Instances"); + for (osg::ref_ptr tag : selection) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) + { + macro.push(new CSMWorld::CloneCommand(referencesTable, objectTag->mObject->getReferenceId(), + "ref#" + std::to_string(referencesTable.rowCount()), CSMWorld::UniversalId::Type_Reference)); + } + // getWorldspaceWidget().clearSelection(Mask_Reference); +} + void CSVRender::InstanceMode::dropInstance(CSVRender::Object* object, float dropHeight) { object->setEdited(Object::Override_Position); diff --git a/apps/opencs/view/render/instancemode.hpp b/apps/opencs/view/render/instancemode.hpp index 5055d08d5b..9267823e22 100644 --- a/apps/opencs/view/render/instancemode.hpp +++ b/apps/opencs/view/render/instancemode.hpp @@ -14,6 +14,8 @@ #include "editmode.hpp" #include "instancedragmodes.hpp" +#include +#include class QDragEnterEvent; class QDropEvent; @@ -60,6 +62,7 @@ namespace CSVRender osg::ref_ptr mParentNode; osg::Vec3 mDragStart; std::vector mObjectsAtDragStart; + CSMWorld::IdTable* mSelectionGroups; int getSubModeFromId(const std::string& id) const; @@ -131,7 +134,10 @@ namespace CSVRender private slots: void subModeChanged(const std::string& id); - void deleteSelectedInstances(bool active); + void deleteSelectedInstances(); + void cloneSelectedInstances(); + void getSelectionGroup(const int group); + void saveSelectionGroup(const int group); void dropSelectedInstancesToCollision(); void dropSelectedInstancesToTerrain(); void dropSelectedInstancesToCollisionSeparately(); diff --git a/apps/opencs/view/render/mask.hpp b/apps/opencs/view/render/mask.hpp index 818be8b228..7f767e19ac 100644 --- a/apps/opencs/view/render/mask.hpp +++ b/apps/opencs/view/render/mask.hpp @@ -11,6 +11,7 @@ namespace CSVRender enum Mask : unsigned int { // elements that are part of the actual scene + Mask_Hidden = 0x0, Mask_Reference = 0x2, Mask_Pathgrid = 0x4, Mask_Water = 0x8, diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index 7782ce36c9..4ad1aaca15 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -33,7 +33,6 @@ #include #include #include -#include #include @@ -485,7 +484,7 @@ CSVRender::Object::~Object() mParentNode->removeChild(mRootNode); } -void CSVRender::Object::setSelected(bool selected) +void CSVRender::Object::setSelected(bool selected, const osg::Vec4f& color) { mSelected = selected; @@ -499,7 +498,7 @@ void CSVRender::Object::setSelected(bool selected) mRootNode->removeChild(mBaseNode); if (selected) { - mOutline->setWireframeColor(osg::Vec4f(1, 1, 1, 1)); + mOutline->setWireframeColor(color); mOutline->addChild(mBaseNode); mRootNode->addChild(mOutline); } diff --git a/apps/opencs/view/render/object.hpp b/apps/opencs/view/render/object.hpp index 436c410c84..5c73b12211 100644 --- a/apps/opencs/view/render/object.hpp +++ b/apps/opencs/view/render/object.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -138,7 +139,7 @@ namespace CSVRender ~Object(); /// Mark the object as selected, selected objects show an outline effect - void setSelected(bool selected); + void setSelected(bool selected, const osg::Vec4f& color = osg::Vec4f(1, 1, 1, 1)); bool getSelected() const; diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index 00d519ecc8..3d5c6fe565 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -875,6 +875,18 @@ std::vector> CSVRender::PagedWorldspaceWidget:: return result; } +void CSVRender::PagedWorldspaceWidget::selectGroup(const std::vector& group) const +{ + for (const auto& [_, cell] : mCells) + cell->selectFromGroup(group); +} + +void CSVRender::PagedWorldspaceWidget::unhideAll() const +{ + for (const auto& [_, cell] : mCells) + cell->unhideAll(); +} + std::vector> CSVRender::PagedWorldspaceWidget::getEdited( unsigned int elementMask) const { diff --git a/apps/opencs/view/render/pagedworldspacewidget.hpp b/apps/opencs/view/render/pagedworldspacewidget.hpp index 9ba8911c7e..744cc7ccb9 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.hpp +++ b/apps/opencs/view/render/pagedworldspacewidget.hpp @@ -163,6 +163,10 @@ namespace CSVRender std::vector> getSelection(unsigned int elementMask) const override; + void selectGroup(const std::vector& group) const override; + + void unhideAll() const override; + std::vector> getEdited(unsigned int elementMask) const override; void setSubMode(int subMode, unsigned int elementMask) override; diff --git a/apps/opencs/view/render/unpagedworldspacewidget.cpp b/apps/opencs/view/render/unpagedworldspacewidget.cpp index fee608b200..899918c3b9 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.cpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.cpp @@ -199,6 +199,16 @@ std::vector> CSVRender::UnpagedWorldspaceWidget return mCell->getSelection(elementMask); } +void CSVRender::UnpagedWorldspaceWidget::selectGroup(const std::vector& group) const +{ + mCell->selectFromGroup(group); +} + +void CSVRender::UnpagedWorldspaceWidget::unhideAll() const +{ + mCell->unhideAll(); +} + std::vector> CSVRender::UnpagedWorldspaceWidget::getEdited( unsigned int elementMask) const { diff --git a/apps/opencs/view/render/unpagedworldspacewidget.hpp b/apps/opencs/view/render/unpagedworldspacewidget.hpp index 10446354e9..89c916415d 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.hpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.hpp @@ -93,6 +93,10 @@ namespace CSVRender std::vector> getSelection(unsigned int elementMask) const override; + void selectGroup(const std::vector& group) const override; + + void unhideAll() const override; + std::vector> getEdited(unsigned int elementMask) const override; void setSubMode(int subMode, unsigned int elementMask) override; diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 6911f5f043..da02c1e179 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -50,6 +50,7 @@ #include "cameracontroller.hpp" #include "instancemode.hpp" +#include "mask.hpp" #include "object.hpp" #include "pathgridmode.hpp" @@ -135,6 +136,15 @@ CSVRender::WorldspaceWidget::WorldspaceWidget(CSMDoc::Document& document, QWidge CSMPrefs::Shortcut* abortShortcut = new CSMPrefs::Shortcut("scene-edit-abort", this); connect(abortShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &WorldspaceWidget::abortDrag); + connect(new CSMPrefs::Shortcut("scene-toggle-visibility", this), qOverload<>(&CSMPrefs::Shortcut::activated), this, + &WorldspaceWidget::toggleHiddenInstances); + + connect(new CSMPrefs::Shortcut("scene-unhide-all", this), qOverload<>(&CSMPrefs::Shortcut::activated), this, + &WorldspaceWidget::unhideAll); + + connect(new CSMPrefs::Shortcut("scene-clear-selection", this), qOverload<>(&CSMPrefs::Shortcut::activated), this, + [this] { this->clearSelection(Mask_Reference); }); + mInConstructor = false; } @@ -740,6 +750,23 @@ void CSVRender::WorldspaceWidget::speedMode(bool activate) mSpeedMode = activate; } +void CSVRender::WorldspaceWidget::toggleHiddenInstances() +{ + const std::vector> selection = getSelection(Mask_Reference); + + if (selection.empty()) + return; + + const CSVRender::ObjectTag* firstSelection = dynamic_cast(selection.begin()->get()); + + const CSVRender::Mask firstMask + = firstSelection->mObject->getRootNode()->getNodeMask() == Mask_Hidden ? Mask_Reference : Mask_Hidden; + + for (const auto& object : selection) + if (const auto objectTag = dynamic_cast(object.get())) + objectTag->mObject->getRootNode()->setNodeMask(firstMask); +} + void CSVRender::WorldspaceWidget::handleInteraction(InteractionType type, bool activate) { if (activate) diff --git a/apps/opencs/view/render/worldspacewidget.hpp b/apps/opencs/view/render/worldspacewidget.hpp index 442f4922f0..505d985ffa 100644 --- a/apps/opencs/view/render/worldspacewidget.hpp +++ b/apps/opencs/view/render/worldspacewidget.hpp @@ -201,6 +201,10 @@ namespace CSVRender virtual std::vector> getSelection(unsigned int elementMask) const = 0; + virtual void selectGroup(const std::vector&) const = 0; + + virtual void unhideAll() const = 0; + virtual std::vector> getEdited(unsigned int elementMask) const = 0; virtual void setSubMode(int subMode, unsigned int elementMask) = 0; @@ -300,6 +304,8 @@ namespace CSVRender void speedMode(bool activate); + void toggleHiddenInstances(); + protected slots: void elementSelectionChanged(); diff --git a/apps/opencs/view/world/dragrecordtable.cpp b/apps/opencs/view/world/dragrecordtable.cpp index ae8c1cb708..11a0c9a540 100644 --- a/apps/opencs/view/world/dragrecordtable.cpp +++ b/apps/opencs/view/world/dragrecordtable.cpp @@ -92,7 +92,7 @@ void CSVWorld::DragRecordTable::dropEvent(QDropEvent* event) if (CSVWorld::DragDropUtils::isTopicOrJournal(*event, display)) { const CSMWorld::TableMimeData* tableMimeData = CSVWorld::DragDropUtils::getTableMimeData(*event); - for (auto universalId : tableMimeData->getData()) + for (const auto& universalId : tableMimeData->getData()) { emit createNewInfoRecord(universalId.getId()); } diff --git a/apps/opencs/view/world/table.cpp b/apps/opencs/view/world/table.cpp index 1e80805630..4212e952e8 100644 --- a/apps/opencs/view/world/table.cpp +++ b/apps/opencs/view/world/table.cpp @@ -694,10 +694,10 @@ void CSVWorld::Table::previewRecord() if (selectedRows.size() == 1) { - std::string id = getUniversalId(selectedRows.begin()->row()).getId(); + CSMWorld::UniversalId id = getUniversalId(selectedRows.begin()->row()); QModelIndex index - = mModel->getModelIndex(id, mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Modification)); + = mModel->getModelIndex(id.getId(), mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Modification)); if (mModel->data(index) != CSMWorld::RecordBase::State_Deleted) emit editRequest(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Preview, id), ""); diff --git a/apps/opencs_tests/model/world/testuniversalid.cpp b/apps/opencs_tests/model/world/testuniversalid.cpp index 2e610b0dd0..54538a591d 100644 --- a/apps/opencs_tests/model/world/testuniversalid.cpp +++ b/apps/opencs_tests/model/world/testuniversalid.cpp @@ -177,11 +177,11 @@ namespace CSMWorld UniversalId::ArgumentType_Id, "Instance", "Instance: f", ":./instance.png" }, Params{ UniversalId(UniversalId::Type_Reference, ESM::RefId::stringRefId("g")), UniversalId::Type_Reference, - UniversalId::Class_SubRecord, UniversalId::ArgumentType_RefId, "Instance", "Instance: \"g\"", + UniversalId::Class_SubRecord, UniversalId::ArgumentType_RefId, "Instance", "Instance: g", ":./instance.png" }, Params{ UniversalId(UniversalId::Type_Reference, ESM::RefId::index(ESM::REC_SKIL, 42)), UniversalId::Type_Reference, UniversalId::Class_SubRecord, UniversalId::ArgumentType_RefId, "Instance", - "Instance: Index:SKIL:0x2a", ":./instance.png" }, + "Instance: SKIL:0x2a", ":./instance.png" }, }; INSTANTIATE_TEST_SUITE_P(ValidParams, CSMWorldUniversalIdValidPerTypeTest, ValuesIn(validParams)); diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index db44b91159..373de3683d 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -24,7 +24,7 @@ add_openmw_dir (mwrender bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover postprocessor pingpongcull luminancecalculator pingpongcanvas transparentpass precipitationocclusion ripples - actorutil + actorutil distortion ) add_openmw_dir (mwinput diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index b0b49f3acd..5bbc0211c1 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -219,8 +219,6 @@ int runApplication(int argc, char* argv[]) Platform::init(); #ifdef __APPLE__ - std::filesystem::path binary_path = std::filesystem::absolute(std::filesystem::path(argv[0])); - std::filesystem::current_path(binary_path.parent_path()); setenv("OSG_GL_TEXTURE_STORAGE", "OFF", 0); #endif diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index 06a68efe4a..f3cea83224 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -29,6 +29,14 @@ namespace ESM struct LuaScripts; } +namespace LuaUtil +{ + namespace InputAction + { + class Registry; + } +} + namespace MWBase { // \brief LuaManager is the central interface through which the engine invokes lua scripts. @@ -53,6 +61,7 @@ namespace MWBase virtual void objectActivated(const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; virtual void useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) = 0; virtual void exteriorCreated(MWWorld::CellStore& cell) = 0; + virtual void actorDied(const MWWorld::Ptr& actor) = 0; virtual void questUpdated(const ESM::RefId& questId, int stage) = 0; // `arg` is either forwarded from MWGui::pushGuiMode or empty @@ -60,7 +69,8 @@ namespace MWBase // TODO: notify LuaManager about other events // virtual void objectOnHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, - // const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) = 0; + // const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful, + // DamageSourceType sourceType) = 0; struct InputEvent { diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 0c60fe9778..4711994b55 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -77,6 +77,7 @@ namespace MWGui class JailScreen; class MessageBox; class PostProcessorHud; + class SettingsWindow; enum ShowInDialogueMode { @@ -156,6 +157,7 @@ namespace MWBase virtual MWGui::ConfirmationDialog* getConfirmationDialog() = 0; virtual MWGui::TradeWindow* getTradeWindow() = 0; virtual MWGui::PostProcessorHud* getPostProcessorHud() = 0; + virtual MWGui::SettingsWindow* getSettingsWindow() = 0; /// Make the player use an item, while updating GUI state accordingly virtual void useItem(const MWWorld::Ptr& item, bool force = false) = 0; diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 14e3b2b3b7..4247ef2e3e 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -185,7 +185,7 @@ namespace MWBase virtual std::string_view getCellName(const ESM::Cell* cell) const = 0; - virtual void removeRefScript(MWWorld::RefData* ref) = 0; + virtual void removeRefScript(const MWWorld::CellRef* ref) = 0; //< Remove the script attached to ref from mLocalScripts virtual MWWorld::Ptr getPtr(const ESM::RefId& name, bool activeOnly) = 0; diff --git a/apps/openmw/mwclass/activator.cpp b/apps/openmw/mwclass/activator.cpp index fc6cfadb55..01437b2abd 100644 --- a/apps/openmw/mwclass/activator.cpp +++ b/apps/openmw/mwclass/activator.cpp @@ -110,7 +110,7 @@ namespace MWClass text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } @@ -145,12 +145,11 @@ namespace MWClass = getModel(ptr); // Assume it's not empty, since we wouldn't have gotten the soundgen otherwise const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const ESM::RefId* creatureId = nullptr; - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); for (const ESM::Creature& iter : store.get()) { if (!iter.mModel.empty() - && Misc::StringUtils::ciEqual(model, Misc::ResourceHelpers::correctMeshPath(iter.mModel, vfs))) + && Misc::StringUtils::ciEqual(model, Misc::ResourceHelpers::correctMeshPath(iter.mModel))) { creatureId = !iter.mOriginal.empty() ? &iter.mOriginal : &iter.mId; break; diff --git a/apps/openmw/mwclass/apparatus.cpp b/apps/openmw/mwclass/apparatus.cpp index 10687171a0..1ff7ef5bd6 100644 --- a/apps/openmw/mwclass/apparatus.cpp +++ b/apps/openmw/mwclass/apparatus.cpp @@ -105,7 +105,7 @@ namespace MWClass text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/armor.cpp b/apps/openmw/mwclass/armor.cpp index 4006f21ce7..28bb1ff35c 100644 --- a/apps/openmw/mwclass/armor.cpp +++ b/apps/openmw/mwclass/armor.cpp @@ -265,7 +265,7 @@ namespace MWClass if (!info.enchant.empty()) info.remainingEnchantCharge = static_cast(ptr.getCellRef().getEnchantmentCharge()); - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/book.cpp b/apps/openmw/mwclass/book.cpp index 55de7a64ab..d731f56394 100644 --- a/apps/openmw/mwclass/book.cpp +++ b/apps/openmw/mwclass/book.cpp @@ -127,7 +127,7 @@ namespace MWClass info.enchant = ref->mBase->mEnchant; - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/classmodel.hpp b/apps/openmw/mwclass/classmodel.hpp index 5d1019ee1d..65c2f87a14 100644 --- a/apps/openmw/mwclass/classmodel.hpp +++ b/apps/openmw/mwclass/classmodel.hpp @@ -1,8 +1,6 @@ #ifndef OPENMW_MWCLASS_CLASSMODEL_H #define OPENMW_MWCLASS_CLASSMODEL_H -#include "../mwbase/environment.hpp" - #include "../mwworld/livecellref.hpp" #include "../mwworld/ptr.hpp" @@ -19,8 +17,7 @@ namespace MWClass const MWWorld::LiveCellRef* ref = ptr.get(); if (!ref->mBase->mModel.empty()) - return Misc::ResourceHelpers::correctMeshPath( - ref->mBase->mModel, MWBase::Environment::get().getResourceSystem()->getVFS()); + return Misc::ResourceHelpers::correctMeshPath(ref->mBase->mModel); return {}; } diff --git a/apps/openmw/mwclass/clothing.cpp b/apps/openmw/mwclass/clothing.cpp index 0614c92eb9..32a0b62729 100644 --- a/apps/openmw/mwclass/clothing.cpp +++ b/apps/openmw/mwclass/clothing.cpp @@ -172,7 +172,7 @@ namespace MWClass if (!info.enchant.empty()) info.remainingEnchantCharge = static_cast(ptr.getCellRef().getEnchantmentCharge()); - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index 0efbbc84fd..28779f971f 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -271,7 +271,7 @@ namespace MWClass text += "\nYou can not use evidence chests"; } - info.text = text; + info.text = std::move(text); return info; } @@ -306,7 +306,7 @@ namespace MWClass if (newPtr.getRefData().getCustomData()) { MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); - newPtr.getContainerStore()->setPtr(newPtr); + getContainerStore(newPtr).setPtr(newPtr); } return newPtr; } diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 2628cd3905..bb9c1bc277 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -284,7 +284,8 @@ namespace MWClass if (!success) { - victim.getClass().onHit(victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false); + victim.getClass().onHit( + victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } @@ -345,24 +346,35 @@ namespace MWClass MWMechanics::diseaseContact(victim, ptr); - victim.getClass().onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true); + victim.getClass().onHit( + victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee); } void Creature::onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const { MWMechanics::CreatureStats& stats = getCreatureStats(ptr); + // Self defense + bool setOnPcHitMe = true; + // NOTE: 'object' and/or 'attacker' may be empty. if (!attacker.isEmpty() && attacker.getClass().isActor() && !stats.getAiSequence().isInCombat(attacker)) + { stats.setAttacked(true); - // Self defense - bool setOnPcHitMe = true; // Note OnPcHitMe is not set for friendly hits. - - // No retaliation for totally static creatures (they have no movement or attacks anyway) - if (isMobile(ptr) && !attacker.isEmpty()) - setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + // No retaliation for totally static creatures (they have no movement or attacks anyway) + if (isMobile(ptr)) + { + bool complain = sourceType == MWMechanics::DamageSourceType::Melee; + bool supportFriendlyFire = sourceType != MWMechanics::DamageSourceType::Ranged; + if (supportFriendlyFire && MWMechanics::friendlyHit(attacker, ptr, complain)) + setOnPcHitMe = false; + else + setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + } + } // Attacker and target store each other as hitattemptactor if they have no one stored yet if (!attacker.isEmpty() && attacker.getClass().isActor()) @@ -587,7 +599,7 @@ namespace MWClass std::string text; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); - info.text = text; + info.text = std::move(text); return info; } @@ -644,12 +656,10 @@ namespace MWClass const std::string model = getModel(ptr); if (!model.empty()) { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); for (const ESM::Creature& creature : store.get()) { if (creature.mId != ourId && creature.mOriginal != ourId && !creature.mModel.empty() - && Misc::StringUtils::ciEqual( - model, Misc::ResourceHelpers::correctMeshPath(creature.mModel, vfs))) + && Misc::StringUtils::ciEqual(model, Misc::ResourceHelpers::correctMeshPath(creature.mModel))) { const ESM::RefId& fallbackId = !creature.mOriginal.empty() ? creature.mOriginal : creature.mId; sound = store.get().begin(); @@ -779,32 +789,26 @@ namespace MWClass const ESM::CreatureState& creatureState = state.asCreatureState(); - if (state.mVersion > 0) + if (!ptr.getRefData().getCustomData()) { - if (!ptr.getRefData().getCustomData()) + if (creatureState.mCreatureStats.mMissingACDT) + ensureCustomData(ptr); + else { - if (creatureState.mCreatureStats.mMissingACDT) - ensureCustomData(ptr); + // Create a CustomData, but don't fill it from ESM records (not needed) + auto data = std::make_unique(); + + if (hasInventoryStore(ptr)) + data->mContainerStore = std::make_unique(); else - { - // Create a CustomData, but don't fill it from ESM records (not needed) - auto data = std::make_unique(); + data->mContainerStore = std::make_unique(); - if (hasInventoryStore(ptr)) - data->mContainerStore = std::make_unique(); - else - data->mContainerStore = std::make_unique(); + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + data->mContainerStore->setPtr(ptr); - MWBase::Environment::get().getWorldModel()->registerPtr(ptr); - data->mContainerStore->setPtr(ptr); - - ptr.getRefData().setCustomData(std::move(data)); - } + ptr.getRefData().setCustomData(std::move(data)); } } - else - ensureCustomData( - ptr); // in openmw 0.30 savegames not all state was saved yet, so need to load it regardless. CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData(); @@ -824,7 +828,7 @@ namespace MWClass } const CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData(); - if (ptr.getRefData().getCount() <= 0 + if (ptr.getCellRef().getCount() <= 0 && (!isFlagBitSet(ptr, ESM::Creature::Respawn) || !customData.mCreatureStats.isDead())) { state.mHasCustomState = false; @@ -844,7 +848,7 @@ namespace MWClass void Creature::respawn(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& creatureStats = getCreatureStats(ptr); - if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead()) + if (ptr.getCellRef().getCount() > 0 && !creatureStats.isDead()) return; if (!creatureStats.isDeathAnimationFinished()) @@ -856,16 +860,16 @@ namespace MWClass static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->mValue.getFloat(); float delay - = ptr.getRefData().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); + = ptr.getCellRef().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); if (isFlagBitSet(ptr, ESM::Creature::Respawn) && creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp()) { if (ptr.getCellRef().hasContentFile()) { - if (ptr.getRefData().getCount() == 0) + if (ptr.getCellRef().getCount() == 0) { - ptr.getRefData().setCount(1); + ptr.getCellRef().setCount(1); const ESM::RefId& script = getScript(ptr); if (!script.empty()) MWBase::Environment::get().getWorld()->getLocalScripts().add(script, ptr); diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index bd7101e93d..b407852242 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -67,7 +67,8 @@ namespace MWClass const osg::Vec3f& hitPosition, bool success) const override; void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const override; + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const override; std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation diff --git a/apps/openmw/mwclass/creaturelevlist.cpp b/apps/openmw/mwclass/creaturelevlist.cpp index 461cf07276..fbae54737c 100644 --- a/apps/openmw/mwclass/creaturelevlist.cpp +++ b/apps/openmw/mwclass/creaturelevlist.cpp @@ -81,7 +81,7 @@ namespace MWClass if (!creature.isEmpty()) { const MWMechanics::CreatureStats& creatureStats = creature.getClass().getCreatureStats(creature); - if (creature.getRefData().getCount() == 0) + if (creature.getCellRef().getCount() == 0) customData.mSpawn = true; else if (creatureStats.isDead()) { diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index 695bea5f10..99acfcf4df 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -293,7 +293,7 @@ namespace MWClass text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/esm4base.hpp b/apps/openmw/mwclass/esm4base.hpp index 9c02af963d..7059ae07cb 100644 --- a/apps/openmw/mwclass/esm4base.hpp +++ b/apps/openmw/mwclass/esm4base.hpp @@ -6,6 +6,8 @@ #include #include +#include "../mwbase/environment.hpp" + #include "../mwgui/tooltips.hpp" #include "../mwworld/cellstore.hpp" diff --git a/apps/openmw/mwclass/esm4npc.cpp b/apps/openmw/mwclass/esm4npc.cpp index 638144eb66..2da7342de4 100644 --- a/apps/openmw/mwclass/esm4npc.cpp +++ b/apps/openmw/mwclass/esm4npc.cpp @@ -88,26 +88,34 @@ namespace MWClass auto data = std::make_unique(); const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); - auto npcRecs = withBaseTemplates(ptr.get()->mBase); + const ESM4::Npc* const base = ptr.get()->mBase; + auto npcRecs = withBaseTemplates(base); data->mTraits = chooseTemplate(npcRecs, ESM4::Npc::Template_UseTraits); + + if (data->mTraits == nullptr) + Log(Debug::Warning) << "Traits are not found for ESM4 NPC base record: \"" << base->mEditorId << "\" (" + << ESM::RefId(base->mId) << ")"; + data->mBaseData = chooseTemplate(npcRecs, ESM4::Npc::Template_UseBaseData); - if (!data->mTraits) - throw std::runtime_error("ESM4 NPC traits not found"); - if (!data->mBaseData) - throw std::runtime_error("ESM4 NPC base data not found"); + if (data->mBaseData == nullptr) + Log(Debug::Warning) << "Base data is not found for ESM4 NPC base record: \"" << base->mEditorId << "\" (" + << ESM::RefId(base->mId) << ")"; - data->mRace = store->get().find(data->mTraits->mRace); - if (data->mTraits->mIsTES4) - data->mIsFemale = data->mTraits->mBaseConfig.tes4.flags & ESM4::Npc::TES4_Female; - else if (data->mTraits->mIsFONV) - data->mIsFemale = data->mTraits->mBaseConfig.fo3.flags & ESM4::Npc::FO3_Female; - else if (data->mTraits->mIsFO4) - data->mIsFemale - = data->mTraits->mBaseConfig.fo4.flags & ESM4::Npc::TES5_Female; // FO4 flags are the same as TES5 - else - data->mIsFemale = data->mTraits->mBaseConfig.tes5.flags & ESM4::Npc::TES5_Female; + if (data->mTraits != nullptr) + { + data->mRace = store->get().find(data->mTraits->mRace); + if (data->mTraits->mIsTES4) + data->mIsFemale = data->mTraits->mBaseConfig.tes4.flags & ESM4::Npc::TES4_Female; + else if (data->mTraits->mIsFONV) + data->mIsFemale = data->mTraits->mBaseConfig.fo3.flags & ESM4::Npc::FO3_Female; + else if (data->mTraits->mIsFO4) + data->mIsFemale + = data->mTraits->mBaseConfig.fo4.flags & ESM4::Npc::TES5_Female; // FO4 flags are the same as TES5 + else + data->mIsFemale = data->mTraits->mBaseConfig.tes5.flags & ESM4::Npc::TES5_Female; + } if (auto inv = chooseTemplate(npcRecs, ESM4::Npc::Template_UseInventory)) { @@ -116,7 +124,7 @@ namespace MWClass if (auto* armor = ESM4Impl::resolveLevelled(ESM::FormId::fromUint32(item.item))) data->mEquippedArmor.push_back(armor); - else if (data->mTraits->mIsTES4) + else if (data->mTraits != nullptr && data->mTraits->mIsTES4) { const auto* clothing = ESM4Impl::resolveLevelled( ESM::FormId::fromUint32(item.item)); @@ -170,17 +178,21 @@ namespace MWClass std::string ESM4Npc::getModel(const MWWorld::ConstPtr& ptr) const { const ESM4NpcCustomData& data = getCustomData(ptr); + if (data.mTraits == nullptr) + return {}; std::string_view model; if (data.mTraits->mIsTES4) model = data.mTraits->mModel; else model = data.mIsFemale ? data.mRace->mModelFemale : data.mRace->mModelMale; - const VFS::Manager* vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - return Misc::ResourceHelpers::correctMeshPath(model, vfs); + return Misc::ResourceHelpers::correctMeshPath(model); } std::string_view ESM4Npc::getName(const MWWorld::ConstPtr& ptr) const { - return getCustomData(ptr).mBaseData->mFullName; + const ESM4::Npc* const baseData = getCustomData(ptr).mBaseData; + if (baseData == nullptr) + return {}; + return baseData->mFullName; } } diff --git a/apps/openmw/mwclass/ingredient.cpp b/apps/openmw/mwclass/ingredient.cpp index 3e07a24610..5225170be7 100644 --- a/apps/openmw/mwclass/ingredient.cpp +++ b/apps/openmw/mwclass/ingredient.cpp @@ -145,7 +145,7 @@ namespace MWClass } info.effects = list; - info.text = text; + info.text = std::move(text); info.isIngredient = true; return info; diff --git a/apps/openmw/mwclass/light.cpp b/apps/openmw/mwclass/light.cpp index 92ba8e1512..6e34e3c2bd 100644 --- a/apps/openmw/mwclass/light.cpp +++ b/apps/openmw/mwclass/light.cpp @@ -177,7 +177,7 @@ namespace MWClass text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/lockpick.cpp b/apps/openmw/mwclass/lockpick.cpp index 6c46f2e66f..d3c3d479e4 100644 --- a/apps/openmw/mwclass/lockpick.cpp +++ b/apps/openmw/mwclass/lockpick.cpp @@ -122,7 +122,7 @@ namespace MWClass text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/misc.cpp b/apps/openmw/mwclass/misc.cpp index ae78773fa1..0f26dfd2df 100644 --- a/apps/openmw/mwclass/misc.cpp +++ b/apps/openmw/mwclass/misc.cpp @@ -79,8 +79,8 @@ namespace MWClass const MWWorld::LiveCellRef* ref = ptr.get(); int value = ref->mBase->mData.mValue; - if (ptr.getCellRef().getGoldValue() > 1 && ptr.getRefData().getCount() == 1) - value = ptr.getCellRef().getGoldValue(); + if (isGold(ptr) && ptr.getCellRef().getCount() != 1) + value = 1; if (!ptr.getCellRef().getSoul().empty()) { @@ -167,7 +167,7 @@ namespace MWClass text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } @@ -189,8 +189,7 @@ namespace MWClass const MWWorld::LiveCellRef* ref = newRef.getPtr().get(); MWWorld::Ptr ptr(cell.insert(ref), &cell); - ptr.getCellRef().setGoldValue(goldAmount); - ptr.getRefData().setCount(1); + ptr.getCellRef().setCount(goldAmount); return ptr; } @@ -203,7 +202,7 @@ namespace MWClass { const MWWorld::LiveCellRef* ref = ptr.get(); newPtr = MWWorld::Ptr(cell.insert(ref), &cell); - newPtr.getRefData().setCount(count); + newPtr.getCellRef().setCount(count); } newPtr.getCellRef().unsetRefNum(); newPtr.getRefData().setLuaScripts(nullptr); @@ -216,10 +215,9 @@ namespace MWClass MWWorld::Ptr newPtr; if (isGold(ptr)) { - newPtr = createGold(cell, getValue(ptr) * ptr.getRefData().getCount()); + newPtr = createGold(cell, getValue(ptr) * ptr.getCellRef().getCount()); newPtr.getRefData() = ptr.getRefData(); newPtr.getCellRef().setRefNum(ptr.getCellRef().getRefNum()); - newPtr.getRefData().setCount(1); } else { diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index b9e8bc8dfb..4f295f7b35 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -61,6 +61,23 @@ namespace { + struct NpcParts + { + const ESM::RefId mSwimLeft = ESM::RefId::stringRefId("Swim Left"); + const ESM::RefId mSwimRight = ESM::RefId::stringRefId("Swim Right"); + const ESM::RefId mFootWaterLeft = ESM::RefId::stringRefId("FootWaterLeft"); + const ESM::RefId mFootWaterRight = ESM::RefId::stringRefId("FootWaterRight"); + const ESM::RefId mFootBareLeft = ESM::RefId::stringRefId("FootBareLeft"); + const ESM::RefId mFootBareRight = ESM::RefId::stringRefId("FootBareRight"); + const ESM::RefId mFootLightLeft = ESM::RefId::stringRefId("footLightLeft"); + const ESM::RefId mFootLightRight = ESM::RefId::stringRefId("footLightRight"); + const ESM::RefId mFootMediumRight = ESM::RefId::stringRefId("FootMedRight"); + const ESM::RefId mFootMediumLeft = ESM::RefId::stringRefId("FootMedLeft"); + const ESM::RefId mFootHeavyLeft = ESM::RefId::stringRefId("footHeavyLeft"); + const ESM::RefId mFootHeavyRight = ESM::RefId::stringRefId("footHeavyRight"); + }; + + const NpcParts npcParts; int is_even(double d) { @@ -92,13 +109,7 @@ namespace const auto& attributes = MWBase::Environment::get().getESMStore()->get(); int level = creatureStats.getLevel(); for (const ESM::Attribute& attribute : attributes) - { - auto index = ESM::Attribute::refIdToIndex(attribute.mId); - assert(index >= 0); - - const ESM::Race::MaleFemale& value = race->mData.mAttributeValues[static_cast(index)]; - creatureStats.setAttribute(attribute.mId, male ? value.mMale : value.mFemale); - } + creatureStats.setAttribute(attribute.mId, race->mData.getAttribute(attribute.mId, male)); // class bonus const ESM::Class* class_ = MWBase::Environment::get().getESMStore()->get().find(npc->mClass); @@ -319,14 +330,8 @@ namespace MWClass for (size_t i = 0; i < ref->mBase->mNpdt.mSkills.size(); ++i) data->mNpcStats.getSkill(ESM::Skill::indexToRefId(i)).setBase(ref->mBase->mNpdt.mSkills[i]); - data->mNpcStats.setAttribute(ESM::Attribute::Strength, ref->mBase->mNpdt.mStrength); - data->mNpcStats.setAttribute(ESM::Attribute::Intelligence, ref->mBase->mNpdt.mIntelligence); - data->mNpcStats.setAttribute(ESM::Attribute::Willpower, ref->mBase->mNpdt.mWillpower); - data->mNpcStats.setAttribute(ESM::Attribute::Agility, ref->mBase->mNpdt.mAgility); - data->mNpcStats.setAttribute(ESM::Attribute::Speed, ref->mBase->mNpdt.mSpeed); - data->mNpcStats.setAttribute(ESM::Attribute::Endurance, ref->mBase->mNpdt.mEndurance); - data->mNpcStats.setAttribute(ESM::Attribute::Personality, ref->mBase->mNpdt.mPersonality); - data->mNpcStats.setAttribute(ESM::Attribute::Luck, ref->mBase->mNpdt.mLuck); + for (size_t i = 0; i < ref->mBase->mNpdt.mAttributes.size(); ++i) + data->mNpcStats.setAttribute(ESM::Attribute::indexToRefId(i), ref->mBase->mNpdt.mAttributes[i]); data->mNpcStats.setHealth(ref->mBase->mNpdt.mHealth); data->mNpcStats.setMagicka(ref->mBase->mNpdt.mMana); @@ -443,24 +448,22 @@ namespace MWClass models.push_back(Settings::models().mXbaseanimfemale); models.push_back(Settings::models().mXbaseanim); - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - if (!npc->mBase->mModel.empty()) - models.push_back(Misc::ResourceHelpers::correctMeshPath(npc->mBase->mModel, vfs)); + models.push_back(Misc::ResourceHelpers::correctMeshPath(npc->mBase->mModel)); if (!npc->mBase->mHead.empty()) { const ESM::BodyPart* head = MWBase::Environment::get().getESMStore()->get().search(npc->mBase->mHead); if (head) - models.push_back(Misc::ResourceHelpers::correctMeshPath(head->mModel, vfs)); + models.push_back(Misc::ResourceHelpers::correctMeshPath(head->mModel)); } if (!npc->mBase->mHair.empty()) { const ESM::BodyPart* hair = MWBase::Environment::get().getESMStore()->get().search(npc->mBase->mHair); if (hair) - models.push_back(Misc::ResourceHelpers::correctMeshPath(hair->mModel, vfs)); + models.push_back(Misc::ResourceHelpers::correctMeshPath(hair->mModel)); } bool female = (npc->mBase->mFlags & ESM::NPC::Female); @@ -499,7 +502,7 @@ namespace MWClass const ESM::BodyPart* part = MWBase::Environment::get().getESMStore()->get().search(partname); if (part && !part->mModel.empty()) - models.push_back(Misc::ResourceHelpers::correctMeshPath(part->mModel, vfs)); + models.push_back(Misc::ResourceHelpers::correctMeshPath(part->mModel)); } } } @@ -513,7 +516,7 @@ namespace MWClass { const ESM::BodyPart* part = *it; if (part && !part->mModel.empty()) - models.push_back(Misc::ResourceHelpers::correctMeshPath(part->mModel, vfs)); + models.push_back(Misc::ResourceHelpers::correctMeshPath(part->mModel)); } } } @@ -611,7 +614,8 @@ namespace MWClass float damage = 0.0f; if (!success) { - othercls.onHit(victim, damage, false, weapon, ptr, osg::Vec3f(), false); + othercls.onHit( + victim, damage, false, weapon, ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); MWMechanics::reduceWeaponCondition(damage, false, weapon, ptr); MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); return; @@ -684,24 +688,29 @@ namespace MWClass MWMechanics::diseaseContact(victim, ptr); - othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true); + othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee); } void Npc::onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const { MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); MWMechanics::CreatureStats& stats = getCreatureStats(ptr); bool wasDead = stats.isDead(); - // Note OnPcHitMe is not set for friendly hits. bool setOnPcHitMe = true; // NOTE: 'object' and/or 'attacker' may be empty. if (!attacker.isEmpty() && attacker.getClass().isActor() && !stats.getAiSequence().isInCombat(attacker)) { stats.setAttacked(true); - setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + bool complain = sourceType == MWMechanics::DamageSourceType::Melee; + bool supportFriendlyFire = sourceType != MWMechanics::DamageSourceType::Ranged; + if (supportFriendlyFire && MWMechanics::friendlyHit(attacker, ptr, complain)) + setOnPcHitMe = false; + else + setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); } // Attacker and target store each other as hitattemptactor if they have no one stored yet @@ -1199,24 +1208,24 @@ namespace MWClass if (ptr == MWMechanics::getPlayer() && ptr.isInCell() && MWBase::Environment::get().getWorld()->isFirstPerson()) { if (ref->mBase->isMale()) - scale *= race->mData.mHeight.mMale; + scale *= race->mData.mMaleHeight; else - scale *= race->mData.mHeight.mFemale; + scale *= race->mData.mFemaleHeight; return; } if (ref->mBase->isMale()) { - scale.x() *= race->mData.mWeight.mMale; - scale.y() *= race->mData.mWeight.mMale; - scale.z() *= race->mData.mHeight.mMale; + scale.x() *= race->mData.mMaleWeight; + scale.y() *= race->mData.mMaleWeight; + scale.z() *= race->mData.mMaleHeight; } else { - scale.x() *= race->mData.mWeight.mFemale; - scale.y() *= race->mData.mWeight.mFemale; - scale.z() *= race->mData.mHeight.mFemale; + scale.x() *= race->mData.mFemaleWeight; + scale.y() *= race->mData.mFemaleWeight; + scale.z() *= race->mData.mFemaleHeight; } } @@ -1233,19 +1242,6 @@ namespace MWClass ESM::RefId Npc::getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const { - static const ESM::RefId swimLeft = ESM::RefId::stringRefId("Swim Left"); - static const ESM::RefId swimRight = ESM::RefId::stringRefId("Swim Right"); - static const ESM::RefId footWaterLeft = ESM::RefId::stringRefId("FootWaterLeft"); - static const ESM::RefId footWaterRight = ESM::RefId::stringRefId("FootWaterRight"); - static const ESM::RefId footBareLeft = ESM::RefId::stringRefId("FootBareLeft"); - static const ESM::RefId footBareRight = ESM::RefId::stringRefId("FootBareRight"); - static const ESM::RefId footLightLeft = ESM::RefId::stringRefId("footLightLeft"); - static const ESM::RefId footLightRight = ESM::RefId::stringRefId("footLightRight"); - static const ESM::RefId footMediumRight = ESM::RefId::stringRefId("FootMedRight"); - static const ESM::RefId footMediumLeft = ESM::RefId::stringRefId("FootMedLeft"); - static const ESM::RefId footHeavyLeft = ESM::RefId::stringRefId("footHeavyLeft"); - static const ESM::RefId footHeavyRight = ESM::RefId::stringRefId("footHeavyRight"); - if (name == "left" || name == "right") { MWBase::World* world = MWBase::Environment::get().getWorld(); @@ -1253,9 +1249,9 @@ namespace MWClass return ESM::RefId(); osg::Vec3f pos(ptr.getRefData().getPosition().asVec3()); if (world->isSwimming(ptr)) - return (name == "left") ? swimLeft : swimRight; + return (name == "left") ? npcParts.mSwimLeft : npcParts.mSwimRight; if (world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) - return (name == "left") ? footWaterLeft : footWaterRight; + return (name == "left") ? npcParts.mFootWaterLeft : npcParts.mFootWaterRight; if (world->isOnGround(ptr)) { if (getNpcStats(ptr).isWerewolf() @@ -1270,15 +1266,15 @@ namespace MWClass const MWWorld::InventoryStore& inv = Npc::getInventoryStore(ptr); MWWorld::ConstContainerStoreIterator boots = inv.getSlot(MWWorld::InventoryStore::Slot_Boots); if (boots == inv.end() || boots->getType() != ESM::Armor::sRecordId) - return (name == "left") ? footBareLeft : footBareRight; + return (name == "left") ? npcParts.mFootBareLeft : npcParts.mFootBareRight; ESM::RefId skill = boots->getClass().getEquipmentSkill(*boots); if (skill == ESM::Skill::LightArmor) - return (name == "left") ? footLightLeft : footLightRight; + return (name == "left") ? npcParts.mFootLightLeft : npcParts.mFootLightRight; else if (skill == ESM::Skill::MediumArmor) - return (name == "left") ? footMediumLeft : footMediumRight; + return (name == "left") ? npcParts.mFootMediumLeft : npcParts.mFootMediumRight; else if (skill == ESM::Skill::HeavyArmor) - return (name == "left") ? footHeavyLeft : footHeavyRight; + return (name == "left") ? npcParts.mFootHeavyLeft : npcParts.mFootHeavyRight; } return ESM::RefId(); } @@ -1287,9 +1283,9 @@ namespace MWClass if (name == "land") return ESM::RefId(); if (name == "swimleft") - return swimLeft; + return npcParts.mSwimLeft; if (name == "swimright") - return swimRight; + return npcParts.mSwimRight; // TODO: I have no idea what these are supposed to do for NPCs since they use // voiced dialog for various conditions like health loss and combat taunts. Maybe // only for biped creatures? @@ -1333,25 +1329,19 @@ namespace MWClass const ESM::NpcState& npcState = state.asNpcState(); - if (state.mVersion > 0) + if (!ptr.getRefData().getCustomData()) { - if (!ptr.getRefData().getCustomData()) + if (npcState.mCreatureStats.mMissingACDT) + ensureCustomData(ptr); + else { - if (npcState.mCreatureStats.mMissingACDT) - ensureCustomData(ptr); - else - { - // Create a CustomData, but don't fill it from ESM records (not needed) - auto data = std::make_unique(); - MWBase::Environment::get().getWorldModel()->registerPtr(ptr); - data->mInventoryStore.setPtr(ptr); - ptr.getRefData().setCustomData(std::move(data)); - } + // Create a CustomData, but don't fill it from ESM records (not needed) + auto data = std::make_unique(); + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + data->mInventoryStore.setPtr(ptr); + ptr.getRefData().setCustomData(std::move(data)); } } - else - ensureCustomData( - ptr); // in openmw 0.30 savegames not all state was saved yet, so need to load it regardless. NpcCustomData& customData = ptr.getRefData().getCustomData()->asNpcCustomData(); @@ -1372,7 +1362,7 @@ namespace MWClass } const NpcCustomData& customData = ptr.getRefData().getCustomData()->asNpcCustomData(); - if (ptr.getRefData().getCount() <= 0 + if (ptr.getCellRef().getCount() <= 0 && (!(ptr.get()->mBase->mFlags & ESM::NPC::Respawn) || !customData.mNpcStats.isDead())) { state.mHasCustomState = false; @@ -1409,7 +1399,7 @@ namespace MWClass void Npc::respawn(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& creatureStats = getCreatureStats(ptr); - if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead()) + if (ptr.getCellRef().getCount() > 0 && !creatureStats.isDead()) return; if (!creatureStats.isDeathAnimationFinished()) @@ -1421,16 +1411,16 @@ namespace MWClass static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->mValue.getFloat(); float delay - = ptr.getRefData().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); + = ptr.getCellRef().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); if (ptr.get()->mBase->mFlags & ESM::NPC::Respawn && creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp()) { if (ptr.getCellRef().hasContentFile()) { - if (ptr.getRefData().getCount() == 0) + if (ptr.getCellRef().getCount() == 0) { - ptr.getRefData().setCount(1); + ptr.getCellRef().setCount(1); const ESM::RefId& script = getScript(ptr); if (!script.empty()) MWBase::Environment::get().getWorld()->getLocalScripts().add(script, ptr); diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index eb8cafc9d1..ca0d0ac95d 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -82,7 +82,8 @@ namespace MWClass const osg::Vec3f& hitPosition, bool success) const override; void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const override; + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const override; void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: diff --git a/apps/openmw/mwclass/potion.cpp b/apps/openmw/mwclass/potion.cpp index 5811ec10db..7cf0c54f5c 100644 --- a/apps/openmw/mwclass/potion.cpp +++ b/apps/openmw/mwclass/potion.cpp @@ -118,7 +118,7 @@ namespace MWClass text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/probe.cpp b/apps/openmw/mwclass/probe.cpp index 7a6c00824d..96c94339bb 100644 --- a/apps/openmw/mwclass/probe.cpp +++ b/apps/openmw/mwclass/probe.cpp @@ -121,7 +121,7 @@ namespace MWClass text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/repair.cpp b/apps/openmw/mwclass/repair.cpp index 0d38271aab..cf4a42be70 100644 --- a/apps/openmw/mwclass/repair.cpp +++ b/apps/openmw/mwclass/repair.cpp @@ -123,7 +123,7 @@ namespace MWClass text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index 1cc2c86761..2c9a9b5c7a 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -243,7 +243,7 @@ namespace MWClass text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } @@ -276,8 +276,8 @@ namespace MWClass return { 0, "#{sInventoryMessage1}" }; // Do not allow equip weapons from inventory during attack - if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc) - && MWBase::Environment::get().getWindowManager()->isGuiMode()) + if (npc.isInCell() && MWBase::Environment::get().getWindowManager()->isGuiMode() + && MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc)) return { 0, "#{sCantEquipWeapWarning}" }; std::pair, bool> slots_ = getEquipmentSlots(ptr); diff --git a/apps/openmw/mwdialogue/journalimp.cpp b/apps/openmw/mwdialogue/journalimp.cpp index e4d9453c83..28a2e16699 100644 --- a/apps/openmw/mwdialogue/journalimp.cpp +++ b/apps/openmw/mwdialogue/journalimp.cpp @@ -250,7 +250,7 @@ namespace MWDialogue void Journal::readRecord(ESM::ESMReader& reader, uint32_t type) { - if (type == ESM::REC_JOUR || type == ESM::REC_JOUR_LEGACY) + if (type == ESM::REC_JOUR) { ESM::JournalEntry record; record.load(reader); diff --git a/apps/openmw/mwgui/alchemywindow.cpp b/apps/openmw/mwgui/alchemywindow.cpp index 333722a149..5a6245fca0 100644 --- a/apps/openmw/mwgui/alchemywindow.cpp +++ b/apps/openmw/mwgui/alchemywindow.cpp @@ -151,7 +151,7 @@ namespace MWGui if (mIngredients[i]->isUserString("ToolTipType")) { MWWorld::Ptr ingred = *mIngredients[i]->getUserData(); - if (ingred.getRefData().getCount() == 0) + if (ingred.getCellRef().getCount() == 0) mAlchemy->removeIngredient(i); } @@ -395,8 +395,10 @@ namespace MWGui { std::string suggestedName = mAlchemy->suggestPotionName(); if (suggestedName != mSuggestedPotionName) + { mNameEdit->setCaptionWithReplacing(suggestedName); - mSuggestedPotionName = suggestedName; + mSuggestedPotionName = std::move(suggestedName); + } mSortModel->clearDragItems(); @@ -413,7 +415,7 @@ namespace MWGui } if (!item.isEmpty()) - mSortModel->addDragItem(item, item.getRefData().getCount()); + mSortModel->addDragItem(item, item.getCellRef().getCount()); if (ingredient->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(ingredient->getChildAt(0)); @@ -428,12 +430,12 @@ namespace MWGui ingredient->setUserString("ToolTipType", "ItemPtr"); ingredient->setUserData(MWWorld::Ptr(item)); - ingredient->setCount(item.getRefData().getCount()); + ingredient->setCount(item.getCellRef().getCount()); } mItemView->update(); - std::set effectIds = mAlchemy->listEffects(); + std::vector effectIds = mAlchemy->listEffects(); Widgets::SpellEffectList list; unsigned int effectIndex = 0; for (const MWMechanics::EffectKey& effectKey : effectIds) diff --git a/apps/openmw/mwgui/bookpage.cpp b/apps/openmw/mwgui/bookpage.cpp index 5d9256b20d..1966442513 100644 --- a/apps/openmw/mwgui/bookpage.cpp +++ b/apps/openmw/mwgui/bookpage.cpp @@ -1065,7 +1065,7 @@ namespace MWGui { createActiveFormats(newBook); - mBook = newBook; + mBook = std::move(newBook); setPage(newPage); if (newPage < mBook->mPages.size()) diff --git a/apps/openmw/mwgui/charactercreation.cpp b/apps/openmw/mwgui/charactercreation.cpp index 19f7d97176..4141e61e34 100644 --- a/apps/openmw/mwgui/charactercreation.cpp +++ b/apps/openmw/mwgui/charactercreation.cpp @@ -333,10 +333,10 @@ namespace MWGui if (!classId.empty()) MWBase::Environment::get().getMechanicsManager()->setPlayerClass(classId); - const ESM::Class* klass = MWBase::Environment::get().getESMStore()->get().find(classId); - if (klass) + const ESM::Class* pickedClass = MWBase::Environment::get().getESMStore()->get().find(classId); + if (pickedClass) { - mPlayerClass = *klass; + mPlayerClass = *pickedClass; } MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mPickClassDialog)); } @@ -454,30 +454,30 @@ namespace MWGui { if (mCreateClassDialog) { - ESM::Class klass; - klass.mName = mCreateClassDialog->getName(); - klass.mDescription = mCreateClassDialog->getDescription(); - klass.mData.mSpecialization = mCreateClassDialog->getSpecializationId(); - klass.mData.mIsPlayable = 0x1; - klass.mRecordFlags = 0; + ESM::Class createdClass; + createdClass.mName = mCreateClassDialog->getName(); + createdClass.mDescription = mCreateClassDialog->getDescription(); + createdClass.mData.mSpecialization = mCreateClassDialog->getSpecializationId(); + createdClass.mData.mIsPlayable = 0x1; + createdClass.mRecordFlags = 0; std::vector attributes = mCreateClassDialog->getFavoriteAttributes(); - assert(attributes.size() >= klass.mData.mAttribute.size()); - for (size_t i = 0; i < klass.mData.mAttribute.size(); ++i) - klass.mData.mAttribute[i] = ESM::Attribute::refIdToIndex(attributes[i]); + assert(attributes.size() >= createdClass.mData.mAttribute.size()); + for (size_t i = 0; i < createdClass.mData.mAttribute.size(); ++i) + createdClass.mData.mAttribute[i] = ESM::Attribute::refIdToIndex(attributes[i]); std::vector majorSkills = mCreateClassDialog->getMajorSkills(); std::vector minorSkills = mCreateClassDialog->getMinorSkills(); - assert(majorSkills.size() >= klass.mData.mSkills.size()); - assert(minorSkills.size() >= klass.mData.mSkills.size()); - for (size_t i = 0; i < klass.mData.mSkills.size(); ++i) + assert(majorSkills.size() >= createdClass.mData.mSkills.size()); + assert(minorSkills.size() >= createdClass.mData.mSkills.size()); + for (size_t i = 0; i < createdClass.mData.mSkills.size(); ++i) { - klass.mData.mSkills[i][1] = ESM::Skill::refIdToIndex(majorSkills[i]); - klass.mData.mSkills[i][0] = ESM::Skill::refIdToIndex(minorSkills[i]); + createdClass.mData.mSkills[i][1] = ESM::Skill::refIdToIndex(majorSkills[i]); + createdClass.mData.mSkills[i][0] = ESM::Skill::refIdToIndex(minorSkills[i]); } - MWBase::Environment::get().getMechanicsManager()->setPlayerClass(klass); - mPlayerClass = klass; + MWBase::Environment::get().getMechanicsManager()->setPlayerClass(createdClass); + mPlayerClass = std::move(createdClass); // Do not delete dialog, so that choices are remembered in case we want to go back and adjust them later mCreateClassDialog->setVisible(false); @@ -666,9 +666,10 @@ namespace MWGui MWBase::Environment::get().getMechanicsManager()->setPlayerClass(mGenerateClass); - const ESM::Class* klass = MWBase::Environment::get().getESMStore()->get().find(mGenerateClass); + const ESM::Class* generatedClass + = MWBase::Environment::get().getESMStore()->get().find(mGenerateClass); - mPlayerClass = *klass; + mPlayerClass = *generatedClass; } void CharacterCreation::onGenerateClassBack() diff --git a/apps/openmw/mwgui/class.cpp b/apps/openmw/mwgui/class.cpp index d6b4e7f635..839f0f5072 100644 --- a/apps/openmw/mwgui/class.cpp +++ b/apps/openmw/mwgui/class.cpp @@ -248,27 +248,27 @@ namespace MWGui if (mCurrentClassId.empty()) return; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - const ESM::Class* klass = store.get().search(mCurrentClassId); - if (!klass) + const ESM::Class* currentClass = store.get().search(mCurrentClassId); + if (!currentClass) return; ESM::Class::Specialization specialization - = static_cast(klass->mData.mSpecialization); + = static_cast(currentClass->mData.mSpecialization); std::string specName{ MWBase::Environment::get().getWindowManager()->getGameSettingString( ESM::Class::sGmstSpecializationIds[specialization], ESM::Class::sGmstSpecializationIds[specialization]) }; mSpecializationName->setCaption(specName); ToolTips::createSpecializationToolTip(mSpecializationName, specName, specialization); - mFavoriteAttribute[0]->setAttributeId(ESM::Attribute::indexToRefId(klass->mData.mAttribute[0])); - mFavoriteAttribute[1]->setAttributeId(ESM::Attribute::indexToRefId(klass->mData.mAttribute[1])); + mFavoriteAttribute[0]->setAttributeId(ESM::Attribute::indexToRefId(currentClass->mData.mAttribute[0])); + mFavoriteAttribute[1]->setAttributeId(ESM::Attribute::indexToRefId(currentClass->mData.mAttribute[1])); ToolTips::createAttributeToolTip(mFavoriteAttribute[0], mFavoriteAttribute[0]->getAttributeId()); ToolTips::createAttributeToolTip(mFavoriteAttribute[1], mFavoriteAttribute[1]->getAttributeId()); - for (size_t i = 0; i < klass->mData.mSkills.size(); ++i) + for (size_t i = 0; i < currentClass->mData.mSkills.size(); ++i) { - ESM::RefId minor = ESM::Skill::indexToRefId(klass->mData.mSkills[i][0]); - ESM::RefId major = ESM::Skill::indexToRefId(klass->mData.mSkills[i][1]); + ESM::RefId minor = ESM::Skill::indexToRefId(currentClass->mData.mSkills[i][0]); + ESM::RefId major = ESM::Skill::indexToRefId(currentClass->mData.mSkills[i][1]); mMinorSkill[i]->setSkillId(minor); mMajorSkill[i]->setSkillId(major); ToolTips::createSkillToolTip(mMinorSkill[i], minor); diff --git a/apps/openmw/mwgui/console.cpp b/apps/openmw/mwgui/console.cpp index d4553b9664..b430e08142 100644 --- a/apps/openmw/mwgui/console.cpp +++ b/apps/openmw/mwgui/console.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include #include @@ -20,6 +19,8 @@ #include #include +#include "apps/openmw/mwgui/textcolours.hpp" + #include "../mwscript/extensions.hpp" #include "../mwscript/interpretercontext.hpp" @@ -439,7 +440,7 @@ namespace MWGui // If new search term reset position, otherwise continue from current position if (newSearchTerm != mCurrentSearchTerm) { - mCurrentSearchTerm = newSearchTerm; + mCurrentSearchTerm = std::move(newSearchTerm); mCurrentOccurrenceIndex = std::string::npos; } diff --git a/apps/openmw/mwgui/containeritemmodel.cpp b/apps/openmw/mwgui/containeritemmodel.cpp index ba4f7156c9..af1b585cff 100644 --- a/apps/openmw/mwgui/containeritemmodel.cpp +++ b/apps/openmw/mwgui/containeritemmodel.cpp @@ -129,7 +129,7 @@ namespace MWGui { if (stacks(*it, item.mBase)) { - int quantity = it->mRef->mData.getCount(false); + int quantity = it->mRef->mRef.getCount(false); // If this is a restocking quantity, just don't remove it if (quantity < 0 && mTrading) toRemove += quantity; @@ -144,11 +144,11 @@ namespace MWGui { if (stacks(source, item.mBase)) { - int refCount = source.getRefData().getCount(); + int refCount = source.getCellRef().getCount(); if (refCount - toRemove <= 0) MWBase::Environment::get().getWorld()->deleteObject(source); else - source.getRefData().setCount(std::max(0, refCount - toRemove)); + source.getCellRef().setCount(std::max(0, refCount - toRemove)); toRemove -= refCount; if (toRemove <= 0) return; @@ -176,7 +176,7 @@ namespace MWGui if (stacks(*it, itemStack.mBase)) { // we already have an item stack of this kind, add to it - itemStack.mCount += it->getRefData().getCount(); + itemStack.mCount += it->getCellRef().getCount(); found = true; break; } @@ -185,7 +185,7 @@ namespace MWGui if (!found) { // no stack yet, create one - ItemStack newItem(*it, this, it->getRefData().getCount()); + ItemStack newItem(*it, this, it->getCellRef().getCount()); mItems.push_back(newItem); } } @@ -198,7 +198,7 @@ namespace MWGui if (stacks(source, itemStack.mBase)) { // we already have an item stack of this kind, add to it - itemStack.mCount += source.getRefData().getCount(); + itemStack.mCount += source.getCellRef().getCount(); found = true; break; } @@ -207,7 +207,7 @@ namespace MWGui if (!found) { // no stack yet, create one - ItemStack newItem(source, this, source.getRefData().getCount()); + ItemStack newItem(source, this, source.getCellRef().getCount()); mItems.push_back(newItem); } } diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 79673463ef..0e44b8c03e 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -23,9 +23,11 @@ #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/player.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/npcstats.hpp" #include "bookpage.hpp" #include "textcolours.hpp" @@ -736,6 +738,15 @@ namespace MWGui bool dispositionVisible = false; if (!mPtr.isEmpty() && mPtr.getClass().isNpc()) { + // If actor was a witness to a crime which was payed off, + // restore original disposition immediately. + MWMechanics::NpcStats& npcStats = mPtr.getClass().getNpcStats(mPtr); + if (npcStats.getCrimeId() != -1 && npcStats.getCrimeDispositionModifier() != 0) + { + if (npcStats.getCrimeId() <= MWBase::Environment::get().getWorld()->getPlayer().getCrimeId()) + npcStats.setCrimeDispositionModifier(0); + } + dispositionVisible = true; mDispositionBar->setProgressRange(100); mDispositionBar->setProgressPosition( diff --git a/apps/openmw/mwgui/draganddrop.cpp b/apps/openmw/mwgui/draganddrop.cpp index c99e97e37d..0fa2cc4e21 100644 --- a/apps/openmw/mwgui/draganddrop.cpp +++ b/apps/openmw/mwgui/draganddrop.cpp @@ -126,7 +126,7 @@ namespace MWGui void DragAndDrop::onFrame() { - if (mIsOnDragAndDrop && mItem.mBase.getRefData().getCount() == 0) + if (mIsOnDragAndDrop && mItem.mBase.getCellRef().getCount() == 0) finish(); } diff --git a/apps/openmw/mwgui/enchantingdialog.cpp b/apps/openmw/mwgui/enchantingdialog.cpp index f5bcb1fb5f..8264dd60b6 100644 --- a/apps/openmw/mwgui/enchantingdialog.cpp +++ b/apps/openmw/mwgui/enchantingdialog.cpp @@ -373,7 +373,7 @@ namespace MWGui { MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("enchant fail")); MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage34}"); - if (!mEnchanting.getGem().isEmpty() && !mEnchanting.getGem().getRefData().getCount()) + if (!mEnchanting.getGem().isEmpty() && !mEnchanting.getGem().getCellRef().getCount()) { setSoulGem(MWWorld::Ptr()); mEnchanting.nextCastStyle(); diff --git a/apps/openmw/mwgui/formatting.cpp b/apps/openmw/mwgui/formatting.cpp index 7f62bbf49c..b2d9415897 100644 --- a/apps/openmw/mwgui/formatting.cpp +++ b/apps/openmw/mwgui/formatting.cpp @@ -221,7 +221,7 @@ namespace MWGui::Formatting } } - mAttributes[key] = value; + mAttributes[key] = std::move(value); } } diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index bd38174183..1c8aad5447 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -423,17 +423,21 @@ namespace MWGui mSpellBox->setUserString("Spell", spellId.serialize()); mSpellBox->setUserData(MyGUI::Any::Null); - // use the icon of the first effect - const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find( - spell->mEffects.mList.front().mEffectID); - - std::string icon = effect->mIcon; - std::replace(icon.begin(), icon.end(), '/', '\\'); - int slashPos = icon.rfind('\\'); - icon.insert(slashPos + 1, "b_"); - icon = Misc::ResourceHelpers::correctIconPath(icon, MWBase::Environment::get().getResourceSystem()->getVFS()); - - mSpellImage->setSpellIcon(icon); + if (!spell->mEffects.mList.empty()) + { + // use the icon of the first effect + const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find( + spell->mEffects.mList.front().mEffectID); + std::string icon = effect->mIcon; + std::replace(icon.begin(), icon.end(), '/', '\\'); + size_t slashPos = icon.rfind('\\'); + icon.insert(slashPos + 1, "b_"); + icon = Misc::ResourceHelpers::correctIconPath( + icon, MWBase::Environment::get().getResourceSystem()->getVFS()); + mSpellImage->setSpellIcon(icon); + } + else + mSpellImage->setSpellIcon({}); } void HUD::setSelectedEnchantItem(const MWWorld::Ptr& item, int chargePercent) diff --git a/apps/openmw/mwgui/inventoryitemmodel.cpp b/apps/openmw/mwgui/inventoryitemmodel.cpp index e20637d58c..b52d49b7c2 100644 --- a/apps/openmw/mwgui/inventoryitemmodel.cpp +++ b/apps/openmw/mwgui/inventoryitemmodel.cpp @@ -115,7 +115,7 @@ namespace MWGui if (!item.getClass().showsInInventory(item)) continue; - ItemStack newItem(item, this, item.getRefData().getCount()); + ItemStack newItem(item, this, item.getCellRef().getCount()); if (mActor.getClass().hasInventoryStore(mActor)) { diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index 932723c3fd..0fbd15dda2 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -560,7 +560,7 @@ namespace MWGui if (mEquippedStackableCount.has_value()) { // the count to unequip - int count = ptr.getRefData().getCount() - mDragAndDrop->mDraggedCount - mEquippedStackableCount.value(); + int count = ptr.getCellRef().getCount() - mDragAndDrop->mDraggedCount - mEquippedStackableCount.value(); if (count > 0) { MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); @@ -604,7 +604,7 @@ namespace MWGui // Save the currently equipped count before useItem() if (slotIt != invStore.end() && slotIt->getCellRef().getRefId() == ptr.getCellRef().getRefId()) - mEquippedStackableCount = slotIt->getRefData().getCount(); + mEquippedStackableCount = slotIt->getCellRef().getCount(); else mEquippedStackableCount = 0; } @@ -735,7 +735,7 @@ namespace MWGui if (!object.getClass().hasToolTip(object)) return; - int count = object.getRefData().getCount(); + int count = object.getCellRef().getCount(); if (object.getClass().isGold(object)) count *= object.getClass().getValue(object); @@ -755,8 +755,7 @@ namespace MWGui // add to player inventory // can't use ActionTake here because we need an MWWorld::Ptr to the newly inserted object - MWWorld::Ptr newObject - = *player.getClass().getContainerStore(player).add(object, object.getRefData().getCount()); + MWWorld::Ptr newObject = *player.getClass().getContainerStore(player).add(object, count); // remove from world MWBase::Environment::get().getWorld()->deleteObject(object); diff --git a/apps/openmw/mwgui/itemmodel.cpp b/apps/openmw/mwgui/itemmodel.cpp index 4bdd2399fe..a4cf48fcbe 100644 --- a/apps/openmw/mwgui/itemmodel.cpp +++ b/apps/openmw/mwgui/itemmodel.cpp @@ -58,7 +58,7 @@ namespace MWGui MWWorld::Ptr ItemModel::moveItem(const ItemStack& item, size_t count, ItemModel* otherModel, bool allowAutoEquip) { MWWorld::Ptr ret = MWWorld::Ptr(); - if (static_cast(item.mBase.getRefData().getCount()) <= count) + if (static_cast(item.mBase.getCellRef().getCount()) <= count) { // We are moving the full stack ret = otherModel->addItem(item, count, allowAutoEquip); diff --git a/apps/openmw/mwgui/itemwidget.cpp b/apps/openmw/mwgui/itemwidget.cpp index 5ee74c6e87..05fff2d40f 100644 --- a/apps/openmw/mwgui/itemwidget.cpp +++ b/apps/openmw/mwgui/itemwidget.cpp @@ -202,7 +202,7 @@ namespace MWGui setIcon(ptr); } - void SpellWidget::setSpellIcon(const std::string& icon) + void SpellWidget::setSpellIcon(std::string_view icon) { if (mFrame && !mCurrentFrame.empty()) { diff --git a/apps/openmw/mwgui/itemwidget.hpp b/apps/openmw/mwgui/itemwidget.hpp index 29b0063203..63837ae92f 100644 --- a/apps/openmw/mwgui/itemwidget.hpp +++ b/apps/openmw/mwgui/itemwidget.hpp @@ -58,7 +58,7 @@ namespace MWGui { MYGUI_RTTI_DERIVED(SpellWidget) public: - void setSpellIcon(const std::string& icon); + void setSpellIcon(std::string_view icon); }; } diff --git a/apps/openmw/mwgui/mainmenu.cpp b/apps/openmw/mwgui/mainmenu.cpp index 37e835f1a4..d0c55f432e 100644 --- a/apps/openmw/mwgui/mainmenu.cpp +++ b/apps/openmw/mwgui/mainmenu.cpp @@ -18,6 +18,7 @@ #include "backgroundimage.hpp" #include "confirmationdialog.hpp" #include "savegamedialog.hpp" +#include "settingswindow.hpp" #include "videowidget.hpp" namespace MWGui @@ -97,7 +98,9 @@ namespace MWGui winMgr->removeGuiMode(GM_MainMenu); } else if (name == "options") - winMgr->pushGuiMode(GM_Settings); + { + winMgr->getSettingsWindow()->setVisible(true); + } else if (name == "credits") winMgr->playVideo("mw_credits.bik", true); else if (name == "exitgame") diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index 6d79b5f9d7..cb6ba79f9e 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -300,11 +300,9 @@ namespace MWGui return MyGUI::IntCoord(position.left - halfMarkerSize, position.top - halfMarkerSize, markerSize, markerSize); } - MyGUI::Widget* LocalMapBase::createDoorMarker( - const std::string& name, const MyGUI::VectorString& notes, float x, float y) const + MyGUI::Widget* LocalMapBase::createDoorMarker(const std::string& name, float x, float y) const { MarkerUserData data(mLocalMapRender); - data.notes = notes; data.caption = name; MarkerWidget* markerWidget = mLocalMap->createWidget( "MarkerButton", getMarkerCoordinates(x, y, data, 8), MyGUI::Align::Default); @@ -662,8 +660,9 @@ namespace MWGui MarkerUserData* data; if (mDoorMarkersToRecycle.empty()) { - markerWidget = createDoorMarker(marker.name, destNotes, marker.x, marker.y); + markerWidget = createDoorMarker(marker.name, marker.x, marker.y); data = markerWidget->getUserData(); + data->notes = std::move(destNotes); doorMarkerCreated(markerWidget); } else @@ -672,7 +671,7 @@ namespace MWGui mDoorMarkersToRecycle.pop_back(); data = markerWidget->getUserData(); - data->notes = destNotes; + data->notes = std::move(destNotes); data->caption = marker.name; markerWidget->setCoord(getMarkerCoordinates(marker.x, marker.y, *data, 8)); markerWidget->setVisible(true); diff --git a/apps/openmw/mwgui/mapwindow.hpp b/apps/openmw/mwgui/mapwindow.hpp index 5afc8c7c8a..29759a4365 100644 --- a/apps/openmw/mwgui/mapwindow.hpp +++ b/apps/openmw/mwgui/mapwindow.hpp @@ -170,8 +170,7 @@ namespace MWGui MyGUI::IntPoint getMarkerPosition(float worldX, float worldY, MarkerUserData& markerPos) const; MyGUI::IntCoord getMarkerCoordinates( float worldX, float worldY, MarkerUserData& markerPos, size_t markerSize) const; - MyGUI::Widget* createDoorMarker( - const std::string& name, const MyGUI::VectorString& notes, float x, float y) const; + MyGUI::Widget* createDoorMarker(const std::string& name, float x, float y) const; MyGUI::IntCoord getMarkerCoordinates(MyGUI::Widget* widget, size_t markerSize) const; virtual void notifyPlayerUpdate() {} diff --git a/apps/openmw/mwgui/mode.hpp b/apps/openmw/mwgui/mode.hpp index 63f81e8b47..44f9743743 100644 --- a/apps/openmw/mwgui/mode.hpp +++ b/apps/openmw/mwgui/mode.hpp @@ -6,7 +6,6 @@ namespace MWGui enum GuiMode { GM_None, - GM_Settings, // Settings window GM_Inventory, // Inventory mode GM_Container, GM_Companion, diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index fb03ab99c9..204fa00492 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -85,7 +85,7 @@ namespace MWGui { MWWorld::Ptr item = *mKey[index].button->getUserData(); // Make sure the item is available and is not broken - if (item.isEmpty() || item.getRefData().getCount() < 1 + if (item.isEmpty() || item.getCellRef().getCount() < 1 || (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) { // Try searching for a compatible replacement @@ -383,12 +383,12 @@ namespace MWGui item = nullptr; // check the item is available and not broken - if (item.isEmpty() || item.getRefData().getCount() < 1 + if (item.isEmpty() || item.getCellRef().getCount() < 1 || (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) { item = store.findReplacement(key->id); - if (item.isEmpty() || item.getRefData().getCount() < 1) + if (item.isEmpty() || item.getCellRef().getCount() < 1) { MWBase::Environment::get().getWindowManager()->messageBox("#{sQuickMenu5} " + key->name); diff --git a/apps/openmw/mwgui/recharge.cpp b/apps/openmw/mwgui/recharge.cpp index c2acea93ce..7d57988d97 100644 --- a/apps/openmw/mwgui/recharge.cpp +++ b/apps/openmw/mwgui/recharge.cpp @@ -75,7 +75,7 @@ namespace MWGui mChargeLabel->setCaptionWithReplacing("#{sCharges} " + MyGUI::utility::toString(creature->mData.mSoul)); - bool toolBoxVisible = (gem.getRefData().getCount() != 0); + bool toolBoxVisible = gem.getCellRef().getCount() != 0; mGemBox->setVisible(toolBoxVisible); mGemBox->setUserString("Hidden", toolBoxVisible ? "false" : "true"); diff --git a/apps/openmw/mwgui/referenceinterface.cpp b/apps/openmw/mwgui/referenceinterface.cpp index de7c93d862..6dad6b8543 100644 --- a/apps/openmw/mwgui/referenceinterface.cpp +++ b/apps/openmw/mwgui/referenceinterface.cpp @@ -2,14 +2,14 @@ namespace MWGui { - ReferenceInterface::ReferenceInterface() {} + ReferenceInterface::ReferenceInterface() = default; - ReferenceInterface::~ReferenceInterface() {} + ReferenceInterface::~ReferenceInterface() = default; void ReferenceInterface::checkReferenceAvailable() { // check if count of the reference has become 0 - if (!mPtr.isEmpty() && mPtr.getRefData().getCount() == 0) + if (!mPtr.isEmpty() && mPtr.getCellRef().getCount() == 0) { mPtr = MWWorld::Ptr(); onReferenceUnavailable(); diff --git a/apps/openmw/mwgui/repair.cpp b/apps/openmw/mwgui/repair.cpp index 63b51d7d2c..c1602b8407 100644 --- a/apps/openmw/mwgui/repair.cpp +++ b/apps/openmw/mwgui/repair.cpp @@ -86,7 +86,7 @@ namespace MWGui mUsesLabel->setCaptionWithReplacing("#{sUses} " + MyGUI::utility::toString(uses)); mQualityLabel->setCaptionWithReplacing("#{sQuality} " + qualityStr.str()); - bool toolBoxVisible = (mRepair.getTool().getRefData().getCount() != 0); + bool toolBoxVisible = (mRepair.getTool().getCellRef().getCount() != 0); mToolBox->setVisible(toolBoxVisible); mToolBox->setUserString("Hidden", toolBoxVisible ? "false" : "true"); @@ -142,7 +142,7 @@ namespace MWGui void Repair::onRepairItem(MyGUI::Widget* /*sender*/, const MWWorld::Ptr& ptr) { - if (!mRepair.getTool().getRefData().getCount()) + if (!mRepair.getTool().getCellRef().getCount()) return; mRepair.repair(ptr); diff --git a/apps/openmw/mwgui/review.cpp b/apps/openmw/mwgui/review.cpp index 4ea21df00c..ddce2c5f50 100644 --- a/apps/openmw/mwgui/review.cpp +++ b/apps/openmw/mwgui/review.cpp @@ -148,11 +148,11 @@ namespace MWGui mUpdateSkillArea = true; } - void ReviewDialog::setClass(const ESM::Class& class_) + void ReviewDialog::setClass(const ESM::Class& playerClass) { - mKlass = class_; - mClassWidget->setCaption(mKlass.mName); - ToolTips::createClassToolTip(mClassWidget, mKlass); + mClass = playerClass; + mClassWidget->setCaption(mClass.mName); + ToolTips::createClassToolTip(mClassWidget, mClass); } void ReviewDialog::setBirthSign(const ESM::RefId& signId) diff --git a/apps/openmw/mwgui/review.hpp b/apps/openmw/mwgui/review.hpp index 6f594c60f0..7226ad628d 100644 --- a/apps/openmw/mwgui/review.hpp +++ b/apps/openmw/mwgui/review.hpp @@ -30,7 +30,7 @@ namespace MWGui void setPlayerName(const std::string& name); void setRace(const ESM::RefId& raceId); - void setClass(const ESM::Class& class_); + void setClass(const ESM::Class& playerClass); void setBirthSign(const ESM::RefId& signId); void setHealth(const MWMechanics::DynamicStat& value); @@ -96,7 +96,7 @@ namespace MWGui std::map mSkillWidgetMap; ESM::RefId mRaceId, mBirthSignId; std::string mName; - ESM::Class mKlass; + ESM::Class mClass; std::vector mSkillWidgets; //< Skills and other information bool mUpdateSkillArea; diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 1060b3a20f..fbd54586df 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -240,7 +240,7 @@ namespace MWGui } SettingsWindow::SettingsWindow() - : WindowBase("openmw_settings_window.layout") + : WindowModal("openmw_settings_window.layout") , mKeyboardMode(true) , mCurrentPage(-1) { @@ -450,7 +450,7 @@ namespace MWGui void SettingsWindow::onOkButtonClicked(MyGUI::Widget* _sender) { - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Settings); + setVisible(false); } void SettingsWindow::onResolutionSelected(MyGUI::ListBox* _sender, size_t index) @@ -1041,6 +1041,8 @@ namespace MWGui void SettingsWindow::onOpen() { + WindowModal::onOpen(); + highlightCurrentResolution(); updateControlsBox(); updateLightSettings(); diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index 1f96f7de54..47951ef121 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -7,7 +7,7 @@ namespace MWGui { - class SettingsWindow : public WindowBase + class SettingsWindow : public WindowModal { public: SettingsWindow(); diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 1618f34a7a..d668db1dec 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -119,7 +119,7 @@ namespace MWGui void EditEffectDialog::newEffect(const ESM::MagicEffect* effect) { - bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0; + bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0 || mConstantEffect; bool allowTouch = (effect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; setMagicEffect(effect); @@ -240,7 +240,7 @@ namespace MWGui // cycle through range types until we find something that's allowed // does not handle the case where nothing is allowed (this should be prevented before opening the Add Effect // dialog) - bool allowSelf = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0; + bool allowSelf = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0 || mConstantEffect; bool allowTouch = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; bool allowTarget = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTarget) && !mConstantEffect; if (mEffect.mRange == ESM::RT_Self && !allowSelf) @@ -472,7 +472,7 @@ namespace MWGui ESM::EffectList effectList; effectList.mList = mEffects; - mSpell.mEffects = effectList; + mSpell.mEffects = std::move(effectList); mSpell.mData.mCost = int(y); mSpell.mData.mType = ESM::Spell::ST_Spell; mSpell.mData.mFlags = 0; @@ -629,7 +629,7 @@ namespace MWGui const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find(mSelectedKnownEffectId); - bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0; + bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0 || mConstantEffect; bool allowTouch = (effect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; bool allowTarget = (effect->mData.mFlags & ESM::MagicEffect::CastTarget) && !mConstantEffect; @@ -751,25 +751,9 @@ namespace MWGui void EffectEditorBase::setConstantEffect(bool constant) { mAddEffectDialog.setConstantEffect(constant); + if (!mConstantEffect && constant) + for (ESM::ENAMstruct& effect : mEffects) + effect.mRange = ESM::RT_Self; mConstantEffect = constant; - - if (!constant) - return; - - for (auto it = mEffects.begin(); it != mEffects.end();) - { - if (it->mRange != ESM::RT_Self) - { - auto& store = *MWBase::Environment::get().getESMStore(); - auto magicEffect = store.get().find(it->mEffectID); - if ((magicEffect->mData.mFlags & ESM::MagicEffect::CastSelf) == 0) - { - it = mEffects.erase(it); - continue; - } - it->mRange = ESM::RT_Self; - } - ++it; - } } } diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index 5337e2f798..aa29dfc156 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -172,7 +172,7 @@ namespace MWGui w += 16; ToolTipInfo* tooltipInfo = image->getUserData(); - tooltipInfo->text = sourcesDescription; + tooltipInfo->text = std::move(sourcesDescription); // Fade out if (totalDuration >= fadeTime && fadeTime > 0.f) diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index f340d072e0..385464da24 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -137,7 +137,7 @@ namespace MWGui newSpell.mItem = item; newSpell.mId = item.getCellRef().getRefId(); newSpell.mName = item.getClass().getName(item); - newSpell.mCount = item.getRefData().getCount(); + newSpell.mCount = item.getCellRef().getCount(); newSpell.mType = Spell::Type_EnchantedItem; newSpell.mSelected = invStore.getSelectedEnchantItem() == it; diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index bdcc4e76d7..0a0343831d 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -120,7 +120,7 @@ namespace MWGui tooltipSize = createToolTip(info, checkOwned()); } else - tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), true); + tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), true); MyGUI::IntPoint tooltipPosition = MyGUI::InputManager::getInstance().getMousePosition(); position(tooltipPosition, tooltipSize, viewSize); @@ -187,7 +187,7 @@ namespace MWGui if (mFocusObject.isEmpty()) return; - tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), false, checkOwned()); + tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), false, checkOwned()); } else if (type == "ItemModelIndex") { @@ -211,7 +211,7 @@ namespace MWGui mFocusObject = item; if (!mFocusObject.isEmpty()) - tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), false); + tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), false); } else if (type == "Spell") { @@ -237,19 +237,21 @@ namespace MWGui params.mNoTarget = false; effects.push_back(params); } - if (MWMechanics::spellIncreasesSkill( - spell)) // display school of spells that contribute to skill progress + // display school of spells that contribute to skill progress + if (MWMechanics::spellIncreasesSkill(spell)) { - MWWorld::Ptr player = MWMechanics::getPlayer(); - const auto& school - = store->get().find(MWMechanics::getSpellSchool(spell, player))->mSchool; - info.text = "#{sSchool}: " + MyGUI::TextIterator::toTagsString(school->mName).asUTF8(); + ESM::RefId id = MWMechanics::getSpellSchool(spell, MWMechanics::getPlayer()); + if (!id.empty()) + { + const auto& school = store->get().find(id)->mSchool; + info.text = "#{sSchool}: " + MyGUI::TextIterator::toTagsString(school->mName).asUTF8(); + } } auto cost = focus->getUserString("SpellCost"); if (!cost.empty() && cost != "0") info.text += MWGui::ToolTips::getValueString(MWMechanics::calcSpellCost(*spell), "#{sCastCost}"); - info.effects = effects; + info.effects = std::move(effects); tooltipSize = createToolTip(info); } else if (type == "Layout") @@ -304,7 +306,7 @@ namespace MWGui { if (!mFocusObject.isEmpty()) { - MyGUI::IntSize tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), true, checkOwned()); + MyGUI::IntSize tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), true, checkOwned()); setCoord(viewSize.width / 2 - tooltipSize.width / 2, std::max(0, int(mFocusToolTipY * viewSize.height - tooltipSize.height)), tooltipSize.width, @@ -479,10 +481,13 @@ namespace MWGui MyGUI::IntCoord(padding.left + 8 + 4, totalSize.height + padding.top, 300 - padding.left - 8 - 4, 300 - totalSize.height), MyGUI::Align::Default); - edit->setEditMultiLine(true); - edit->setEditWordWrap(true); - edit->setCaption(note); - edit->setSize(edit->getWidth(), edit->getTextSize().height); + constexpr size_t maxLength = 60; + std::string shortenedNote = note.substr(0, std::min(maxLength, note.find('\n'))); + if (shortenedNote.size() < note.size()) + shortenedNote += " ..."; + edit->setCaption(shortenedNote); + MyGUI::IntSize noteTextSize = edit->getTextSize(); + edit->setSize(std::max(edit->getWidth(), noteTextSize.width), noteTextSize.height); icon->setPosition(icon->getLeft(), (edit->getTop() + edit->getBottom()) / 2 - icon->getHeight() / 2); totalSize.height += std::max(edit->getHeight(), icon->getHeight()); totalSize.width = std::max(totalSize.width, edit->getWidth() + 8 + 4); diff --git a/apps/openmw/mwgui/trainingwindow.cpp b/apps/openmw/mwgui/trainingwindow.cpp index c915619a1a..5395f6db1c 100644 --- a/apps/openmw/mwgui/trainingwindow.cpp +++ b/apps/openmw/mwgui/trainingwindow.cpp @@ -167,7 +167,7 @@ namespace MWGui // You can not train a skill above its governing attribute if (pcStats.getSkill(skill->mId).getBase() - >= pcStats.getAttribute(ESM::Attribute::indexToRefId(skill->mData.mAttribute)).getBase()) + >= pcStats.getAttribute(ESM::Attribute::indexToRefId(skill->mData.mAttribute)).getModified()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage17}"); return; diff --git a/apps/openmw/mwgui/windowbase.cpp b/apps/openmw/mwgui/windowbase.cpp index 4c191eaeb8..a680e38cf8 100644 --- a/apps/openmw/mwgui/windowbase.cpp +++ b/apps/openmw/mwgui/windowbase.cpp @@ -58,7 +58,7 @@ void WindowBase::setVisible(bool visible) onClose(); } -bool WindowBase::isVisible() +bool WindowBase::isVisible() const { return mMainWidget->getVisible(); } diff --git a/apps/openmw/mwgui/windowbase.hpp b/apps/openmw/mwgui/windowbase.hpp index 88b46b0bd2..54fb269305 100644 --- a/apps/openmw/mwgui/windowbase.hpp +++ b/apps/openmw/mwgui/windowbase.hpp @@ -37,7 +37,7 @@ namespace MWGui /// Sets the visibility of the window void setVisible(bool visible) override; /// Returns the visibility state of the window - bool isVisible(); + bool isVisible() const; void center(); diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 3bd779a186..63d974ff35 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -408,7 +408,6 @@ namespace MWGui mSettingsWindow = settingsWindow.get(); mWindows.push_back(std::move(settingsWindow)); trackWindow(mSettingsWindow, makeSettingsWindowSettingValues()); - mGuiModeStates[GM_Settings] = GuiModeState(mSettingsWindow); auto confirmationDialog = std::make_unique(); mConfirmationDialog = confirmationDialog.get(); @@ -1475,6 +1474,10 @@ namespace MWGui { return mPostProcessorHud; } + MWGui::SettingsWindow* WindowManager::getSettingsWindow() + { + return mSettingsWindow; + } void WindowManager::useItem(const MWWorld::Ptr& item, bool bypassBeastRestrictions) { diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 7ee0554a26..b378a76ff8 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -182,6 +182,7 @@ namespace MWGui MWGui::ConfirmationDialog* getConfirmationDialog() override; MWGui::TradeWindow* getTradeWindow() override; MWGui::PostProcessorHud* getPostProcessorHud() override; + MWGui::SettingsWindow* getSettingsWindow() override; /// Make the player use an item, while updating GUI state accordingly void useItem(const MWWorld::Ptr& item, bool bypassBeastRestrictions = false) override; diff --git a/apps/openmw/mwinput/controllermanager.cpp b/apps/openmw/mwinput/controllermanager.cpp index 7054f72c8f..8e6496ddf1 100644 --- a/apps/openmw/mwinput/controllermanager.cpp +++ b/apps/openmw/mwinput/controllermanager.cpp @@ -34,16 +34,27 @@ namespace MWInput { if (!controllerBindingsFile.empty()) { - SDL_GameControllerAddMappingsFromFile(Files::pathToUnicodeString(controllerBindingsFile).c_str()); + const int result + = SDL_GameControllerAddMappingsFromFile(Files::pathToUnicodeString(controllerBindingsFile).c_str()); + if (result < 0) + Log(Debug::Error) << "Failed to add game controller mappings from file \"" << controllerBindingsFile + << "\": " << SDL_GetError(); } if (!userControllerBindingsFile.empty()) { - SDL_GameControllerAddMappingsFromFile(Files::pathToUnicodeString(userControllerBindingsFile).c_str()); + const int result + = SDL_GameControllerAddMappingsFromFile(Files::pathToUnicodeString(userControllerBindingsFile).c_str()); + if (result < 0) + Log(Debug::Error) << "Failed to add game controller mappings from user file \"" + << userControllerBindingsFile << "\": " << SDL_GetError(); } // Open all presently connected sticks - int numSticks = SDL_NumJoysticks(); + const int numSticks = SDL_NumJoysticks(); + if (numSticks < 0) + Log(Debug::Error) << "Failed to get number of joysticks: " << SDL_GetError(); + for (int i = 0; i < numSticks; i++) { if (SDL_IsGameController(i)) @@ -52,11 +63,17 @@ namespace MWInput evt.which = i; static const int fakeDeviceID = 1; ControllerManager::controllerAdded(fakeDeviceID, evt); - Log(Debug::Info) << "Detected game controller: " << SDL_GameControllerNameForIndex(i); + if (const char* name = SDL_GameControllerNameForIndex(i)) + Log(Debug::Info) << "Detected game controller: " << name; + else + Log(Debug::Warning) << "Detected game controller without a name: " << SDL_GetError(); } else { - Log(Debug::Info) << "Detected unusable controller: " << SDL_JoystickNameForIndex(i); + if (const char* name = SDL_JoystickNameForIndex(i)) + Log(Debug::Info) << "Detected unusable controller: " << name; + else + Log(Debug::Warning) << "Detected unusable controller without a name: " << SDL_GetError(); } } @@ -77,7 +94,7 @@ namespace MWInput // We keep track of our own mouse position, so that moving the mouse while in // game mode does not move the position of the GUI cursor float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); - const float gamepadCursorSpeed = Settings::input().mEnableController; + const float gamepadCursorSpeed = Settings::input().mGamepadCursorSpeed; const float xMove = xAxis * dt * 1500.0f / uiScale * gamepadCursorSpeed; const float yMove = yAxis * dt * 1500.0f / uiScale * gamepadCursorSpeed; @@ -336,8 +353,11 @@ namespace MWInput return; if (!SDL_GameControllerHasSensor(cntrl, SDL_SENSOR_GYRO)) return; - if (SDL_GameControllerSetSensorEnabled(cntrl, SDL_SENSOR_GYRO, SDL_TRUE) < 0) + if (const int result = SDL_GameControllerSetSensorEnabled(cntrl, SDL_SENSOR_GYRO, SDL_TRUE); result < 0) + { + Log(Debug::Error) << "Failed to enable game controller sensor: " << SDL_GetError(); return; + } mGyroAvailable = true; #endif } @@ -353,7 +373,11 @@ namespace MWInput #if SDL_VERSION_ATLEAST(2, 0, 14) SDL_GameController* cntrl = mBindingsManager->getControllerOrNull(); if (cntrl && mGyroAvailable) - SDL_GameControllerGetSensorData(cntrl, SDL_SENSOR_GYRO, gyro, 3); + { + const int result = SDL_GameControllerGetSensorData(cntrl, SDL_SENSOR_GYRO, gyro, 3); + if (result < 0) + Log(Debug::Error) << "Failed to get game controller sensor data: " << SDL_GetError(); + } #endif return std::array({ gyro[0], gyro[1], gyro[2] }); } diff --git a/apps/openmw/mwinput/mousemanager.cpp b/apps/openmw/mwinput/mousemanager.cpp index 363a78fadd..55b50b91ae 100644 --- a/apps/openmw/mwinput/mousemanager.cpp +++ b/apps/openmw/mwinput/mousemanager.cpp @@ -13,6 +13,8 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwgui/settingswindow.hpp" + #include "../mwworld/player.hpp" #include "actions.hpp" @@ -156,8 +158,9 @@ namespace MWInput // Don't trigger any mouse bindings while in settings menu, otherwise rebinding controls becomes impossible // Also do not trigger bindings when input controls are disabled, e.g. during save loading - if (MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Settings - && !input->controlsDisabled()) + const MWGui::SettingsWindow* settingsWindow + = MWBase::Environment::get().getWindowManager()->getSettingsWindow(); + if ((!settingsWindow || !settingsWindow->isVisible()) && !input->controlsDisabled()) mBindingsManager->mousePressed(arg, id); } diff --git a/apps/openmw/mwlua/cellbindings.cpp b/apps/openmw/mwlua/cellbindings.cpp index e43354bfaf..081df13a0e 100644 --- a/apps/openmw/mwlua/cellbindings.cpp +++ b/apps/openmw/mwlua/cellbindings.cpp @@ -43,6 +43,8 @@ #include #include +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/worldmodel.hpp" @@ -121,7 +123,7 @@ namespace MWLua cell.mStore->load(); ObjectIdList res = std::make_shared>(); auto visitor = [&](const MWWorld::Ptr& ptr) { - if (ptr.getRefData().isDeleted()) + if (ptr.mRef->isDeleted()) return true; MWBase::Environment::get().getWorldModel()->registerPtr(ptr); if (getLiveCellRefType(ptr.mRef) == ptr.getType()) diff --git a/apps/openmw/mwlua/factionbindings.cpp b/apps/openmw/mwlua/factionbindings.cpp index a43cec874f..87ce6ced39 100644 --- a/apps/openmw/mwlua/factionbindings.cpp +++ b/apps/openmw/mwlua/factionbindings.cpp @@ -122,7 +122,8 @@ namespace MWLua return "ESM3_FactionRank[" + rec.mFactionId.toDebugString() + ", " + std::to_string(rec.mRankIndex + 1) + "]"; }; - rankT["name"] = sol::readonly_property([](const FactionRank& rec) { return rec.mRankName; }); + rankT["name"] + = sol::readonly_property([](const FactionRank& rec) -> std::string_view { return rec.mRankName; }); rankT["primarySkillValue"] = sol::readonly_property([](const FactionRank& rec) { return rec.mPrimarySkill; }); rankT["favouredSkillValue"] = sol::readonly_property([](const FactionRank& rec) { return rec.mFavouredSkill; }); rankT["factionReaction"] = sol::readonly_property([](const FactionRank& rec) { return rec.mFactReaction; }); diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp index 414bb575b7..315b0db7bc 100644 --- a/apps/openmw/mwlua/inputbindings.cpp +++ b/apps/openmw/mwlua/inputbindings.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -11,6 +12,7 @@ #include "../mwbase/inputmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwinput/actions.hpp" +#include "luamanagerimp.hpp" namespace sol { @@ -18,6 +20,16 @@ namespace sol struct is_automagical : std::false_type { }; + + template <> + struct is_automagical : std::false_type + { + }; + + template <> + struct is_automagical : std::false_type + { + }; } namespace MWLua @@ -47,9 +59,130 @@ namespace MWLua touchpadEvent["pressure"] = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> float { return e.mPressure; }); + auto inputActions = context.mLua->sol().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()); + }; + inputActions[sol::meta_function::pairs] = pairs; + } + + auto actionInfo = context.mLua->sol().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; }); + + auto inputTriggers = context.mLua->sol().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()); + }; + inputTriggers[sol::meta_function::pairs] = pairs; + } + + auto triggerInfo = context.mLua->sol().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; }); + MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); sol::table api(context.mLua->sol(), sol::create); + api["ACTION_TYPE"] + = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ + { "Boolean", LuaUtil::InputAction::Type::Boolean }, + { "Number", LuaUtil::InputAction::Type::Number }, + { "Range", LuaUtil::InputAction::Type::Range }, + })); + + 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["bindAction"] = [manager = context.mLuaManager]( + std::string_view key, const sol::table& callback, sol::table dependencies) { + std::vector parsedDependencies; + parsedDependencies.reserve(dependencies.size()); + for (size_t i = 1; i <= dependencies.size(); ++i) + { + sol::object dependency = dependencies[i]; + if (!dependency.is()) + throw std::domain_error("The dependencies argument must be a list of Action keys"); + parsedDependencies.push_back(dependency.as()); + } + if (!manager->inputActions().bind(key, LuaUtil::Callback::fromLua(callback), parsedDependencies)) + throw std::domain_error("Cyclic action binding"); + }; + api["registerActionHandler"] + = [manager = context.mLuaManager](std::string_view key, const sol::table& callback) { + manager->inputActions().registerHandler(key, LuaUtil::Callback::fromLua(callback)); + }; + api["getBooleanActionValue"] = [manager = context.mLuaManager](std::string_view key) { + return manager->inputActions().valueOfType(key, LuaUtil::InputAction::Type::Boolean); + }; + api["getNumberActionValue"] = [manager = context.mLuaManager](std::string_view key) { + return manager->inputActions().valueOfType(key, LuaUtil::InputAction::Type::Number); + }; + api["getRangeActionValue"] = [manager = context.mLuaManager](std::string_view key) { + return manager->inputActions().valueOfType(key, LuaUtil::InputAction::Type::Range); + }; + + 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["registerTriggerHandler"] + = [manager = context.mLuaManager](std::string_view key, const sol::table& callback) { + manager->inputTriggers().registerHandler(key, LuaUtil::Callback::fromLua(callback)); + }; + api["activateTrigger"] + = [manager = context.mLuaManager](std::string_view key) { manager->inputTriggers().activate(key); }; + api["isIdle"] = [input]() { return input->isIdle(); }; api["isActionPressed"] = [input](int action) { return input->actionIsActive(action); }; api["isKeyPressed"] = [](SDL_Scancode code) -> bool { diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index a50459502b..48f2f3e35d 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -21,6 +21,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/action.hpp" #include "../mwworld/class.hpp" #include "../mwworld/datetimemanager.hpp" @@ -86,6 +87,7 @@ namespace MWLua api["getRealTime"] = []() { return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); }; + api["getRealFrameDuration"] = []() { return MWBase::Environment::get().getFrameDuration(); }; if (!global) return; diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 48b0c15381..2417e9e340 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -21,11 +21,13 @@ #include #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwrender/postprocessor.hpp" #include "../mwworld/datetimemanager.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/player.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/scene.hpp" #include "../mwworld/worldmodel.hpp" @@ -123,8 +125,8 @@ namespace MWLua void LuaManager::update() { - if (Settings::lua().mGcStepsPerFrame > 0) - lua_gc(mLua.sol(), LUA_GCSTEP, Settings::lua().mGcStepsPerFrame); + if (const int steps = Settings::lua().mGcStepsPerFrame; steps > 0) + lua_gc(mLua.sol(), LUA_GCSTEP, steps); if (mPlayer.isEmpty()) return; // The game is not started yet. @@ -140,9 +142,8 @@ namespace MWLua mObjectLists.update(); - std::erase_if(mActiveLocalScripts, [](const LocalScripts* l) { - return l->getPtrOrEmpty().isEmpty() || l->getPtrOrEmpty().getRefData().isDeleted(); - }); + std::erase_if(mActiveLocalScripts, + [](const LocalScripts* l) { return l->getPtrOrEmpty().isEmpty() || l->getPtrOrEmpty().mRef->isDeleted(); }); mGlobalScripts.statsNextFrame(); for (LocalScripts* scripts : mActiveLocalScripts) @@ -225,10 +226,12 @@ namespace MWLua playerScripts->processInputEvent(event); } mInputEvents.clear(); + double frameDuration = MWBase::Environment::get().getWorld()->getTimeManager()->isPaused() + ? 0.0 + : MWBase::Environment::get().getFrameDuration(); + mInputActions.update(frameDuration); if (playerScripts) - playerScripts->onFrame(MWBase::Environment::get().getWorld()->getTimeManager()->isPaused() - ? 0.0 - : MWBase::Environment::get().getFrameDuration()); + playerScripts->onFrame(frameDuration); mProcessingInputEvents = false; for (const std::string& message : mUIMessages) @@ -291,6 +294,8 @@ namespace MWLua } mGlobalStorage.clearTemporaryAndRemoveCallbacks(); mPlayerStorage.clearTemporaryAndRemoveCallbacks(); + mInputActions.clear(); + mInputTriggers.clear(); for (int i = 0; i < 5; ++i) lua_gc(mLua.sol(), LUA_GCCOLLECT, 0); } @@ -344,6 +349,13 @@ namespace MWLua playerScripts->uiModeChanged(argId, false); } + void LuaManager::actorDied(const MWWorld::Ptr& actor) + { + if (actor.isEmpty()) + return; + mLuaEvents.addLocalEvent({ getId(actor), "Died", {} }); + } + void LuaManager::useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) { MWBase::Environment::get().getWorldModel()->registerPtr(object); @@ -513,6 +525,8 @@ namespace MWLua MWBase::Environment::get().getL10nManager()->dropCache(); mUiResourceManager.clear(); mLua.dropScriptCache(); + mInputActions.clear(); + mInputTriggers.clear(); initConfiguration(); { // Reload global scripts diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 404820cc6b..8bd189d8e9 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -85,6 +86,7 @@ namespace MWLua void objectTeleported(const MWWorld::Ptr& ptr) override; void questUpdated(const ESM::RefId& questId, int stage) override; void uiModeChanged(const MWWorld::Ptr& arg) override; + void actorDied(const MWWorld::Ptr& actor) override; MWBase::LuaManager::ActorControls* getActorControls(const MWWorld::Ptr&) const override; @@ -143,6 +145,9 @@ namespace MWLua void reportStats(unsigned int frameNumber, osg::Stats& stats) const; std::string formatResourceUsageStats() const override; + LuaUtil::InputAction::Registry& inputActions() { return mInputActions; } + LuaUtil::InputTrigger::Registry& inputTriggers() { return mInputTriggers; } + private: void initConfiguration(); LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, @@ -205,6 +210,9 @@ namespace MWLua LuaUtil::LuaStorage mGlobalStorage{ mLua.sol() }; LuaUtil::LuaStorage mPlayerStorage{ mLua.sol() }; + + LuaUtil::InputAction::Registry mInputActions; + LuaUtil::InputTrigger::Registry mInputTriggers; }; } diff --git a/apps/openmw/mwlua/magicbindings.cpp b/apps/openmw/mwlua/magicbindings.cpp index fb714c81c6..3d57ab24fc 100644 --- a/apps/openmw/mwlua/magicbindings.cpp +++ b/apps/openmw/mwlua/magicbindings.cpp @@ -769,8 +769,13 @@ namespace MWLua sol::state_view lua(ts); self.reset(); return sol::as_function([lua, self]() mutable -> std::pair { - if (!self.isEnd()) + while (!self.isEnd()) { + if (self.mIterator->second.getBase() == 0 && self.mIterator->second.getModifier() == 0.f) + { + self.advance(); + continue; + } ActiveEffect effect = ActiveEffect{ self.mIterator->first, self.mIterator->second }; auto result = sol::make_object(lua, effect); @@ -778,10 +783,7 @@ namespace MWLua self.advance(); return { key, result }; } - else - { - return { sol::lua_nil, sol::lua_nil }; - } + return { sol::lua_nil, sol::lua_nil }; }); }; @@ -823,7 +825,7 @@ namespace MWLua if (auto* store = effects.getStore()) if (auto effect = store->get(key)) return ActiveEffect{ key, effect.value() }; - return sol::nullopt; + return ActiveEffect{ key, MWMechanics::EffectParam() }; }; // types.Actor.activeEffects(o):removeEffect(id, ?arg) @@ -839,7 +841,7 @@ namespace MWLua // 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. - auto ptr = effects.mActor.ptr(); + const auto& ptr = effects.mActor.ptr(); auto& activeSpells = ptr.getClass().getCreatureStats(ptr).getActiveSpells(); activeSpells.purgeEffect(ptr, key.mId, key.mArg); diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index e938d90e5e..748d963bdc 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -20,6 +20,9 @@ #include "../mwmechanics/creaturestats.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + #include "luaevents.hpp" #include "luamanagerimp.hpp" #include "types/types.hpp" @@ -124,7 +127,7 @@ namespace MWLua newPtr = cls.moveToCell(ptr, *destCell, toPos(pos, rot)); ptr.getCellRef().unsetRefNum(); ptr.getRefData().setLuaScripts(nullptr); - ptr.getRefData().setCount(0); + ptr.getCellRef().setCount(0); ESM::RefId script = cls.getScript(newPtr); if (!script.empty()) world->getLocalScripts().add(script, newPtr); @@ -256,7 +259,7 @@ namespace MWLua [types = getTypeToPackageTable(context.mLua->sol())]( const ObjectT& o) mutable { return types[getLiveCellRefType(o.ptr().mRef)]; }); - objectT["count"] = sol::readonly_property([](const ObjectT& o) { return o.ptr().getRefData().getCount(); }); + objectT["count"] = sol::readonly_property([](const ObjectT& o) { return o.ptr().getCellRef().getCount(); }); objectT[sol::meta_function::equal_to] = [](const ObjectT& a, const ObjectT& b) { return a.id() == b.id(); }; objectT[sol::meta_function::to_string] = &ObjectT::toString; objectT["sendEvent"] = [context](const ObjectT& dest, std::string eventName, const sol::object& eventData) { @@ -340,10 +343,10 @@ namespace MWLua auto isEnabled = [](const ObjectT& o) { return o.ptr().getRefData().isEnabled(); }; auto setEnabled = [context](const GObject& object, bool enable) { - if (enable && object.ptr().getRefData().isDeleted()) + if (enable && object.ptr().mRef->isDeleted()) throw std::runtime_error("Object is removed"); context.mLuaManager->addAction([object, enable] { - if (object.ptr().getRefData().isDeleted()) + if (object.ptr().mRef->isDeleted()) return; if (object.ptr().isInCell()) { @@ -417,20 +420,20 @@ namespace MWLua using DelayedRemovalFn = std::function; auto removeFn = [](const MWWorld::Ptr ptr, int countToRemove) -> std::optional { - int rawCount = ptr.getRefData().getCount(false); + int rawCount = ptr.getCellRef().getCount(false); int currentCount = std::abs(rawCount); int signedCountToRemove = (rawCount < 0 ? -1 : 1) * countToRemove; if (countToRemove <= 0 || countToRemove > currentCount) throw std::runtime_error("Can't remove " + std::to_string(countToRemove) + " of " + std::to_string(currentCount) + " items"); - ptr.getRefData().setCount(rawCount - signedCountToRemove); // Immediately change count + ptr.getCellRef().setCount(rawCount - signedCountToRemove); // Immediately change count if (!ptr.getContainerStore() && currentCount > countToRemove) return std::nullopt; // Delayed action to trigger side effects return [signedCountToRemove](MWWorld::Ptr ptr) { // Restore the original count - ptr.getRefData().setCount(ptr.getRefData().getCount(false) + signedCountToRemove); + ptr.getCellRef().setCount(ptr.getCellRef().getCount(false) + signedCountToRemove); // And now remove properly if (ptr.getContainerStore()) ptr.getContainerStore()->remove(ptr, std::abs(signedCountToRemove), false); @@ -443,7 +446,7 @@ namespace MWLua }; objectT["remove"] = [removeFn, context](const GObject& object, sol::optional count) { std::optional delayed - = removeFn(object.ptr(), count.value_or(object.ptr().getRefData().getCount())); + = removeFn(object.ptr(), count.value_or(object.ptr().getCellRef().getCount())); if (delayed.has_value()) context.mLuaManager->addAction([fn = *delayed, object] { fn(object.ptr()); }); }; @@ -463,7 +466,7 @@ namespace MWLua }; objectT["moveInto"] = [removeFn, context](const GObject& object, const sol::object& dest) { const MWWorld::Ptr& ptr = object.ptr(); - int count = ptr.getRefData().getCount(); + int count = ptr.getCellRef().getCount(); MWWorld::Ptr destPtr; if (dest.is()) destPtr = dest.as().ptr(); @@ -474,9 +477,9 @@ namespace MWLua std::optional delayedRemovalFn = removeFn(ptr, count); context.mLuaManager->addAction([item = object, count, cont = GObject(destPtr), delayedRemovalFn] { const MWWorld::Ptr& oldPtr = item.ptr(); - auto& refData = oldPtr.getRefData(); + auto& refData = oldPtr.getCellRef(); refData.setCount(count); // temporarily undo removal to run ContainerStore::add - refData.enable(); + oldPtr.getRefData().enable(); cont.ptr().getClass().getContainerStore(cont.ptr()).add(oldPtr, count, false); refData.setCount(0); if (delayedRemovalFn.has_value()) @@ -487,7 +490,7 @@ namespace MWLua const osg::Vec3f& pos, const sol::object& options) { MWWorld::CellStore* cell = findCell(cellOrName, pos); MWWorld::Ptr ptr = object.ptr(); - int count = ptr.getRefData().getCount(); + int count = ptr.getCellRef().getCount(); if (count == 0) throw std::runtime_error("Object is either removed or already in the process of teleporting"); osg::Vec3f rot = ptr.getRefData().getPosition().asRotationVec3(); @@ -508,9 +511,9 @@ namespace MWLua context.mLuaManager->addAction( [object, cell, pos, rot, count, delayedRemovalFn, placeOnGround] { MWWorld::Ptr oldPtr = object.ptr(); - oldPtr.getRefData().setCount(count); + oldPtr.getCellRef().setCount(count); MWWorld::Ptr newPtr = oldPtr.getClass().moveToCell(oldPtr, *cell); - oldPtr.getRefData().setCount(0); + oldPtr.getCellRef().setCount(0); newPtr.getRefData().disable(); teleportNotPlayer(newPtr, cell, pos, rot, placeOnGround); delayedRemovalFn(oldPtr); @@ -522,10 +525,10 @@ namespace MWLua [cell, pos, rot, placeOnGround] { teleportPlayer(cell, pos, rot, placeOnGround); }); else { - ptr.getRefData().setCount(0); + ptr.getCellRef().setCount(0); context.mLuaManager->addAction( [object, cell, pos, rot, count, placeOnGround] { - object.ptr().getRefData().setCount(count); + object.ptr().getCellRef().setCount(count); teleportNotPlayer(object.ptr(), cell, pos, rot, placeOnGround); }, "TeleportAction"); diff --git a/apps/openmw/mwlua/stats.cpp b/apps/openmw/mwlua/stats.cpp index ff74704ed8..02bed00bf5 100644 --- a/apps/openmw/mwlua/stats.cpp +++ b/apps/openmw/mwlua/stats.cpp @@ -380,7 +380,7 @@ namespace MWLua addProp(context, attributeStatT, "base", &MWMechanics::AttributeValue::getBase); addProp(context, attributeStatT, "damage", &MWMechanics::AttributeValue::getDamage); attributeStatT["modified"] - = sol::property([=](const AttributeStat& stat) { return stat.getModified(context); }); + = sol::readonly_property([=](const AttributeStat& stat) { return stat.getModified(context); }); addProp(context, attributeStatT, "modifier", &MWMechanics::AttributeValue::getModifier); sol::table attributes(context.mLua->sol(), sol::create); stats["attributes"] = LuaUtil::makeReadOnly(attributes); @@ -399,7 +399,8 @@ namespace MWLua auto skillStatT = context.mLua->sol().new_usertype("SkillStat"); addProp(context, skillStatT, "base", &MWMechanics::SkillValue::getBase); addProp(context, skillStatT, "damage", &MWMechanics::SkillValue::getDamage); - skillStatT["modified"] = sol::property([=](const SkillStat& stat) { return stat.getModified(context); }); + skillStatT["modified"] + = sol::readonly_property([=](const SkillStat& stat) { return stat.getModified(context); }); addProp(context, skillStatT, "modifier", &MWMechanics::SkillValue::getModifier); skillStatT["progress"] = sol::property([context](const SkillStat& stat) { return stat.getProgress(context); }, [context](const SkillStat& stat, const sol::object& value) { stat.cache(context, "progress", value); }); diff --git a/apps/openmw/mwlua/types/activator.cpp b/apps/openmw/mwlua/types/activator.cpp index a9edc1493f..43e03952f7 100644 --- a/apps/openmw/mwlua/types/activator.cpp +++ b/apps/openmw/mwlua/types/activator.cpp @@ -5,10 +5,6 @@ #include #include -#include -#include -#include - namespace sol { template <> @@ -43,8 +39,6 @@ namespace MWLua { void addActivatorBindings(sol::table activator, const Context& context) { - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - activator["createRecordDraft"] = tableToActivator; addRecordFunctionBinding(activator, context); @@ -54,8 +48,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Activator& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Activator& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Activator& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); + record["model"] = sol::readonly_property([](const ESM::Activator& rec) -> std::string { + return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["mwscript"] = sol::readonly_property( [](const ESM::Activator& rec) -> std::string { return rec.mScript.serializeText(); }); diff --git a/apps/openmw/mwlua/types/actor.cpp b/apps/openmw/mwlua/types/actor.cpp index 370e9e7f69..473b0d3301 100644 --- a/apps/openmw/mwlua/types/actor.cpp +++ b/apps/openmw/mwlua/types/actor.cpp @@ -6,8 +6,10 @@ #include #include +#include "apps/openmw/mwbase/environment.hpp" #include "apps/openmw/mwbase/mechanicsmanager.hpp" #include "apps/openmw/mwbase/windowmanager.hpp" +#include "apps/openmw/mwbase/world.hpp" #include "apps/openmw/mwmechanics/actorutil.hpp" #include "apps/openmw/mwmechanics/creaturestats.hpp" #include "apps/openmw/mwmechanics/drawstate.hpp" @@ -37,7 +39,7 @@ namespace MWLua itemPtr = MWBase::Environment::get().getWorldModel()->getPtr(std::get(item)); if (old_it != store.end() && *old_it == itemPtr) return { old_it, true }; // already equipped - if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0 + if (itemPtr.isEmpty() || itemPtr.getCellRef().getCount() == 0 || itemPtr.getContainerStore() != static_cast(&store)) { Log(Debug::Warning) << "Object" << std::get(item).toString() << " is not in inventory"; @@ -51,7 +53,7 @@ namespace MWLua if (old_it != store.end() && old_it->getCellRef().getRefId() == recordId) return { old_it, true }; // already equipped itemPtr = store.search(recordId); - if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0) + if (itemPtr.isEmpty() || itemPtr.getCellRef().getCount() == 0) { Log(Debug::Warning) << "There is no object with recordId='" << stringId << "' in inventory"; return { store.end(), false }; diff --git a/apps/openmw/mwlua/types/actor.hpp b/apps/openmw/mwlua/types/actor.hpp index 4a16b65cbf..6700b4a403 100644 --- a/apps/openmw/mwlua/types/actor.hpp +++ b/apps/openmw/mwlua/types/actor.hpp @@ -4,16 +4,15 @@ #include #include -#include - -#include "apps/openmw/mwworld/esmstore.hpp" -#include -#include #include #include +#include + +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" #include "../context.hpp" -#include "../object.hpp" + namespace MWLua { diff --git a/apps/openmw/mwlua/types/apparatus.cpp b/apps/openmw/mwlua/types/apparatus.cpp index 10bdbcdd29..282dd0669d 100644 --- a/apps/openmw/mwlua/types/apparatus.cpp +++ b/apps/openmw/mwlua/types/apparatus.cpp @@ -5,9 +5,7 @@ #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -38,8 +36,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Apparatus& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Apparatus& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Apparatus& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); + record["model"] = sol::readonly_property([](const ESM::Apparatus& rec) -> std::string { + return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["mwscript"] = sol::readonly_property( [](const ESM::Apparatus& rec) -> std::string { return rec.mScript.serializeText(); }); diff --git a/apps/openmw/mwlua/types/armor.cpp b/apps/openmw/mwlua/types/armor.cpp index 4808107ebf..91a4c05d8b 100644 --- a/apps/openmw/mwlua/types/armor.cpp +++ b/apps/openmw/mwlua/types/armor.cpp @@ -5,9 +5,7 @@ #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -95,9 +93,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Armor& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Armor& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Armor& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + record["model"] = sol::readonly_property( + [](const ESM::Armor& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["icon"] = sol::readonly_property([vfs](const ESM::Armor& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); diff --git a/apps/openmw/mwlua/types/book.cpp b/apps/openmw/mwlua/types/book.cpp index 4fe2f9d071..ce2138127d 100644 --- a/apps/openmw/mwlua/types/book.cpp +++ b/apps/openmw/mwlua/types/book.cpp @@ -4,14 +4,12 @@ #include #include +#include #include #include #include -#include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -104,9 +102,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Book& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + record["model"] = sol::readonly_property( + [](const ESM::Book& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["mwscript"] = sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mScript.serializeText(); }); record["icon"] = sol::readonly_property([vfs](const ESM::Book& rec) -> std::string { diff --git a/apps/openmw/mwlua/types/clothing.cpp b/apps/openmw/mwlua/types/clothing.cpp index 7f4d6e7002..894748946d 100644 --- a/apps/openmw/mwlua/types/clothing.cpp +++ b/apps/openmw/mwlua/types/clothing.cpp @@ -5,9 +5,7 @@ #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -90,9 +88,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Clothing& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Clothing& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Clothing& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + record["model"] = sol::readonly_property( + [](const ESM::Clothing& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["icon"] = sol::readonly_property([vfs](const ESM::Clothing& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); diff --git a/apps/openmw/mwlua/types/container.cpp b/apps/openmw/mwlua/types/container.cpp index 25a1c1adce..332cf6ac2e 100644 --- a/apps/openmw/mwlua/types/container.cpp +++ b/apps/openmw/mwlua/types/container.cpp @@ -5,10 +5,7 @@ #include #include -#include -#include -#include -#include +#include "apps/openmw/mwworld/class.hpp" namespace sol { @@ -42,8 +39,6 @@ namespace MWLua }; container["capacity"] = container["getCapacity"]; // for compatibility; should be removed later - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - addRecordFunctionBinding(container, context); sol::usertype record = context.mLua->sol().new_usertype("ESM3_Container"); @@ -53,8 +48,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Container& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Container& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Container& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); + record["model"] = sol::readonly_property([](const ESM::Container& rec) -> std::string { + return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["mwscript"] = sol::readonly_property( [](const ESM::Container& rec) -> std::string { return rec.mScript.serializeText(); }); diff --git a/apps/openmw/mwlua/types/creature.cpp b/apps/openmw/mwlua/types/creature.cpp index 332a9b9b14..ddf90bf8c5 100644 --- a/apps/openmw/mwlua/types/creature.cpp +++ b/apps/openmw/mwlua/types/creature.cpp @@ -6,10 +6,6 @@ #include #include -#include -#include -#include - namespace sol { template <> @@ -29,8 +25,6 @@ namespace MWLua { "Humanoid", ESM::Creature::Humanoid }, })); - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - addRecordFunctionBinding(creature, context); sol::usertype record = context.mLua->sol().new_usertype("ESM3_Creature"); @@ -39,9 +33,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Creature& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Creature& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Creature& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + record["model"] = sol::readonly_property( + [](const ESM::Creature& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["mwscript"] = sol::readonly_property( [](const ESM::Creature& rec) -> std::string { return rec.mScript.serializeText(); }); record["baseCreature"] = sol::readonly_property( diff --git a/apps/openmw/mwlua/types/door.cpp b/apps/openmw/mwlua/types/door.cpp index 5a2cfc8aee..df1d10015a 100644 --- a/apps/openmw/mwlua/types/door.cpp +++ b/apps/openmw/mwlua/types/door.cpp @@ -7,7 +7,6 @@ #include #include -#include "apps/openmw/mwworld/esmstore.hpp" #include "apps/openmw/mwworld/worldmodel.hpp" namespace sol @@ -55,8 +54,6 @@ namespace MWLua return sol::make_object(lua, LCell{ &cell }); }; - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - addRecordFunctionBinding(door, context); sol::usertype record = context.mLua->sol().new_usertype("ESM3_Door"); @@ -65,9 +62,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Door& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Door& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Door& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + record["model"] = sol::readonly_property( + [](const ESM::Door& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["mwscript"] = sol::readonly_property([](const ESM::Door& rec) -> std::string { return rec.mScript.serializeText(); }); record["openSound"] = sol::readonly_property( @@ -95,8 +91,6 @@ namespace MWLua return sol::make_object(lua, LCell{ &cell }); }; - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - addRecordFunctionBinding(door, context, "ESM4Door"); sol::usertype record = context.mLua->sol().new_usertype("ESM4_Door"); @@ -106,9 +100,8 @@ namespace MWLua record["id"] = sol::readonly_property( [](const ESM4::Door& rec) -> std::string { return ESM::RefId(rec.mId).serializeText(); }); record["name"] = sol::readonly_property([](const ESM4::Door& rec) -> std::string { return rec.mFullName; }); - record["model"] = sol::readonly_property([vfs](const ESM4::Door& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + record["model"] = sol::readonly_property( + [](const ESM4::Door& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["isAutomatic"] = sol::readonly_property( [](const ESM4::Door& rec) -> bool { return rec.mDoorFlags & ESM4::Door::Flag_AutomaticDoor; }); } diff --git a/apps/openmw/mwlua/types/ingredient.cpp b/apps/openmw/mwlua/types/ingredient.cpp index 31791a19ea..abfd2329ce 100644 --- a/apps/openmw/mwlua/types/ingredient.cpp +++ b/apps/openmw/mwlua/types/ingredient.cpp @@ -6,10 +6,7 @@ #include #include -#include - -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -33,8 +30,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Ingredient& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Ingredient& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Ingredient& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); + record["model"] = sol::readonly_property([](const ESM::Ingredient& rec) -> std::string { + return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["mwscript"] = sol::readonly_property( [](const ESM::Ingredient& rec) -> std::string { return rec.mScript.serializeText(); }); diff --git a/apps/openmw/mwlua/types/item.cpp b/apps/openmw/mwlua/types/item.cpp index b616075496..648229a5e5 100644 --- a/apps/openmw/mwlua/types/item.cpp +++ b/apps/openmw/mwlua/types/item.cpp @@ -15,7 +15,7 @@ namespace MWLua item["setEnchantmentCharge"] = [](const GObject& object, float charge) { object.ptr().getCellRef().setEnchantmentCharge(charge); }; item["isRestocking"] - = [](const Object& object) -> bool { return object.ptr().getRefData().getCount(false) < 0; }; + = [](const Object& object) -> bool { return object.ptr().getCellRef().getCount(false) < 0; }; addItemDataBindings(item, context); } diff --git a/apps/openmw/mwlua/types/light.cpp b/apps/openmw/mwlua/types/light.cpp index 347bb61641..5a357994a3 100644 --- a/apps/openmw/mwlua/types/light.cpp +++ b/apps/openmw/mwlua/types/light.cpp @@ -5,9 +5,7 @@ #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -31,9 +29,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Light& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Light& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Light& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + record["model"] = sol::readonly_property( + [](const ESM::Light& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["icon"] = sol::readonly_property([vfs](const ESM::Light& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); diff --git a/apps/openmw/mwlua/types/lockable.cpp b/apps/openmw/mwlua/types/lockable.cpp index e7413edef5..2569f42ee4 100644 --- a/apps/openmw/mwlua/types/lockable.cpp +++ b/apps/openmw/mwlua/types/lockable.cpp @@ -1,11 +1,11 @@ - #include "types.hpp" + #include #include #include #include -#include +#include "apps/openmw/mwworld/esmstore.hpp" namespace MWLua { diff --git a/apps/openmw/mwlua/types/lockpick.cpp b/apps/openmw/mwlua/types/lockpick.cpp index 786471461a..373de4b24d 100644 --- a/apps/openmw/mwlua/types/lockpick.cpp +++ b/apps/openmw/mwlua/types/lockpick.cpp @@ -5,9 +5,7 @@ #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -31,9 +29,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Lockpick& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Lockpick& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Lockpick& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + record["model"] = sol::readonly_property( + [](const ESM::Lockpick& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["mwscript"] = sol::readonly_property( [](const ESM::Lockpick& rec) -> std::string { return rec.mScript.serializeText(); }); record["icon"] = sol::readonly_property([vfs](const ESM::Lockpick& rec) -> std::string { diff --git a/apps/openmw/mwlua/types/misc.cpp b/apps/openmw/mwlua/types/misc.cpp index d359534638..f83864477f 100644 --- a/apps/openmw/mwlua/types/misc.cpp +++ b/apps/openmw/mwlua/types/misc.cpp @@ -6,9 +6,8 @@ #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" namespace sol { @@ -83,8 +82,8 @@ namespace MWLua record["id"] = sol::readonly_property( [](const ESM::Miscellaneous& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Miscellaneous& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); + record["model"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> std::string { + return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["mwscript"] = sol::readonly_property( [](const ESM::Miscellaneous& rec) -> std::string { return rec.mScript.serializeText(); }); diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 68d13342c4..30c8fd60d9 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -5,12 +5,12 @@ #include #include -#include -#include -#include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwbase/mechanicsmanager.hpp" +#include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwmechanics/npcstats.hpp" +#include "apps/openmw/mwworld/class.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" #include "../classbindings.hpp" #include "../localscripts.hpp" @@ -268,7 +268,7 @@ namespace MWLua npcStats.setFactionReputation(factionId, existingReputation + value); }; - npc["expell"] = [](Object& actor, std::string_view faction) { + npc["expel"] = [](Object& actor, std::string_view faction) { if (dynamic_cast(&actor) && !dynamic_cast(&actor)) throw std::runtime_error("Local scripts can modify only self"); diff --git a/apps/openmw/mwlua/types/player.cpp b/apps/openmw/mwlua/types/player.cpp index cef0753817..07b238fa36 100644 --- a/apps/openmw/mwlua/types/player.cpp +++ b/apps/openmw/mwlua/types/player.cpp @@ -1,12 +1,13 @@ #include "types.hpp" #include "../luamanagerimp.hpp" -#include -#include -#include -#include -#include -#include + +#include "apps/openmw/mwbase/inputmanager.hpp" +#include "apps/openmw/mwbase/journal.hpp" +#include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwmechanics/npcstats.hpp" +#include "apps/openmw/mwworld/class.hpp" +#include "apps/openmw/mwworld/globals.hpp" namespace MWLua { diff --git a/apps/openmw/mwlua/types/potion.cpp b/apps/openmw/mwlua/types/potion.cpp index 022af56b02..50aca6d9e7 100644 --- a/apps/openmw/mwlua/types/potion.cpp +++ b/apps/openmw/mwlua/types/potion.cpp @@ -5,9 +5,7 @@ #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -73,9 +71,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Potion& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Potion& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Potion& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + record["model"] = sol::readonly_property( + [](const ESM::Potion& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["icon"] = sol::readonly_property([vfs](const ESM::Potion& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); diff --git a/apps/openmw/mwlua/types/probe.cpp b/apps/openmw/mwlua/types/probe.cpp index 668e58c98c..30c84326a5 100644 --- a/apps/openmw/mwlua/types/probe.cpp +++ b/apps/openmw/mwlua/types/probe.cpp @@ -5,9 +5,7 @@ #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -31,9 +29,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Probe& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Probe& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Probe& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + record["model"] = sol::readonly_property( + [](const ESM::Probe& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["mwscript"] = sol::readonly_property([](const ESM::Probe& rec) -> std::string { return rec.mScript.serializeText(); }); record["icon"] = sol::readonly_property([vfs](const ESM::Probe& rec) -> std::string { diff --git a/apps/openmw/mwlua/types/repair.cpp b/apps/openmw/mwlua/types/repair.cpp index 75d0a17c49..880a74d131 100644 --- a/apps/openmw/mwlua/types/repair.cpp +++ b/apps/openmw/mwlua/types/repair.cpp @@ -5,9 +5,7 @@ #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -31,9 +29,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Repair& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Repair& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Repair& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + record["model"] = sol::readonly_property( + [](const ESM::Repair& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["mwscript"] = sol::readonly_property([](const ESM::Repair& rec) -> std::string { return rec.mScript.serializeText(); }); record["icon"] = sol::readonly_property([vfs](const ESM::Repair& rec) -> std::string { diff --git a/apps/openmw/mwlua/types/static.cpp b/apps/openmw/mwlua/types/static.cpp index 76dac4fa00..78f8cffd67 100644 --- a/apps/openmw/mwlua/types/static.cpp +++ b/apps/openmw/mwlua/types/static.cpp @@ -5,10 +5,6 @@ #include #include -#include -#include -#include - namespace sol { template <> @@ -21,8 +17,6 @@ namespace MWLua { void addStaticBindings(sol::table stat, const Context& context) { - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - addRecordFunctionBinding(stat, context); sol::usertype record = context.mLua->sol().new_usertype("ESM3_Static"); @@ -30,8 +24,7 @@ namespace MWLua = [](const ESM::Static& rec) -> std::string { return "ESM3_Static[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Static& rec) -> std::string { return rec.mId.serializeText(); }); - record["model"] = sol::readonly_property([vfs](const ESM::Static& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + record["model"] = sol::readonly_property( + [](const ESM::Static& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); } } diff --git a/apps/openmw/mwlua/types/terminal.cpp b/apps/openmw/mwlua/types/terminal.cpp index b0f8e3be0b..02a9465b91 100644 --- a/apps/openmw/mwlua/types/terminal.cpp +++ b/apps/openmw/mwlua/types/terminal.cpp @@ -6,8 +6,6 @@ #include #include -#include "apps/openmw/mwworld/esmstore.hpp" - namespace sol { template <> @@ -21,9 +19,6 @@ namespace MWLua void addESM4TerminalBindings(sol::table term, const Context& context) { - - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - addRecordFunctionBinding(term, context, "ESM4Terminal"); sol::usertype record = context.mLua->sol().new_usertype("ESM4_Terminal"); @@ -38,8 +33,8 @@ namespace MWLua record["resultText"] = sol::readonly_property([](const ESM4::Terminal& rec) -> std::string { return rec.mResultText; }); record["name"] = sol::readonly_property([](const ESM4::Terminal& rec) -> std::string { return rec.mFullName; }); - record["model"] = sol::readonly_property([vfs](const ESM4::Terminal& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); + record["model"] = sol::readonly_property([](const ESM4::Terminal& rec) -> std::string { + return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); } } diff --git a/apps/openmw/mwlua/types/types.hpp b/apps/openmw/mwlua/types/types.hpp index adac372277..b52846508a 100644 --- a/apps/openmw/mwlua/types/types.hpp +++ b/apps/openmw/mwlua/types/types.hpp @@ -7,7 +7,6 @@ #include #include "apps/openmw/mwbase/environment.hpp" -#include "apps/openmw/mwbase/world.hpp" #include "apps/openmw/mwworld/esmstore.hpp" #include "apps/openmw/mwworld/store.hpp" diff --git a/apps/openmw/mwlua/types/weapon.cpp b/apps/openmw/mwlua/types/weapon.cpp index 993f1ecc95..f09cd96dad 100644 --- a/apps/openmw/mwlua/types/weapon.cpp +++ b/apps/openmw/mwlua/types/weapon.cpp @@ -5,9 +5,7 @@ #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -16,7 +14,6 @@ namespace sol { }; } -#include namespace { @@ -129,9 +126,8 @@ namespace MWLua record["id"] = sol::readonly_property([](const ESM::Weapon& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Weapon& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Weapon& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + record["model"] = sol::readonly_property( + [](const ESM::Weapon& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); record["icon"] = sol::readonly_property([vfs](const ESM::Weapon& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index 98a653949d..30f190ad38 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -47,7 +47,6 @@ namespace MWLua } const std::unordered_map modeToName{ - { MWGui::GM_Settings, "SettingsMenu" }, { MWGui::GM_Inventory, "Interface" }, { MWGui::GM_Container, "Container" }, { MWGui::GM_Companion, "Companion" }, @@ -162,8 +161,8 @@ namespace MWLua api["_getMenuTransparency"] = []() -> float { return Settings::gui().mMenuTransparency; }; auto uiLayer = context.mLua->sol().new_usertype("UiLayer"); - uiLayer["name"] = sol::property([](LuaUi::Layer& self) { return self.name(); }); - uiLayer["size"] = sol::property([](LuaUi::Layer& self) { return self.size(); }); + uiLayer["name"] = sol::readonly_property([](LuaUi::Layer& self) -> std::string_view { return self.name(); }); + uiLayer["size"] = sol::readonly_property([](LuaUi::Layer& self) { return self.size(); }); uiLayer[sol::meta_function::to_string] = [](LuaUi::Layer& self) { return Misc::StringUtils::format("UiLayer(%s)", self.name()); }; diff --git a/apps/openmw/mwlua/vfsbindings.cpp b/apps/openmw/mwlua/vfsbindings.cpp index ad32520649..0eccb336c2 100644 --- a/apps/openmw/mwlua/vfsbindings.cpp +++ b/apps/openmw/mwlua/vfsbindings.cpp @@ -161,7 +161,8 @@ namespace MWLua auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); sol::usertype handle = context.mLua->sol().new_usertype("FileHandle"); - handle["fileName"] = sol::readonly_property([](const FileHandle& self) { return self.mFileName; }); + handle["fileName"] + = sol::readonly_property([](const FileHandle& self) -> std::string_view { return self.mFileName; }); handle[sol::meta_function::to_string] = [](const FileHandle& self) { return "FileHandle{'" + self.mFileName + "'" + (!self.mFilePtr ? ", closed" : "") + "}"; }; diff --git a/apps/openmw/mwlua/worker.cpp b/apps/openmw/mwlua/worker.cpp index e8b06cf210..193d340208 100644 --- a/apps/openmw/mwlua/worker.cpp +++ b/apps/openmw/mwlua/worker.cpp @@ -2,7 +2,7 @@ #include "luamanagerimp.hpp" -#include +#include "apps/openmw/profile.hpp" #include #include diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index decbae765b..d8e409d9e2 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -228,6 +228,7 @@ namespace MWMechanics mSpells.emplace_back(ActiveSpellParams{ spell, ptr }); } + bool updateSpellWindow = false; if (ptr.getClass().hasInventoryStore(ptr) && !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished())) { @@ -264,6 +265,7 @@ namespace MWMechanics for (const auto& effect : params.mEffects) MWMechanics::playEffects( ptr, *world->getStore().get().find(effect.mEffectId), playNonLooping); + updateSpellWindow = true; } } } @@ -321,11 +323,8 @@ namespace MWMechanics ESM::RefId::stringRefId("VFX_Reflect")); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (animation && !reflectStatic->mModel.empty()) - { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - animation->addEffect(Misc::ResourceHelpers::correctMeshPath(reflectStatic->mModel, vfs), + animation->addEffect(Misc::ResourceHelpers::correctMeshPath(reflectStatic->mModel), ESM::MagicEffect::Reflect, false); - } caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); } if (removedSpell) @@ -368,6 +367,7 @@ namespace MWMechanics for (const auto& effect : params.mEffects) onMagicEffectRemoved(ptr, params, effect); applyPurges(ptr, &spellIt); + updateSpellWindow = true; continue; } ++spellIt; @@ -380,6 +380,14 @@ namespace MWMechanics if (creatureStats.getMagicEffects().getOrDefault(effect).getMagnitude() > 0.f) creatureStats.getAiSequence().stopCombat(); } + + if (ptr == player && updateSpellWindow) + { + // Something happened with the spell list -- possibly while the game is paused, + // so we want to make the spell window get the memo. + // We don't normally want to do this, so this targets constant enchantments. + MWBase::Environment::get().getWindowManager()->updateSpellWindow(); + } } void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 3e7b075e62..25c4c97504 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -211,11 +211,8 @@ namespace const ESM::Static* const fx = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Soul_Trap")); if (fx != nullptr) - { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel, vfs), "", + world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), "", creature.getRefData().getPosition().asVec3()); - } MWBase::Environment::get().getSoundManager()->playSound3D( creature.getRefData().getPosition().asVec3(), ESM::RefId::stringRefId("conjuration hit"), 1.f, 1.f); @@ -707,10 +704,9 @@ namespace MWMechanics } } - // Make guards go aggressive with creatures that are in combat, unless the creature is a follower or escorter + // Make guards go aggressive with creatures and werewolves that are in combat const auto world = MWBase::Environment::get().getWorld(); - if (!aggressive && actor1.getClass().isClass(actor1, "Guard") && !actor2.getClass().isNpc() - && creatureStats2.getAiSequence().isInCombat()) + if (!aggressive && actor1.getClass().isClass(actor1, "Guard") && creatureStats2.getAiSequence().isInCombat()) { // Check if the creature is too far static const float fAlarmRadius @@ -718,20 +714,30 @@ namespace MWMechanics if (sqrDist > fAlarmRadius * fAlarmRadius) return; - bool followerOrEscorter = false; - for (const auto& package : creatureStats2.getAiSequence()) + bool targetIsCreature = !actor2.getClass().isNpc(); + if (targetIsCreature || actor2.getClass().getNpcStats(actor2).isWerewolf()) { - // The follow package must be first or have nothing but combat before it - if (package->sideWithTarget()) + bool followerOrEscorter = false; + // ...unless the creature has allies + if (targetIsCreature) { - followerOrEscorter = true; - break; + for (const auto& package : creatureStats2.getAiSequence()) + { + // The follow package must be first or have nothing but combat before it + if (package->sideWithTarget()) + { + followerOrEscorter = true; + break; + } + else if (package->getTypeId() != MWMechanics::AiPackageTypeId::Combat) + break; + } } - else if (package->getTypeId() != MWMechanics::AiPackageTypeId::Combat) - break; + // Morrowind also checks "known werewolf" flag, but the player is never in combat + // so this code is unreachable for the player + if (!followerOrEscorter) + aggressive = true; } - if (!followerOrEscorter) - aggressive = true; } // If any of the above conditions turned actor1 aggressive towards actor2, do an awareness check. If it passes, @@ -1155,6 +1161,9 @@ namespace MWMechanics creatureStats.setAlarmed(false); creatureStats.setAiSetting(AiSetting::Fight, ptr.getClass().getBaseFightRating(ptr)); + // Restore original disposition + npcStats.setCrimeDispositionModifier(0); + // Update witness crime id npcStats.setCrimeId(-1); } @@ -1730,6 +1739,8 @@ namespace MWMechanics actor.getClass().getCreatureStats(actor).notifyDied(); ++mDeathCount[actor.getCellRef().getRefId()]; + + MWBase::Environment::get().getLuaManager()->actorDied(actor); } void Actors::resurrect(const MWWorld::Ptr& ptr) const @@ -1814,12 +1825,8 @@ namespace MWMechanics const ESM::Static* fx = MWBase::Environment::get().getESMStore()->get().search( ESM::RefId::stringRefId("VFX_Summon_End")); if (fx) - { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); MWBase::Environment::get().getWorld()->spawnEffect( - Misc::ResourceHelpers::correctMeshPath(fx->mModel, vfs), "", - ptr.getRefData().getPosition().asVec3()); - } + Misc::ResourceHelpers::correctMeshPath(fx->mModel), "", ptr.getRefData().getPosition().asVec3()); // Remove the summoned creature's summoned creatures as well MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); @@ -2088,6 +2095,10 @@ namespace MWMechanics if (ally.getClass().getCreatureStats(ally).getAiSequence().getCombatTargets(enemies) && std::find(enemies.begin(), enemies.end(), actorPtr) != enemies.end()) break; + enemies.clear(); + if (actorPtr.getClass().getCreatureStats(actorPtr).getAiSequence().getCombatTargets(enemies) + && std::find(enemies.begin(), enemies.end(), ally) != enemies.end()) + break; } list.push_back(package->getTarget()); } diff --git a/apps/openmw/mwmechanics/aiactivate.cpp b/apps/openmw/mwmechanics/aiactivate.cpp index 31abda44c2..be4fe5e674 100644 --- a/apps/openmw/mwmechanics/aiactivate.cpp +++ b/apps/openmw/mwmechanics/aiactivate.cpp @@ -30,7 +30,7 @@ namespace MWMechanics // Stop if the target doesn't exist // Really we should be checking whether the target is currently registered with the MechanicsManager - if (target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled()) + if (target == MWWorld::Ptr() || !target.getCellRef().getCount() || !target.getRefData().isEnabled()) return true; // Turn to target and move to it directly, without pathfinding. diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 5285fb31dd..12072df2ac 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -115,7 +115,7 @@ namespace MWMechanics if (target.isEmpty()) return true; - if (!target.getRefData().getCount() + if (!target.getCellRef().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently // registered with the MechanicsManager || target.getClass().getCreatureStats(target).isDead()) @@ -283,8 +283,8 @@ namespace MWMechanics const MWBase::World* world = MWBase::Environment::get().getWorld(); // Try to build path to the target. const auto agentBounds = world->getPathfindingAgentBounds(actor); - const auto navigatorFlags = getNavigatorFlags(actor); - const auto areaCosts = getAreaCosts(actor); + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); const auto& pathGridGraph = getPathGridGraph(pathgrid); mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, agentBounds, @@ -478,8 +478,7 @@ namespace MWMechanics MWWorld::Ptr AiCombat::getTarget() const { - if (mCachedTarget.isEmpty() || mCachedTarget.getRefData().isDeleted() - || !mCachedTarget.getRefData().isEnabled()) + if (mCachedTarget.isEmpty() || mCachedTarget.mRef->isDeleted() || !mCachedTarget.getRefData().isEnabled()) { mCachedTarget = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); } diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index b78c1fd6ee..b4779dc900 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -99,7 +99,7 @@ namespace MWMechanics // Target is not here right now, wait for it to return // Really we should be checking whether the target is currently registered with the MechanicsManager - if (target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled()) + if (target == MWWorld::Ptr() || !target.getCellRef().getCount() || !target.getRefData().isEnabled()) return false; actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Nothing); @@ -119,21 +119,6 @@ namespace MWMechanics const osg::Vec3f targetPos(target.getRefData().getPosition().asVec3()); const osg::Vec3f targetDir = targetPos - actorPos; - // AiFollow requires the target to be in range and within sight for the initial activation - if (!mActive) - { - storage.mTimer -= duration; - - if (storage.mTimer < 0) - { - if (targetDir.length2() < 500 * 500 && MWBase::Environment::get().getWorld()->getLOS(actor, target)) - mActive = true; - storage.mTimer = 0.5f; - } - } - if (!mActive) - return false; - // In the original engine the first follower stays closer to the player than any subsequent followers. // Followers beyond the first usually attempt to stand inside each other. osg::Vec3f::value_type floatingDistance = 0; @@ -152,6 +137,23 @@ namespace MWMechanics floatingDistance += getHalfExtents(actor) * 2; short followDistance = static_cast(floatingDistance); + // AiFollow requires the target to be in range and within sight for the initial activation + if (!mActive) + { + storage.mTimer -= duration; + + if (storage.mTimer < 0) + { + float activeRange = followDistance + 384.f; + if (targetDir.length2() < activeRange * activeRange + && MWBase::Environment::get().getWorld()->getLOS(actor, target)) + mActive = true; + storage.mTimer = 0.5f; + } + } + if (!mActive) + return false; + if (!mAlwaysFollow) // Update if you only follow for a bit { // Check if we've run out of time diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 183c30bfb7..a265c70cf4 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -63,7 +63,7 @@ MWWorld::Ptr MWMechanics::AiPackage::getTarget() const { if (!mCachedTarget.isEmpty()) { - if (mCachedTarget.getRefData().isDeleted() || !mCachedTarget.getRefData().isEnabled()) + if (mCachedTarget.mRef->isDeleted() || !mCachedTarget.getRefData().isEnabled()) mCachedTarget = MWWorld::Ptr(); else return mCachedTarget; @@ -157,8 +157,10 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& { const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(pathgrid), - agentBounds, getNavigatorFlags(actor), getAreaCosts(actor), endTolerance, pathType); + agentBounds, navigatorFlags, areaCosts, endTolerance, pathType); mRotateOnTheRunChecks = 3; // give priority to go directly on target if there is minimal opportunity @@ -486,14 +488,12 @@ DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld:: return result; } -DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts(const MWWorld::Ptr& actor) const +DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts( + const MWWorld::Ptr& actor, DetourNavigator::Flags flags) const { DetourNavigator::AreaCosts costs; - const DetourNavigator::Flags flags = getNavigatorFlags(actor); const MWWorld::Class& actorClass = actor.getClass(); - const float swimSpeed = (flags & DetourNavigator::Flag_swim) == 0 ? 0.0f : actorClass.getSwimSpeed(actor); - const float walkSpeed = [&] { if ((flags & DetourNavigator::Flag_walk) == 0) return 0.0f; @@ -502,6 +502,14 @@ DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts(const MWWorld::P return actorClass.getRunSpeed(actor); }(); + const float swimSpeed = [&] { + if ((flags & DetourNavigator::Flag_swim) == 0) + return 0.0f; + if (hasWaterWalking(actor)) + return walkSpeed; + return actorClass.getSwimSpeed(actor); + }(); + const float maxSpeed = std::max(swimSpeed, walkSpeed); if (maxSpeed == 0) diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index fa018609e4..9e13ee9cd5 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -150,7 +150,7 @@ namespace MWMechanics DetourNavigator::Flags getNavigatorFlags(const MWWorld::Ptr& actor) const; - DetourNavigator::AreaCosts getAreaCosts(const MWWorld::Ptr& actor) const; + DetourNavigator::AreaCosts getAreaCosts(const MWWorld::Ptr& actor, DetourNavigator::Flags flags) const; const AiPackageTypeId mTypeId; const Options mOptions; diff --git a/apps/openmw/mwmechanics/aipursue.cpp b/apps/openmw/mwmechanics/aipursue.cpp index 2ae4ce5def..461db45133 100644 --- a/apps/openmw/mwmechanics/aipursue.cpp +++ b/apps/openmw/mwmechanics/aipursue.cpp @@ -12,6 +12,7 @@ #include "actorutil.hpp" #include "character.hpp" #include "creaturestats.hpp" +#include "npcstats.hpp" namespace MWMechanics { @@ -37,7 +38,7 @@ namespace MWMechanics // Stop if the target doesn't exist // Really we should be checking whether the target is currently registered with the MechanicsManager - if (target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled()) + if (target == MWWorld::Ptr() || !target.getCellRef().getCount() || !target.getRefData().isEnabled()) return true; if (isTargetMagicallyHidden(target) @@ -47,6 +48,9 @@ namespace MWMechanics if (target.getClass().getCreatureStats(target).isDead()) return true; + if (target.getClass().getNpcStats(target).getBounty() <= 0) + return true; + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Nothing); // Set the target destination @@ -79,7 +83,7 @@ namespace MWMechanics { if (!mCachedTarget.isEmpty()) { - if (mCachedTarget.getRefData().isDeleted() || !mCachedTarget.getRefData().isEnabled()) + if (mCachedTarget.mRef->isDeleted() || !mCachedTarget.getRefData().isEnabled()) mCachedTarget = MWWorld::Ptr(); else return mCachedTarget; diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index af35be3763..5d6f25ecb8 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -6,6 +6,8 @@ #include #include +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "actorutil.hpp" #include "aiactivate.hpp" @@ -17,6 +19,7 @@ #include "aipursue.hpp" #include "aitravel.hpp" #include "aiwander.hpp" +#include "creaturestats.hpp" namespace MWMechanics { @@ -365,7 +368,20 @@ namespace MWMechanics // Stop combat when a non-combat AI package is added if (isActualAiPackage(package.getTypeId())) + { + if (package.getTypeId() == MWMechanics::AiPackageTypeId::Follow + || package.getTypeId() == MWMechanics::AiPackageTypeId::Escort) + { + const auto& mechanicsManager = MWBase::Environment::get().getMechanicsManager(); + std::vector newAllies = mechanicsManager->getActorsSidingWith(package.getTarget()); + std::vector allies = mechanicsManager->getActorsSidingWith(actor); + for (const auto& ally : allies) + ally.getClass().getCreatureStats(ally).getAiSequence().stopCombat(newAllies); + for (const auto& ally : newAllies) + ally.getClass().getCreatureStats(ally).getAiSequence().stopCombat(allies); + } stopCombat(); + } // We should return a wandering actor back after combat, casting or pursuit. // The same thing for actors without AI packages. diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 30aad2e89a..be2601dc37 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -224,8 +224,10 @@ namespace MWMechanics { const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); constexpr float endTolerance = 0; + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid), - agentBounds, getNavigatorFlags(actor), getAreaCosts(actor), endTolerance, PathType::Full); + agentBounds, navigatorFlags, areaCosts, endTolerance, PathType::Full); } if (mPathFinder.isPathConstructed()) @@ -367,8 +369,8 @@ namespace MWMechanics const auto world = MWBase::Environment::get().getWorld(); const auto agentBounds = world->getPathfindingAgentBounds(actor); const auto navigator = world->getNavigator(); - const auto navigatorFlags = getNavigatorFlags(actor); - const auto areaCosts = getAreaCosts(actor); + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); auto& prng = MWBase::Environment::get().getWorld()->getPrng(); do diff --git a/apps/openmw/mwmechanics/alchemy.cpp b/apps/openmw/mwmechanics/alchemy.cpp index 5f8ffc1750..aea3e36632 100644 --- a/apps/openmw/mwmechanics/alchemy.cpp +++ b/apps/openmw/mwmechanics/alchemy.cpp @@ -24,45 +24,64 @@ #include "creaturestats.hpp" #include "magiceffects.hpp" +namespace +{ + constexpr size_t sNumEffects = 4; + + std::optional toKey(const ESM::Ingredient& ingredient, size_t i) + { + if (ingredient.mData.mEffectID[i] < 0) + return {}; + ESM::RefId arg = ESM::Skill::indexToRefId(ingredient.mData.mSkills[i]); + if (arg.empty()) + arg = ESM::Attribute::indexToRefId(ingredient.mData.mAttributes[i]); + return MWMechanics::EffectKey(ingredient.mData.mEffectID[i], arg); + } + + bool containsEffect(const ESM::Ingredient& ingredient, const MWMechanics::EffectKey& effect) + { + for (size_t j = 0; j < sNumEffects; ++j) + { + if (toKey(ingredient, j) == effect) + return true; + } + return false; + } +} + MWMechanics::Alchemy::Alchemy() : mValue(0) - , mPotionName("") { } -std::set MWMechanics::Alchemy::listEffects() const +std::vector MWMechanics::Alchemy::listEffects() const { - std::map effects; - - for (TIngredientsIterator iter(mIngredients.begin()); iter != mIngredients.end(); ++iter) + // We care about the order of these effects as each effect can affect the next when applied. + // The player can affect effect order by placing ingredients into different slots + std::vector effects; + for (size_t slotI = 0; slotI < mIngredients.size() - 1; ++slotI) { - if (!iter->isEmpty()) + if (mIngredients[slotI].isEmpty()) + continue; + const ESM::Ingredient* ingredient = mIngredients[slotI].get()->mBase; + for (size_t slotJ = slotI + 1; slotJ < mIngredients.size(); ++slotJ) { - const MWWorld::LiveCellRef* ingredient = iter->get(); - - std::set seenEffects; - - for (int i = 0; i < 4; ++i) - if (ingredient->mBase->mData.mEffectID[i] != -1) + if (mIngredients[slotJ].isEmpty()) + continue; + const ESM::Ingredient* ingredient2 = mIngredients[slotJ].get()->mBase; + for (size_t i = 0; i < sNumEffects; ++i) + { + if (const auto key = toKey(*ingredient, i)) { - ESM::RefId arg = ESM::Skill::indexToRefId(ingredient->mBase->mData.mSkills[i]); - if (arg.empty()) - arg = ESM::Attribute::indexToRefId(ingredient->mBase->mData.mAttributes[i]); - EffectKey key(ingredient->mBase->mData.mEffectID[i], arg); - - if (seenEffects.insert(key).second) - ++effects[key]; + if (std::find(effects.begin(), effects.end(), *key) != effects.end()) + continue; + if (containsEffect(*ingredient2, *key)) + effects.push_back(*key); } + } } } - - std::set effects2; - - for (std::map::const_iterator iter(effects.begin()); iter != effects.end(); ++iter) - if (iter->second > 1) - effects2.insert(iter->first); - - return effects2; + return effects; } void MWMechanics::Alchemy::applyTools(int flags, float& value) const @@ -133,7 +152,7 @@ void MWMechanics::Alchemy::updateEffects() return; // find effects - std::set effects(listEffects()); + std::vector effects = listEffects(); // general alchemy factor float x = getAlchemyFactor(); @@ -150,14 +169,14 @@ void MWMechanics::Alchemy::updateEffects() x * MWBase::Environment::get().getESMStore()->get().find("iAlchemyMod")->mValue.getFloat()); // build quantified effect list - for (std::set::const_iterator iter(effects.begin()); iter != effects.end(); ++iter) + for (const auto& effectKey : effects) { const ESM::MagicEffect* magicEffect - = MWBase::Environment::get().getESMStore()->get().find(iter->mId); + = MWBase::Environment::get().getESMStore()->get().find(effectKey.mId); if (magicEffect->mData.mBaseCost <= 0) { - const std::string os = "invalid base cost for magic effect " + std::to_string(iter->mId); + const std::string os = "invalid base cost for magic effect " + std::to_string(effectKey.mId); throw std::runtime_error(os); } @@ -198,15 +217,15 @@ void MWMechanics::Alchemy::updateEffects() if (magnitude > 0 && duration > 0) { ESM::ENAMstruct effect; - effect.mEffectID = iter->mId; + effect.mEffectID = effectKey.mId; effect.mAttribute = -1; effect.mSkill = -1; if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill) - effect.mSkill = ESM::Skill::refIdToIndex(iter->mArg); + effect.mSkill = ESM::Skill::refIdToIndex(effectKey.mArg); else if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute) - effect.mAttribute = ESM::Attribute::refIdToIndex(iter->mArg); + effect.mAttribute = ESM::Attribute::refIdToIndex(effectKey.mArg); effect.mRange = 0; effect.mArea = 0; @@ -241,7 +260,7 @@ const ESM::Potion* MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) co bool mismatch = false; - for (int i = 0; i < static_cast(iter->mEffects.mList.size()); ++i) + for (size_t i = 0; i < iter->mEffects.mList.size(); ++i) { const ESM::ENAMstruct& first = iter->mEffects.mList[i]; const ESM::ENAMstruct& second = mEffects[i]; @@ -270,7 +289,7 @@ void MWMechanics::Alchemy::removeIngredients() { iter->getContainerStore()->remove(*iter, 1); - if (iter->getRefData().getCount() < 1) + if (iter->getCellRef().getCount() < 1) *iter = MWWorld::Ptr(); } @@ -350,7 +369,7 @@ int MWMechanics::Alchemy::countPotionsToBrew() const for (TIngredientsIterator iter(beginIngredients()); iter != endIngredients(); ++iter) if (!iter->isEmpty()) { - int count = iter->getRefData().getCount(); + int count = iter->getCellRef().getCount(); if ((count > 0 && count < toBrew) || toBrew < 0) toBrew = count; } @@ -578,7 +597,7 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::createSingle() std::string MWMechanics::Alchemy::suggestPotionName() { - std::set effects = listEffects(); + std::vector effects = listEffects(); if (effects.empty()) return {}; @@ -595,11 +614,11 @@ std::vector MWMechanics::Alchemy::effectsDescription(const MWWorld: const static auto fWortChanceValue = store->get().find("fWortChanceValue")->mValue.getFloat(); const auto& data = item->mData; - for (auto i = 0; i < 4; ++i) + for (size_t i = 0; i < sNumEffects; ++i) { const auto effectID = data.mEffectID[i]; - if (alchemySkill < fWortChanceValue * (i + 1)) + if (alchemySkill < fWortChanceValue * static_cast(i + 1)) break; if (effectID != -1) diff --git a/apps/openmw/mwmechanics/alchemy.hpp b/apps/openmw/mwmechanics/alchemy.hpp index 1b76e400f5..373ca8b887 100644 --- a/apps/openmw/mwmechanics/alchemy.hpp +++ b/apps/openmw/mwmechanics/alchemy.hpp @@ -1,7 +1,6 @@ #ifndef GAME_MWMECHANICS_ALCHEMY_H #define GAME_MWMECHANICS_ALCHEMY_H -#include #include #include @@ -110,7 +109,7 @@ namespace MWMechanics void setPotionName(const std::string& name); ///< Set name of potion to create - std::set listEffects() const; + std::vector listEffects() const; ///< List all effects shared by at least two ingredients. int addIngredient(const MWWorld::Ptr& ingredient); diff --git a/apps/openmw/mwmechanics/autocalcspell.cpp b/apps/openmw/mwmechanics/autocalcspell.cpp index 6581aacdd2..a2f6d479f1 100644 --- a/apps/openmw/mwmechanics/autocalcspell.cpp +++ b/apps/openmw/mwmechanics/autocalcspell.cpp @@ -74,7 +74,8 @@ namespace MWMechanics ESM::RefId school; float skillTerm; calcWeakestSchool(&spell, actorSkills, school, skillTerm); - assert(!school.empty()); + if (school.empty()) + continue; SchoolCaps& cap = schoolCaps[school]; if (cap.mReachedLimit && spellCost <= cap.mMinCost) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index dd7b97b6a5..20c7fd0a92 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -20,6 +20,7 @@ #include "character.hpp" #include +#include #include #include @@ -1189,7 +1190,7 @@ namespace MWMechanics if (!animPlaying) { int mask = MWRender::Animation::BlendMask_Torso | MWRender::Animation::BlendMask_RightArm; - mAnimation->play("idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, ~0ul); + mAnimation->play("idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, ~0ul, true); } else { @@ -1246,8 +1247,47 @@ namespace MWMechanics } } + bool CharacterController::isLoopingAnimation(std::string_view group) const + { + // In Morrowind, a some animation groups are always considered looping, regardless + // of loop start/stop keys. + // To be match vanilla behavior we probably only need to check this list, but we don't + // want to prevent modded animations with custom group names from looping either. + static const std::unordered_set loopingAnimations = { "walkforward", "walkback", "walkleft", + "walkright", "swimwalkforward", "swimwalkback", "swimwalkleft", "swimwalkright", "runforward", "runback", + "runleft", "runright", "swimrunforward", "swimrunback", "swimrunleft", "swimrunright", "sneakforward", + "sneakback", "sneakleft", "sneakright", "turnleft", "turnright", "swimturnleft", "swimturnright", + "spellturnleft", "spellturnright", "torch", "idle", "idle2", "idle3", "idle4", "idle5", "idle6", "idle7", + "idle8", "idle9", "idlesneak", "idlestorm", "idleswim", "jump", "inventoryhandtohand", + "inventoryweapononehand", "inventoryweapontwohand", "inventoryweapontwowide" }; + static const std::vector shortGroups = getAllWeaponTypeShortGroups(); + + if (mAnimation && mAnimation->getTextKeyTime(std::string(group) + ": loop start") >= 0) + return true; + + // Most looping animations have variants for each weapon type shortgroup. + // Just remove the shortgroup instead of enumerating all of the possible animation groupnames. + // Make sure we pick the longest shortgroup so e.g. "bow" doesn't get picked over "crossbow" + // when the shortgroup is crossbow. + std::size_t suffixLength = 0; + for (std::string_view suffix : shortGroups) + { + if (suffix.length() > suffixLength && group.ends_with(suffix)) + { + suffixLength = suffix.length(); + } + } + group.remove_suffix(suffixLength); + + return loopingAnimations.count(group) > 0; + } + bool CharacterController::updateWeaponState() { + // If the current animation is scripted, we can't do anything here. + if (isScriptedAnimPlaying()) + return false; + const auto world = MWBase::Environment::get().getWorld(); auto& prng = world->getPrng(); MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); @@ -1481,10 +1521,6 @@ namespace MWMechanics sndMgr->stopSound3D(mPtr, wolfRun); } - // Combat for actors with scripted animations obviously will be buggy - if (isScriptedAnimPlaying()) - return forcestateupdate; - float complete = 0.f; bool animPlaying = false; ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass; @@ -1569,66 +1605,62 @@ namespace MWMechanics effects = &spell->mEffects.mList; cast.playSpellCastingEffects(spell); } - if (mCanCast) + if (!effects->empty()) { - const ESM::MagicEffect* effect = store.get().find( - effects->back().mEffectID); // use last effect of list for color of VFX_Hands - - const ESM::Static* castStatic - = world->getStore().get().find(ESM::RefId::stringRefId("VFX_Hands")); - - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - - if (!effects->empty()) + if (mCanCast) { + const ESM::MagicEffect* effect = store.get().find( + effects->back().mEffectID); // use last effect of list for color of VFX_Hands + + const ESM::Static* castStatic + = world->getStore().get().find(ESM::RefId::stringRefId("VFX_Hands")); + if (mAnimation->getNode("Bip01 L Hand")) - mAnimation->addEffect( - Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), -1, false, - "Bip01 L Hand", effect->mParticle); + mAnimation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), + -1, false, "Bip01 L Hand", effect->mParticle); if (mAnimation->getNode("Bip01 R Hand")) - mAnimation->addEffect( - Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), -1, false, - "Bip01 R Hand", effect->mParticle); + mAnimation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), + -1, false, "Bip01 R Hand", effect->mParticle); } - } + // first effect used for casting animation + const ESM::ENAMstruct& firstEffect = effects->front(); - const ESM::ENAMstruct& firstEffect = effects->at(0); // first effect used for casting animation - - std::string startKey; - std::string stopKey; - if (isRandomAttackAnimation(mCurrentWeapon)) - { - startKey = "start"; - stopKey = "stop"; - if (mCanCast) - world->castSpell( - mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately - mCastingManualSpell = false; - mCanCast = false; - } - else - { - switch (firstEffect.mRange) + std::string startKey; + std::string stopKey; + if (isRandomAttackAnimation(mCurrentWeapon)) { - case 0: - mAttackType = "self"; - break; - case 1: - mAttackType = "touch"; - break; - case 2: - mAttackType = "target"; - break; + startKey = "start"; + stopKey = "stop"; + if (mCanCast) + world->castSpell( + mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately + mCastingManualSpell = false; + mCanCast = false; + } + else + { + switch (firstEffect.mRange) + { + case 0: + mAttackType = "self"; + break; + case 1: + mAttackType = "touch"; + break; + case 2: + mAttackType = "target"; + break; + } + + startKey = mAttackType + " start"; + stopKey = mAttackType + " stop"; } - startKey = mAttackType + " start"; - stopKey = mAttackType + " stop"; + mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, + 1, startKey, stopKey, 0.0f, 0); + mUpperBodyState = UpperBodyState::Casting; } - - mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, 1, - startKey, stopKey, 0.0f, 0); - mUpperBodyState = UpperBodyState::Casting; } else { @@ -1857,33 +1889,58 @@ namespace MWMechanics if (!mAnimation->isPlaying(mAnimQueue.front().mGroup)) { - // Remove the finished animation, unless it's a scripted animation that was interrupted by e.g. a rebuild of - // the animation object. - if (mAnimQueue.size() > 1 || !mAnimQueue.front().mScripted || mAnimQueue.front().mLoopCount == 0) + // Playing animations through mwscript is weird. If an animation is + // a looping animation (idle or other cyclical animations), then they + // will end as expected. However, if they are non-looping animations, they + // will stick around forever or until another animation appears in the queue. + bool shouldPlayOrRestart = mAnimQueue.size() > 1; + if (shouldPlayOrRestart || !mAnimQueue.front().mScripted + || (mAnimQueue.front().mLoopCount == 0 && mAnimQueue.front().mLooping)) { + mAnimation->setPlayScriptedOnly(false); mAnimation->disable(mAnimQueue.front().mGroup); mAnimQueue.pop_front(); + shouldPlayOrRestart = true; } + else + // A non-looping animation will stick around forever, so only restart if the animation + // actually was removed for some reason. + shouldPlayOrRestart = !mAnimation->getInfo(mAnimQueue.front().mGroup) + && mAnimation->hasAnimation(mAnimQueue.front().mGroup); - if (!mAnimQueue.empty()) + if (shouldPlayOrRestart) { // Move on to the remaining items of the queue - bool loopfallback = mAnimQueue.front().mGroup.starts_with("idle"); - mAnimation->play(mAnimQueue.front().mGroup, - mAnimQueue.front().mScripted ? Priority_Scripted : Priority_Default, - MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, - mAnimQueue.front().mLoopCount, loopfallback); + playAnimQueue(); } } else { - mAnimQueue.front().mLoopCount = mAnimation->getCurrentLoopCount(mAnimQueue.front().mGroup); + float complete; + size_t loopcount; + mAnimation->getInfo(mAnimQueue.front().mGroup, &complete, nullptr, &loopcount); + mAnimQueue.front().mLoopCount = loopcount; + mAnimQueue.front().mTime = complete; } if (!mAnimQueue.empty()) mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1); } + void CharacterController::playAnimQueue(bool loopStart) + { + if (!mAnimQueue.empty()) + { + clearStateAnimation(mCurrentIdle); + mIdleState = CharState_SpecialIdle; + auto priority = mAnimQueue.front().mScripted ? Priority_Scripted : Priority_Default; + mAnimation->setPlayScriptedOnly(mAnimQueue.front().mScripted); + mAnimation->play(mAnimQueue.front().mGroup, priority, MWRender::Animation::BlendMask_All, false, 1.0f, + (loopStart ? "loop start" : "start"), "stop", mAnimQueue.front().mTime, mAnimQueue.front().mLoopCount, + mAnimQueue.front().mLooping); + } + } + void CharacterController::update(float duration) { MWBase::World* world = MWBase::Environment::get().getWorld(); @@ -1904,7 +1961,7 @@ namespace MWMechanics { const ESM::NPC* npc = mPtr.get()->mBase; const ESM::Race* race = world->getStore().get().find(npc->mRace); - float weight = npc->isMale() ? race->mData.mWeight.mMale : race->mData.mWeight.mFemale; + float weight = npc->isMale() ? race->mData.mMaleWeight : race->mData.mFemaleWeight; scale *= weight; } @@ -2306,17 +2363,16 @@ namespace MWMechanics jumpstate = JumpState_None; } - if (mAnimQueue.empty() || inwater || (sneak && mIdleState != CharState_SpecialIdle)) - { - if (inwater) - idlestate = CharState_IdleSwim; - else if (sneak && !mInJump) - idlestate = CharState_IdleSneak; - else - idlestate = CharState_Idle; - } + updateAnimQueue(); + if (!mAnimQueue.empty()) + idlestate = CharState_SpecialIdle; + else if (sneak && !mInJump) + idlestate = CharState_IdleSneak; else - updateAnimQueue(); + idlestate = CharState_Idle; + + if (inwater) + idlestate = CharState_IdleSwim; if (!mSkipAnim) { @@ -2456,10 +2512,11 @@ namespace MWMechanics if (iter == mAnimQueue.begin()) { - anim.mLoopCount = mAnimation->getCurrentLoopCount(anim.mGroup); float complete; - mAnimation->getInfo(anim.mGroup, &complete, nullptr); + size_t loopcount; + mAnimation->getInfo(anim.mGroup, &complete, nullptr, &loopcount); anim.mTime = complete; + anim.mLoopCount = loopcount; } else { @@ -2485,26 +2542,20 @@ namespace MWMechanics entry.mGroup = iter->mGroup; entry.mLoopCount = iter->mLoopCount; entry.mScripted = true; + entry.mLooping = isLoopingAnimation(entry.mGroup); + entry.mTime = iter->mTime; + if (iter->mAbsolute) + { + float start = mAnimation->getTextKeyTime(iter->mGroup + ": start"); + float stop = mAnimation->getTextKeyTime(iter->mGroup + ": stop"); + float time = std::clamp(iter->mTime, start, stop); + entry.mTime = (time - start) / (stop - start); + } mAnimQueue.push_back(entry); } - const ESM::AnimationState::ScriptedAnimation& anim = state.mScriptedAnims.front(); - float complete = anim.mTime; - if (anim.mAbsolute) - { - float start = mAnimation->getTextKeyTime(anim.mGroup + ": start"); - float stop = mAnimation->getTextKeyTime(anim.mGroup + ": stop"); - float time = std::clamp(anim.mTime, start, stop); - complete = (time - start) / (stop - start); - } - - clearStateAnimation(mCurrentIdle); - mIdleState = CharState_SpecialIdle; - - bool loopfallback = mAnimQueue.front().mGroup.starts_with("idle"); - mAnimation->play(anim.mGroup, Priority_Scripted, MWRender::Animation::BlendMask_All, false, 1.0f, "start", - "stop", complete, anim.mLoopCount, loopfallback); + playAnimQueue(); } } @@ -2517,13 +2568,14 @@ namespace MWMechanics if (isScriptedAnimPlaying() && !scripted) return true; - // If this animation is a looped animation (has a "loop start" key) that is already playing + bool looping = isLoopingAnimation(groupname); + + // If this animation is a looped animation that is already playing // and has not yet reached the end of the loop, allow it to continue animating with its existing loop count // and remove any other animations that were queued. // This emulates observed behavior from the original allows the script "OutsideBanner" to animate banners // correctly. - if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname - && mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": loop start") >= 0 + if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname && looping && mAnimation->isPlaying(groupname)) { float endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": loop stop"); @@ -2538,36 +2590,43 @@ namespace MWMechanics } } - count = std::max(count, 1); + // The loop count in vanilla is weird. + // if played with a count of 0, all objects play exactly once from start to stop. + // But if the count is x > 0, actors and non-actors behave differently. actors will loop + // exactly x times, while non-actors will loop x+1 instead. + if (mPtr.getClass().isActor()) + count--; + count = std::max(count, 0); AnimationQueueEntry entry; entry.mGroup = groupname; - entry.mLoopCount = count - 1; + entry.mLoopCount = count; + entry.mTime = 0.f; entry.mScripted = scripted; + entry.mLooping = looping; + + bool playImmediately = false; if (mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup)) { clearAnimQueue(scripted); - clearStateAnimation(mCurrentIdle); - - mIdleState = CharState_SpecialIdle; - bool loopfallback = entry.mGroup.starts_with("idle"); - mAnimation->play(groupname, scripted && groupname != "idle" ? Priority_Scripted : Priority_Default, - MWRender::Animation::BlendMask_All, false, 1.0f, ((mode == 2) ? "loop start" : "start"), "stop", 0.0f, - count - 1, loopfallback); + playImmediately = true; } else { mAnimQueue.resize(1); } - // "PlayGroup idle" is a special case, used to remove to stop scripted animations playing + // "PlayGroup idle" is a special case, used to stop and remove scripted animations playing if (groupname == "idle") entry.mScripted = false; mAnimQueue.push_back(entry); + if (playImmediately) + playAnimQueue(mode == 2); + return true; } @@ -2578,11 +2637,10 @@ namespace MWMechanics bool CharacterController::isScriptedAnimPlaying() const { + // If the front of the anim queue is scripted, morrowind treats it as if it's + // still playing even if it's actually done. if (!mAnimQueue.empty()) - { - const AnimationQueueEntry& first = mAnimQueue.front(); - return first.mScripted && isAnimPlaying(first.mGroup); - } + return mAnimQueue.front().mScripted; return false; } @@ -2612,6 +2670,7 @@ namespace MWMechanics if (clearScriptedAnims) { + mAnimation->setPlayScriptedOnly(false); mAnimQueue.clear(); return; } @@ -2646,6 +2705,8 @@ namespace MWMechanics playRandomDeath(); } + updateAnimQueue(); + mAnimation->runAnimation(0.f); } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 63491ec776..ee26b61a25 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -135,6 +135,8 @@ namespace MWMechanics { std::string mGroup; size_t mLoopCount; + float mTime; + bool mLooping; bool mScripted; }; typedef std::deque AnimationQueue; @@ -219,6 +221,7 @@ namespace MWMechanics bool isMovementAnimationControlled() const; void updateAnimQueue(); + void playAnimQueue(bool useLoopStart = false); void updateHeadTracking(float duration); @@ -245,6 +248,8 @@ namespace MWMechanics void prepareHit(); + bool isLoopingAnimation(std::string_view group) const; + public: CharacterController(const MWWorld::Ptr& ptr, MWRender::Animation* anim); virtual ~CharacterController(); diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 02279859b5..3f17df96fd 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -10,6 +10,7 @@ #include #include +#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" @@ -230,7 +231,8 @@ namespace MWMechanics if (Misc::Rng::roll0to99(world->getPrng()) >= getHitChance(attacker, victim, skillValue)) { - victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false); + victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false, + MWMechanics::DamageSourceType::Ranged); MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker); return; } @@ -286,7 +288,8 @@ namespace MWMechanics victim.getClass().getContainerStore(victim).add(projectile, 1); } - victim.getClass().onHit(victim, damage, true, projectile, attacker, hitPosition, true); + victim.getClass().onHit( + victim, damage, true, projectile, attacker, hitPosition, true, MWMechanics::DamageSourceType::Ranged); } } @@ -649,4 +652,26 @@ namespace MWMechanics return std::make_pair(result, hitPos); } + + bool friendlyHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& target, bool complain) + { + const MWWorld::Ptr& player = getPlayer(); + if (attacker != player) + return false; + + std::set followersAttacker; + MWBase::Environment::get().getMechanicsManager()->getActorsSidingWith(attacker, followersAttacker); + if (followersAttacker.find(target) == followersAttacker.end()) + return false; + + MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); + statsTarget.friendlyHit(); + if (statsTarget.getFriendlyHits() >= 4) + return false; + + if (complain) + MWBase::Environment::get().getDialogueManager()->say(target, ESM::RefId::stringRefId("hit")); + return true; + } + } diff --git a/apps/openmw/mwmechanics/combat.hpp b/apps/openmw/mwmechanics/combat.hpp index 515d2e406c..92033c7e77 100644 --- a/apps/openmw/mwmechanics/combat.hpp +++ b/apps/openmw/mwmechanics/combat.hpp @@ -66,6 +66,8 @@ namespace MWMechanics // Similarly cursed hit target selection std::pair getHitContact(const MWWorld::Ptr& actor, float reach); + + bool friendlyHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& target, bool complain); } #endif diff --git a/apps/openmw/mwmechanics/damagesourcetype.hpp b/apps/openmw/mwmechanics/damagesourcetype.hpp new file mode 100644 index 0000000000..e140a8106f --- /dev/null +++ b/apps/openmw/mwmechanics/damagesourcetype.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_MWMECHANICS_DAMAGESOURCETYPE_H +#define OPENMW_MWMECHANICS_DAMAGESOURCETYPE_H + +namespace MWMechanics +{ + enum class DamageSourceType + { + Unspecified, + Melee, + Ranged, + Magical, + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp index bba6e7361d..c2afef7c0d 100644 --- a/apps/openmw/mwmechanics/magiceffects.cpp +++ b/apps/openmw/mwmechanics/magiceffects.cpp @@ -64,6 +64,11 @@ namespace MWMechanics return left.mArg < right.mArg; } + bool operator==(const EffectKey& left, const EffectKey& right) + { + return left.mId == right.mId && left.mArg == right.mArg; + } + float EffectParam::getMagnitude() const { return mBase + mModifier; diff --git a/apps/openmw/mwmechanics/magiceffects.hpp b/apps/openmw/mwmechanics/magiceffects.hpp index b9831c0250..4fe5d9fd4e 100644 --- a/apps/openmw/mwmechanics/magiceffects.hpp +++ b/apps/openmw/mwmechanics/magiceffects.hpp @@ -38,6 +38,7 @@ namespace MWMechanics }; bool operator<(const EffectKey& left, const EffectKey& right); + bool operator==(const EffectKey& left, const EffectKey& right); struct EffectParam { diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index f95df16855..59b1392dc9 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -134,14 +134,9 @@ namespace MWMechanics for (size_t i = 0; i < player->mNpdt.mSkills.size(); ++i) npcStats.getSkill(ESM::Skill::indexToRefId(i)).setBase(player->mNpdt.mSkills[i]); - creatureStats.setAttribute(ESM::Attribute::Strength, player->mNpdt.mStrength); - creatureStats.setAttribute(ESM::Attribute::Intelligence, player->mNpdt.mIntelligence); - creatureStats.setAttribute(ESM::Attribute::Willpower, player->mNpdt.mWillpower); - creatureStats.setAttribute(ESM::Attribute::Agility, player->mNpdt.mAgility); - creatureStats.setAttribute(ESM::Attribute::Speed, player->mNpdt.mSpeed); - creatureStats.setAttribute(ESM::Attribute::Endurance, player->mNpdt.mEndurance); - creatureStats.setAttribute(ESM::Attribute::Personality, player->mNpdt.mPersonality); - creatureStats.setAttribute(ESM::Attribute::Luck, player->mNpdt.mLuck); + for (size_t i = 0; i < player->mNpdt.mAttributes.size(); ++i) + npcStats.setAttribute(ESM::Attribute::indexToRefId(i), player->mNpdt.mSkills[i]); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); // race @@ -152,13 +147,7 @@ namespace MWMechanics bool male = (player->mFlags & ESM::NPC::Female) == 0; for (const ESM::Attribute& attribute : esmStore.get()) - { - auto index = ESM::Attribute::refIdToIndex(attribute.mId); - assert(index >= 0); - - const ESM::Race::MaleFemale& value = race->mData.mAttributeValues[static_cast(index)]; - creatureStats.setAttribute(attribute.mId, male ? value.mMale : value.mFemale); - } + creatureStats.setAttribute(attribute.mId, race->mData.getAttribute(attribute.mId, male)); for (const ESM::Skill& skill : esmStore.get()) { @@ -484,8 +473,8 @@ namespace MWMechanics int MechanicsManager::getDerivedDisposition(const MWWorld::Ptr& ptr, bool clamp) { - const MWMechanics::NpcStats& npcSkill = ptr.getClass().getNpcStats(ptr); - float x = static_cast(npcSkill.getBaseDisposition()); + const MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + float x = static_cast(npcStats.getBaseDisposition() + npcStats.getCrimeDispositionModifier()); MWWorld::LiveCellRef* npc = ptr.get(); MWWorld::Ptr playerPtr = getPlayer(); @@ -1033,7 +1022,7 @@ namespace MWMechanics if (stolenIt == mStolenItems.end()) continue; OwnerMap& owners = stolenIt->second; - int itemCount = it->getRefData().getCount(); + int itemCount = it->getCellRef().getCount(); for (OwnerMap::iterator ownerIt = owners.begin(); ownerIt != owners.end();) { int toRemove = std::min(itemCount, ownerIt->second); @@ -1045,7 +1034,7 @@ namespace MWMechanics ++ownerIt; } - int toMove = it->getRefData().getCount() - itemCount; + int toMove = it->getCellRef().getCount() - itemCount; containerStore.add(*it, toMove); store.remove(*it, toMove); @@ -1095,15 +1084,21 @@ namespace MWMechanics } } - if (!(item.getCellRef().getRefId() == MWWorld::ContainerStore::sGoldId)) + const bool isGold = item.getClass().isGold(item); + if (!isGold) { if (victim.isEmpty() - || (victim.getClass().isActor() && victim.getRefData().getCount() > 0 + || (victim.getClass().isActor() && victim.getCellRef().getCount() > 0 && !victim.getClass().getCreatureStats(victim).isDead())) mStolenItems[item.getCellRef().getRefId()][owner] += count; } if (alarm) - commitCrime(ptr, victim, OT_Theft, ownerCellRef->getFaction(), item.getClass().getValue(item) * count); + { + int value = count; + if (!isGold) + value *= item.getClass().getValue(item); + commitCrime(ptr, victim, OT_Theft, ownerCellRef->getFaction(), value); + } } bool MechanicsManager::commitCrime(const MWWorld::Ptr& player, const MWWorld::Ptr& victim, OffenseType type, @@ -1298,67 +1293,140 @@ namespace MWMechanics if (!canReportCrime(actor, victim, playerFollowers)) continue; - if (reported && actor.getClass().isClass(actor, "guard")) + NpcStats& observerStats = actor.getClass().getNpcStats(actor); + + int alarm = observerStats.getAiSetting(AiSetting::Alarm).getBase(); + float alarmTerm = 0.01f * alarm; + + bool isActorVictim = actor == victim; + float dispTerm = isActorVictim ? dispVictim : disp; + + bool isActorGuard = actor.getClass().isClass(actor, "guard"); + + int currentDisposition = getDerivedDisposition(actor); + + bool isPermanent = false; + bool applyOnlyIfHostile = false; + int dispositionModifier = 0; + // Murdering and trespassing seem to do not affect disposition + if (type == OT_Theft) + { + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + else if (type == OT_Pickpocket) + { + if (alarm >= 100 && isActorGuard) + dispositionModifier = static_cast(dispTerm); + else if (isActorVictim && isActorGuard) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + else if (isActorVictim) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm); + } + } + else if (type == OT_Assault) + { + if (isActorVictim && !isActorGuard) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm); + } + else if (alarm >= 100) + dispositionModifier = static_cast(dispTerm); + else if (isActorVictim && isActorGuard) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + else + { + applyOnlyIfHostile = true; + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + } + + bool setCrimeId = false; + if (isPermanent && dispositionModifier != 0 && !applyOnlyIfHostile) + { + setCrimeId = true; + dispositionModifier = std::clamp(dispositionModifier, -currentDisposition, 100 - currentDisposition); + int baseDisposition = observerStats.getBaseDisposition(); + observerStats.setBaseDisposition(baseDisposition + dispositionModifier); + } + else if (dispositionModifier != 0 && !applyOnlyIfHostile) + { + setCrimeId = true; + dispositionModifier = std::clamp(dispositionModifier, -currentDisposition, 100 - currentDisposition); + observerStats.modCrimeDispositionModifier(dispositionModifier); + } + + if (isActorGuard && alarm >= 100) { // Mark as Alarmed for dialogue - actor.getClass().getCreatureStats(actor).setAlarmed(true); + observerStats.setAlarmed(true); - // Set the crime ID, which we will use to calm down participants - // once the bounty has been paid. - actor.getClass().getNpcStats(actor).setCrimeId(id); + setCrimeId = true; - if (!actor.getClass().getCreatureStats(actor).getAiSequence().isInPursuit()) + if (!observerStats.getAiSequence().isInPursuit()) { - actor.getClass().getCreatureStats(actor).getAiSequence().stack(AiPursue(player), actor); + observerStats.getAiSequence().stack(AiPursue(player), actor); } } else { - float dispTerm = (actor == victim) ? dispVictim : disp; - - float alarmTerm - = 0.01f * actor.getClass().getCreatureStats(actor).getAiSetting(AiSetting::Alarm).getBase(); - if (type == OT_Pickpocket && alarmTerm <= 0) + // If Alarm is 0, treat it like 100 to calculate a Fight modifier for a victim of pickpocketing. + // Observers which do not try to arrest player do not care about pickpocketing at all. + if (type == OT_Pickpocket && isActorVictim && alarmTerm == 0.0) alarmTerm = 1.0; + else if (type == OT_Pickpocket && !isActorVictim) + alarmTerm = 0.0; - if (actor != victim) - dispTerm *= alarmTerm; - - float fightTerm = static_cast((actor == victim) ? fightVictim : fight); + float fightTerm = static_cast(isActorVictim ? fightVictim : fight); fightTerm += getFightDispositionBias(dispTerm); fightTerm += getFightDistanceBias(actor, player); fightTerm *= alarmTerm; - const int observerFightRating - = actor.getClass().getCreatureStats(actor).getAiSetting(AiSetting::Fight).getBase(); + const int observerFightRating = observerStats.getAiSetting(AiSetting::Fight).getBase(); if (observerFightRating + fightTerm > 100) fightTerm = static_cast(100 - observerFightRating); fightTerm = std::max(0.f, fightTerm); if (observerFightRating + fightTerm >= 100) { + if (dispositionModifier != 0 && applyOnlyIfHostile) + { + dispositionModifier + = std::clamp(dispositionModifier, -currentDisposition, 100 - currentDisposition); + observerStats.modCrimeDispositionModifier(dispositionModifier); + } + startCombat(actor, player); - NpcStats& observerStats = actor.getClass().getNpcStats(actor); // Apply aggression value to the base Fight rating, so that the actor can continue fighting // after a Calm spell wears off observerStats.setAiSetting(AiSetting::Fight, observerFightRating + static_cast(fightTerm)); - observerStats.setBaseDisposition(observerStats.getBaseDisposition() + static_cast(dispTerm)); - - // Set the crime ID, which we will use to calm down participants - // once the bounty has been paid. - observerStats.setCrimeId(id); + setCrimeId = true; // Mark as Alarmed for dialogue observerStats.setAlarmed(true); } } + + // Set the crime ID, which we will use to calm down participants + // once the bounty has been paid and restore their disposition to player character. + if (setCrimeId) + observerStats.setCrimeId(id); } if (reported) { - player.getClass().getNpcStats(player).setBounty(player.getClass().getNpcStats(player).getBounty() + arg); + player.getClass().getNpcStats(player).setBounty( + std::max(0, player.getClass().getNpcStats(player).getBounty() + arg)); // If committing a crime against a faction member, expell from the faction if (!victim.isEmpty() && victim.getClass().isNpc()) @@ -1404,26 +1472,10 @@ namespace MWMechanics if (target == player || !attacker.getClass().isActor()) return false; - MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); - if (attacker == player) - { - std::set followersAttacker; - getActorsSidingWith(attacker, followersAttacker); - if (followersAttacker.find(target) != followersAttacker.end()) - { - statsTarget.friendlyHit(); - - if (statsTarget.getFriendlyHits() < 4) - { - MWBase::Environment::get().getDialogueManager()->say(target, ESM::RefId::stringRefId("hit")); - return false; - } - } - } - if (canCommitCrimeAgainst(target, attacker)) commitCrime(attacker, target, MWBase::MechanicsManager::OT_Assault); + MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); AiSequence& seq = statsTarget.getAiSequence(); if (!attacker.isEmpty() @@ -1466,10 +1518,13 @@ namespace MWMechanics bool MechanicsManager::canCommitCrimeAgainst(const MWWorld::Ptr& target, const MWWorld::Ptr& attacker) { - const MWMechanics::AiSequence& seq = target.getClass().getCreatureStats(target).getAiSequence(); - return target.getClass().isNpc() && !attacker.isEmpty() && !seq.isInCombat(attacker) - && !isAggressive(target, attacker) && !seq.isEngagedWithActor() - && !target.getClass().getCreatureStats(target).getAiSequence().isInPursuit(); + const MWWorld::Class& cls = target.getClass(); + const MWMechanics::CreatureStats& stats = cls.getCreatureStats(target); + const MWMechanics::AiSequence& seq = stats.getAiSequence(); + return cls.isNpc() && !attacker.isEmpty() && !seq.isInCombat(attacker) && !isAggressive(target, attacker) + && !seq.isEngagedWithActor() && !stats.getAiSequence().isInPursuit() + && !cls.getNpcStats(target).isWerewolf() + && stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Vampirism).getMagnitude() <= 0; } void MechanicsManager::actorKilled(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) @@ -1920,7 +1975,8 @@ namespace MWMechanics if (reported) { - npcStats.setBounty(npcStats.getBounty() + gmst.find("iWereWolfBounty")->mValue.getInteger()); + npcStats.setBounty( + std::max(0, npcStats.getBounty() + gmst.find("iWereWolfBounty")->mValue.getInteger())); } } } diff --git a/apps/openmw/mwmechanics/npcstats.cpp b/apps/openmw/mwmechanics/npcstats.cpp index b9df650fc3..808059fccd 100644 --- a/apps/openmw/mwmechanics/npcstats.cpp +++ b/apps/openmw/mwmechanics/npcstats.cpp @@ -20,6 +20,7 @@ MWMechanics::NpcStats::NpcStats() : mDisposition(0) + , mCrimeDispositionModifier(0) , mReputation(0) , mCrimeId(-1) , mBounty(0) @@ -43,6 +44,21 @@ void MWMechanics::NpcStats::setBaseDisposition(int disposition) mDisposition = disposition; } +int MWMechanics::NpcStats::getCrimeDispositionModifier() const +{ + return mCrimeDispositionModifier; +} + +void MWMechanics::NpcStats::setCrimeDispositionModifier(int value) +{ + mCrimeDispositionModifier = value; +} + +void MWMechanics::NpcStats::modCrimeDispositionModifier(int value) +{ + mCrimeDispositionModifier += value; +} + const MWMechanics::SkillValue& MWMechanics::NpcStats::getSkill(ESM::RefId id) const { auto it = mSkills.find(id); @@ -464,6 +480,7 @@ void MWMechanics::NpcStats::writeState(ESM::NpcStats& state) const state.mFactions[iter->first].mRank = iter->second; state.mDisposition = mDisposition; + state.mCrimeDispositionModifier = mCrimeDispositionModifier; for (const auto& [id, value] : mSkills) { @@ -528,6 +545,7 @@ void MWMechanics::NpcStats::readState(const ESM::NpcStats& state) } mDisposition = state.mDisposition; + mCrimeDispositionModifier = state.mCrimeDispositionModifier; for (size_t i = 0; i < state.mSkills.size(); ++i) { diff --git a/apps/openmw/mwmechanics/npcstats.hpp b/apps/openmw/mwmechanics/npcstats.hpp index b6a655e84f..7113ee6207 100644 --- a/apps/openmw/mwmechanics/npcstats.hpp +++ b/apps/openmw/mwmechanics/npcstats.hpp @@ -22,6 +22,7 @@ namespace MWMechanics class NpcStats : public CreatureStats { int mDisposition; + int mCrimeDispositionModifier; std::map mSkills; // SkillValue.mProgress used by the player only int mReputation; @@ -54,6 +55,10 @@ namespace MWMechanics int getBaseDisposition() const; void setBaseDisposition(int disposition); + int getCrimeDispositionModifier() const; + void setCrimeDispositionModifier(int value); + void modCrimeDispositionModifier(int value); + int getReputation() const; void setReputation(int reputation); diff --git a/apps/openmw/mwmechanics/recharge.cpp b/apps/openmw/mwmechanics/recharge.cpp index 9c42e4088c..6e16436bcc 100644 --- a/apps/openmw/mwmechanics/recharge.cpp +++ b/apps/openmw/mwmechanics/recharge.cpp @@ -38,7 +38,7 @@ namespace MWMechanics bool rechargeItem(const MWWorld::Ptr& item, const MWWorld::Ptr& gem) { - if (!gem.getRefData().getCount()) + if (!gem.getCellRef().getCount()) return false; MWWorld::Ptr player = MWMechanics::getPlayer(); @@ -87,7 +87,7 @@ namespace MWMechanics player.getClass().skillUsageSucceeded(player, ESM::Skill::Enchant, 0); gem.getContainerStore()->remove(gem, 1); - if (gem.getRefData().getCount() == 0) + if (gem.getCellRef().getCount() == 0) { std::string message = MWBase::Environment::get() .getESMStore() diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 52e371b6e9..e4e07b162f 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -41,7 +41,6 @@ namespace MWMechanics const ESM::EffectList& effects, const MWWorld::Ptr& ignore, ESM::RangeType rangeType) const { const auto world = MWBase::Environment::get().getWorld(); - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); std::map> toApply; int index = -1; for (const ESM::ENAMstruct& effectInfo : effects.mList) @@ -75,12 +74,12 @@ namespace MWMechanics { if (effectInfo.mRange == ESM::RT_Target) world->spawnEffect( - Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel, vfs), texture, mHitPosition, 1.0f); + Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, 1.0f); continue; } else - world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel, vfs), texture, - mHitPosition, static_cast(effectInfo.mArea * 2)); + world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, + static_cast(effectInfo.mArea * 2)); // Play explosion sound (make sure to use NoTrack, since we will delete the projectile now) { @@ -532,7 +531,6 @@ namespace MWMechanics { const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); std::vector addedEffects; - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); for (const ESM::ENAMstruct& effectData : effects) { @@ -547,15 +545,15 @@ namespace MWMechanics // check if the effect was already added if (std::find(addedEffects.begin(), addedEffects.end(), - Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs)) + Misc::ResourceHelpers::correctMeshPath(castStatic->mModel)) != addedEffects.end()) continue; MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster); if (animation) { - animation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), effect->mIndex, - false, {}, effect->mParticle); + animation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), effect->mIndex, false, + {}, effect->mParticle); } else { @@ -585,13 +583,13 @@ namespace MWMechanics } scale = std::max(scale, 1.f); MWBase::Environment::get().getWorld()->spawnEffect( - Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), effect->mParticle, pos, scale); + Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), effect->mParticle, pos, scale); } if (animation && !mCaster.getClass().isActor()) animation->addSpellCastGlow(effect); - addedEffects.push_back(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs)); + addedEffects.push_back(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel)); MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); if (!effect->mCastSound.empty()) @@ -628,11 +626,8 @@ namespace MWMechanics { // Don't play particle VFX unless the effect is new or it should be looping. if (playNonLooping || loop) - { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - anim->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), magicEffect.mIndex, - loop, {}, magicEffect.mParticle); - } + anim->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), magicEffect.mIndex, loop, + {}, magicEffect.mParticle); } } } diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index db9ec3e588..e7146f3e7a 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -285,11 +285,8 @@ namespace const ESM::Static* absorbStatic = esmStore.get().find(ESM::RefId::stringRefId("VFX_Absorb")); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); if (animation && !absorbStatic->mModel.empty()) - { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - animation->addEffect(Misc::ResourceHelpers::correctMeshPath(absorbStatic->mModel, vfs), - ESM::MagicEffect::SpellAbsorption, false); - } + animation->addEffect( + Misc::ResourceHelpers::correctMeshPath(absorbStatic->mModel), ESM::MagicEffect::SpellAbsorption, false); const ESM::Spell* spell = esmStore.get().search(spellId); int spellCost = 0; if (spell) @@ -356,7 +353,8 @@ namespace // Notify the target actor they've been hit bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) - target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true); + target.getClass().onHit( + target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true, MWMechanics::DamageSourceType::Magical); // Apply resistances if (!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances)) { @@ -461,10 +459,7 @@ namespace MWMechanics const ESM::Static* fx = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Summon_end")); if (fx) - { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel, vfs), -1); - } + anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), -1); } } else if (caster == getPlayer()) diff --git a/apps/openmw/mwmechanics/summoning.cpp b/apps/openmw/mwmechanics/summoning.cpp index 9b641e5e5c..85a8d971a9 100644 --- a/apps/openmw/mwmechanics/summoning.cpp +++ b/apps/openmw/mwmechanics/summoning.cpp @@ -105,10 +105,7 @@ namespace MWMechanics const ESM::Static* fx = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Summon_Start")); if (fx) - { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel, vfs), -1, false); - } + anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), -1, false); } } catch (std::exception& e) diff --git a/apps/openmw/mwmechanics/weapontype.cpp b/apps/openmw/mwmechanics/weapontype.cpp index 9dd5842f58..8c51629803 100644 --- a/apps/openmw/mwmechanics/weapontype.cpp +++ b/apps/openmw/mwmechanics/weapontype.cpp @@ -8,6 +8,8 @@ #include +#include + namespace MWMechanics { template @@ -416,4 +418,18 @@ namespace MWMechanics return &Weapon::getValue(); } + + std::vector getAllWeaponTypeShortGroups() + { + // Go via a set to eliminate duplicates. + std::set shortGroupSet; + for (int type = ESM::Weapon::Type::First; type <= ESM::Weapon::Type::Last; type++) + { + std::string_view shortGroup = getWeaponType(type)->mShortGroup; + if (!shortGroup.empty()) + shortGroupSet.insert(shortGroup); + } + + return std::vector(shortGroupSet.begin(), shortGroupSet.end()); + } } diff --git a/apps/openmw/mwmechanics/weapontype.hpp b/apps/openmw/mwmechanics/weapontype.hpp index db7b3013f6..efe404d327 100644 --- a/apps/openmw/mwmechanics/weapontype.hpp +++ b/apps/openmw/mwmechanics/weapontype.hpp @@ -1,6 +1,9 @@ #ifndef GAME_MWMECHANICS_WEAPONTYPE_H #define GAME_MWMECHANICS_WEAPONTYPE_H +#include +#include + namespace ESM { struct WeaponType; @@ -21,6 +24,8 @@ namespace MWMechanics MWWorld::ContainerStoreIterator getActiveWeapon(const MWWorld::Ptr& actor, int* weaptype); const ESM::WeaponType* getWeaponType(const int weaponType); + + std::vector getAllWeaponTypeShortGroups(); } #endif diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index dec055d68f..e1efe6d242 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -102,7 +102,11 @@ namespace MWPhysics updateScaleUnsafe(); if (!mRotationallyInvariant) - mRotation = mPtr.getRefData().getBaseNode()->getAttitude(); + { + const SceneUtil::PositionAttitudeTransform* baseNode = mPtr.getRefData().getBaseNode(); + if (baseNode) + mRotation = baseNode->getAttitude(); + } addCollisionMask(getCollisionMask()); updateCollisionObjectPositionUnsafe(); diff --git a/apps/openmw/mwphysics/object.cpp b/apps/openmw/mwphysics/object.cpp index 6e0e2cdc7f..9c97ac7c32 100644 --- a/apps/openmw/mwphysics/object.cpp +++ b/apps/openmw/mwphysics/object.cpp @@ -110,6 +110,9 @@ namespace MWPhysics if (mShapeInstance->mAnimatedShapes.empty()) return false; + if (!mPtr.getRefData().getBaseNode()) + return false; + assert(mShapeInstance->mCollisionShape->isCompound()); btCompoundShape* compound = static_cast(mShapeInstance->mCollisionShape.get()); @@ -137,6 +140,7 @@ namespace MWPhysics osg::NodePath& nodePath = nodePathFound->second; osg::Matrixf matrix = osg::computeLocalToWorld(nodePath); + btVector3 scale = Misc::Convert::toBullet(matrix.getScale()); matrix.orthoNormalize(matrix); btTransform transform; @@ -145,8 +149,15 @@ namespace MWPhysics for (int j = 0; j < 3; ++j) transform.getBasis()[i][j] = matrix(j, i); // NB column/row major difference - // Note: we can not apply scaling here for now since we treat scaled shapes - // as new shapes (btScaledBvhTriangleMeshShape) with 1.0 scale for now + btCollisionShape* childShape = compound->getChildShape(shapeIndex); + btVector3 newScale = compound->getLocalScaling() * scale; + + if (childShape->getLocalScaling() != newScale) + { + childShape->setLocalScaling(newScale); + result = true; + } + if (!(transform == compound->getChildTransform(shapeIndex))) { compound->updateChildTransform(shapeIndex, transform); diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index 600ae6f0ed..e31a1eb711 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -130,8 +130,7 @@ namespace MWRender if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor) return std::string(); if (!bodypart->mModel.empty()) - return Misc::ResourceHelpers::correctMeshPath( - bodypart->mModel, MWBase::Environment::get().getResourceSystem()->getVFS()); + return Misc::ResourceHelpers::correctMeshPath(bodypart->mModel); } } } @@ -315,6 +314,7 @@ namespace MWRender if (node == nullptr) return; + // This is used to avoid playing animations intended for equipped weapons on holstered weapons. SceneUtil::ForceControllerSourcesVisitor removeVisitor(std::make_shared()); node->accept(removeVisitor); } @@ -346,9 +346,7 @@ namespace MWRender if (mesh.empty() || boneName.empty()) return; - // If the scabbard is not found, use a weapon mesh as fallback. - // Note: it is unclear how to handle time for controllers attached to bodyparts, so disable them for now. - // We use the similar approach for other bodyparts. + // If the scabbard is not found, use the weapon mesh as fallback. scabbardName = scabbardName.replace(scabbardName.size() - 4, 4, "_sh.nif"); bool isEnchanted = !weapon->getClass().getEnchantment(*weapon).empty(); if (!mResourceSystem->getVFS()->exists(scabbardName)) @@ -428,7 +426,7 @@ namespace MWRender const auto& weaponType = MWMechanics::getWeaponType(type); if (weaponType->mWeaponClass == ESM::WeaponType::Thrown) { - ammoCount = ammo->getRefData().getCount(); + ammoCount = ammo->getCellRef().getCount(); osg::Group* throwingWeaponNode = getBoneByName(weaponType->mAttachBone); if (throwingWeaponNode && throwingWeaponNode->getNumChildren()) ammoCount--; @@ -441,7 +439,7 @@ namespace MWRender if (ammo == inv.end()) return; - ammoCount = ammo->getRefData().getCount(); + ammoCount = ammo->getCellRef().getCount(); bool arrowAttached = isArrowAttached(); if (arrowAttached) ammoCount--; @@ -516,7 +514,7 @@ namespace MWRender ItemLightMap::iterator iter = mItemLights.find(item); if (iter != mItemLights.end()) { - if (!item.getRefData().getCount()) + if (!item.getCellRef().getCount()) { removeHiddenItemLight(item); } diff --git a/apps/openmw/mwrender/actorspaths.hpp b/apps/openmw/mwrender/actorspaths.hpp index d18197b974..239806576f 100644 --- a/apps/openmw/mwrender/actorspaths.hpp +++ b/apps/openmw/mwrender/actorspaths.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_MWRENDER_AGENTSPATHS_H #define OPENMW_MWRENDER_AGENTSPATHS_H -#include +#include "apps/openmw/mwworld/ptr.hpp" #include diff --git a/apps/openmw/mwrender/actorutil.cpp b/apps/openmw/mwrender/actorutil.cpp index 6cef42d60f..8da921e532 100644 --- a/apps/openmw/mwrender/actorutil.cpp +++ b/apps/openmw/mwrender/actorutil.cpp @@ -1,6 +1,7 @@ #include "actorutil.hpp" #include +#include namespace MWRender { @@ -29,4 +30,11 @@ namespace MWRender return Settings::models().mXbaseanim1st; } } + + bool isDefaultActorSkeleton(std::string_view model) + { + return VFS::Path::pathEqual(Settings::models().mBaseanimkna.get(), model) + || VFS::Path::pathEqual(Settings::models().mBaseanimfemale.get(), model) + || VFS::Path::pathEqual(Settings::models().mBaseanim.get(), model); + } } diff --git a/apps/openmw/mwrender/actorutil.hpp b/apps/openmw/mwrender/actorutil.hpp index bbffc4ad24..3107bf0183 100644 --- a/apps/openmw/mwrender/actorutil.hpp +++ b/apps/openmw/mwrender/actorutil.hpp @@ -2,10 +2,12 @@ #define OPENMW_APPS_OPENMW_MWRENDER_ACTORUTIL_H #include +#include namespace MWRender { const std::string& getActorSkeleton(bool firstPerson, bool female, bool beast, bool werewolf); + bool isDefaultActorSkeleton(std::string_view model); } #endif diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index bac9dbb56c..7fa8e43c37 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -529,6 +529,7 @@ namespace MWRender , mBodyPitchRadians(0.f) , mHasMagicEffects(false) , mAlpha(1.f) + , mPlayScriptedOnly(false) { for (size_t i = 0; i < sNumBlendMasks; i++) mAnimationTimePtr[i] = std::make_shared(); @@ -1020,7 +1021,7 @@ namespace MWRender return false; } - bool Animation::getInfo(std::string_view groupname, float* complete, float* speedmult) const + bool Animation::getInfo(std::string_view groupname, float* complete, float* speedmult, size_t* loopcount) const { AnimStateMap::const_iterator iter = mStates.find(groupname); if (iter == mStates.end()) @@ -1029,6 +1030,8 @@ namespace MWRender *complete = 0.0f; if (speedmult) *speedmult = 0.0f; + if (loopcount) + *loopcount = 0; return false; } @@ -1042,6 +1045,9 @@ namespace MWRender } if (speedmult) *speedmult = iter->second.mSpeedMult; + + if (loopcount) + *loopcount = iter->second.mLoopCount; return true; } @@ -1054,15 +1060,6 @@ namespace MWRender return iter->second.getTime(); } - size_t Animation::getCurrentLoopCount(const std::string& groupname) const - { - AnimStateMap::const_iterator iter = mStates.find(groupname); - if (iter == mStates.end()) - return 0; - - return iter->second.mLoopCount; - } - void Animation::disable(std::string_view groupname) { AnimStateMap::iterator iter = mStates.find(groupname); @@ -1141,23 +1138,12 @@ namespace MWRender osg::Vec3f Animation::runAnimation(float duration) { - // If we have scripted animations, play only them - bool hasScriptedAnims = false; - for (AnimStateMap::iterator stateiter = mStates.begin(); stateiter != mStates.end(); stateiter++) - { - if (stateiter->second.mPriority.contains(int(MWMechanics::Priority_Scripted)) && stateiter->second.mPlaying) - { - hasScriptedAnims = true; - break; - } - } - osg::Vec3f movement(0.f, 0.f, 0.f); AnimStateMap::iterator stateiter = mStates.begin(); while (stateiter != mStates.end()) { AnimState& state = stateiter->second; - if (hasScriptedAnims && !state.mPriority.contains(int(MWMechanics::Priority_Scripted))) + if (mPlayScriptedOnly && !state.mPriority.contains(MWMechanics::Priority_Scripted)) { ++stateiter; continue; @@ -1263,10 +1249,6 @@ namespace MWRender osg::Quat(mHeadPitchRadians, osg::Vec3f(1, 0, 0)) * osg::Quat(yaw, osg::Vec3f(0, 0, 1))); } - // Scripted animations should not cause movement - if (hasScriptedAnims) - return osg::Vec3f(0, 0, 0); - return movement; } @@ -1593,7 +1575,7 @@ namespace MWRender // Notify that this animation has attached magic effects mHasMagicEffects = true; - overrideFirstRootTexture(texture, mResourceSystem, node); + overrideFirstRootTexture(texture, mResourceSystem, *node); } void Animation::removeEffect(int effectId) diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 8615811cc3..24366889c4 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -292,6 +292,8 @@ namespace MWRender osg::ref_ptr mLightListCallback; + bool mPlayScriptedOnly; + const NodeMap& getNodeMap() const; /* Sets the appropriate animations on the bone groups based on priority. @@ -441,7 +443,8 @@ namespace MWRender * \param speedmult Stores the animation speed multiplier * \return True if the animation is active, false otherwise. */ - bool getInfo(std::string_view groupname, float* complete = nullptr, float* speedmult = nullptr) const; + bool getInfo(std::string_view groupname, float* complete = nullptr, float* speedmult = nullptr, + size_t* loopcount = nullptr) const; /// Get the absolute position in the animation track of the first text key with the given group. float getStartTime(const std::string& groupname) const; @@ -453,8 +456,6 @@ namespace MWRender /// the given group. float getCurrentTime(const std::string& groupname) const; - size_t getCurrentLoopCount(const std::string& groupname) const; - /** Disables the specified animation group; * \param groupname Animation group to disable. */ @@ -477,6 +478,9 @@ namespace MWRender MWWorld::MovementDirectionFlags getSupportedMovementDirections( std::span prefixes) const; + bool getPlayScriptedOnly() const { return mPlayScriptedOnly; } + void setPlayScriptedOnly(bool playScriptedOnly) { mPlayScriptedOnly = playScriptedOnly; } + virtual bool useShieldAnimations() const { return false; } virtual bool getWeaponsShown() const { return false; } virtual void showWeapons(bool showWeapon) {} diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index 7767520715..040ba320a1 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -168,7 +168,7 @@ namespace MWRender if (slot == MWWorld::InventoryStore::Slot_CarriedRight) source = mWeaponAnimationTime; else - source = std::make_shared(); + source = mAnimationTimePtr[0]; SceneUtil::AssignControllerSourcesVisitor assignVisitor(source); attached->accept(assignVisitor); diff --git a/apps/openmw/mwrender/distortion.cpp b/apps/openmw/mwrender/distortion.cpp new file mode 100644 index 0000000000..2ca2ace65b --- /dev/null +++ b/apps/openmw/mwrender/distortion.cpp @@ -0,0 +1,28 @@ +#include "distortion.hpp" + +#include + +namespace MWRender +{ + void DistortionCallback::drawImplementation( + osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) + { + osg::State* state = renderInfo.getState(); + size_t frameId = state->getFrameStamp()->getFrameNumber() % 2; + + mFBO[frameId]->apply(*state); + + const osg::Texture* tex + = mFBO[frameId]->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0).getTexture(); + + glViewport(0, 0, tex->getTextureWidth(), tex->getTextureHeight()); + glClearColor(0.0, 0.0, 0.0, 1.0); + glClear(GL_COLOR_BUFFER_BIT); + + bin->drawImplementation(renderInfo, previous); + + tex = mOriginalFBO[frameId]->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0).getTexture(); + glViewport(0, 0, tex->getTextureWidth(), tex->getTextureHeight()); + mOriginalFBO[frameId]->apply(*state); + } +} diff --git a/apps/openmw/mwrender/distortion.hpp b/apps/openmw/mwrender/distortion.hpp new file mode 100644 index 0000000000..736f4ea6f2 --- /dev/null +++ b/apps/openmw/mwrender/distortion.hpp @@ -0,0 +1,28 @@ +#include + +#include + +namespace osg +{ + class FrameBufferObject; +} + +namespace MWRender +{ + class DistortionCallback : public osgUtil::RenderBin::DrawCallback + { + public: + void drawImplementation( + osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override; + + void setFBO(const osg::ref_ptr& fbo, size_t frameId) { mFBO[frameId] = fbo; } + void setOriginalFBO(const osg::ref_ptr& fbo, size_t frameId) + { + mOriginalFBO[frameId] = fbo; + } + + private: + std::array, 2> mFBO; + std::array, 2> mOriginalFBO; + }; +} diff --git a/apps/openmw/mwrender/effectmanager.cpp b/apps/openmw/mwrender/effectmanager.cpp index e5b8431c84..83a4091402 100644 --- a/apps/openmw/mwrender/effectmanager.cpp +++ b/apps/openmw/mwrender/effectmanager.cpp @@ -52,9 +52,9 @@ namespace MWRender node->accept(assignVisitor); if (isMagicVFX) - overrideFirstRootTexture(textureOverride, mResourceSystem, node); + overrideFirstRootTexture(textureOverride, mResourceSystem, *node); else - overrideTexture(textureOverride, mResourceSystem, node); + overrideTexture(textureOverride, mResourceSystem, *node); mParentNode->addChild(trans); diff --git a/apps/openmw/mwrender/esm4npcanimation.cpp b/apps/openmw/mwrender/esm4npcanimation.cpp index 3ea8f829ce..7771735920 100644 --- a/apps/openmw/mwrender/esm4npcanimation.cpp +++ b/apps/openmw/mwrender/esm4npcanimation.cpp @@ -12,8 +12,8 @@ #include #include +#include "../mwbase/environment.hpp" #include "../mwclass/esm4npc.hpp" -#include "../mwworld/customdata.hpp" #include "../mwworld/esmstore.hpp" namespace MWRender @@ -28,11 +28,13 @@ namespace MWRender void ESM4NpcAnimation::updateParts() { - if (!mObjectRoot.get()) + if (mObjectRoot == nullptr) return; const ESM4::Npc* traits = MWClass::ESM4Npc::getTraitsRecord(mPtr); + if (traits == nullptr) + return; if (traits->mIsTES4) - updatePartsTES4(); + updatePartsTES4(*traits); else if (traits->mIsFONV) { // Not implemented yet @@ -42,7 +44,7 @@ namespace MWRender // There is no easy way to distinguish TES5 and FO3. // In case of FO3 the function shouldn't crash the game and will // only lead to the NPC not being rendered. - updatePartsTES5(); + updatePartsTES5(*traits); } } @@ -51,7 +53,7 @@ namespace MWRender if (model.empty()) return; mResourceSystem->getSceneManager()->getInstance( - Misc::ResourceHelpers::correctMeshPath(model, mResourceSystem->getVFS()), mObjectRoot.get()); + Misc::ResourceHelpers::correctMeshPath(model), mObjectRoot.get()); } template @@ -65,9 +67,8 @@ namespace MWRender return rec->mModel; } - void ESM4NpcAnimation::updatePartsTES4() + void ESM4NpcAnimation::updatePartsTES4(const ESM4::Npc& traits) { - const ESM4::Npc* traits = MWClass::ESM4Npc::getTraitsRecord(mPtr); const ESM4::Race* race = MWClass::ESM4Npc::getRace(mPtr); bool isFemale = MWClass::ESM4Npc::isFemale(mPtr); @@ -77,13 +78,13 @@ namespace MWRender insertPart(bodyPart.mesh); for (const ESM4::Race::BodyPart& bodyPart : race->mHeadParts) insertPart(bodyPart.mesh); - if (!traits->mHair.isZeroOrUnset()) + if (!traits.mHair.isZeroOrUnset()) { const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); - if (const ESM4::Hair* hair = store->get().search(traits->mHair)) + if (const ESM4::Hair* hair = store->get().search(traits.mHair)) insertPart(hair->mModel); else - Log(Debug::Error) << "Hair not found: " << ESM::RefId(traits->mHair); + Log(Debug::Error) << "Hair not found: " << ESM::RefId(traits.mHair); } for (const ESM4::Armor* armor : MWClass::ESM4Npc::getEquippedArmor(mPtr)) @@ -111,11 +112,10 @@ namespace MWRender } } - void ESM4NpcAnimation::updatePartsTES5() + void ESM4NpcAnimation::updatePartsTES5(const ESM4::Npc& traits) { const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); - const ESM4::Npc* traits = MWClass::ESM4Npc::getTraitsRecord(mPtr); const ESM4::Race* race = MWClass::ESM4Npc::getRace(mPtr); bool isFemale = MWClass::ESM4Npc::isFemale(mPtr); @@ -132,9 +132,9 @@ namespace MWRender Log(Debug::Error) << "ArmorAddon not found: " << ESM::RefId(armaId); continue; } - bool compatibleRace = arma->mRacePrimary == traits->mRace; + bool compatibleRace = arma->mRacePrimary == traits.mRace; for (auto r : arma->mRaces) - if (r == traits->mRace) + if (r == traits.mRace) compatibleRace = true; if (compatibleRace) armorAddons.push_back(arma); @@ -143,12 +143,12 @@ namespace MWRender for (const ESM4::Armor* armor : MWClass::ESM4Npc::getEquippedArmor(mPtr)) findArmorAddons(armor); - if (!traits->mWornArmor.isZeroOrUnset()) + if (!traits.mWornArmor.isZeroOrUnset()) { - if (const ESM4::Armor* armor = store->get().search(traits->mWornArmor)) + if (const ESM4::Armor* armor = store->get().search(traits.mWornArmor)) findArmorAddons(armor); else - Log(Debug::Error) << "Worn armor not found: " << ESM::RefId(traits->mWornArmor); + Log(Debug::Error) << "Worn armor not found: " << ESM::RefId(traits.mWornArmor); } if (!race->mSkin.isZeroOrUnset()) { @@ -183,7 +183,7 @@ namespace MWRender std::set usedHeadPartTypes; if (usedParts & ESM4::Armor::TES5_Hair) usedHeadPartTypes.insert(ESM4::HeadPart::Type_Hair); - insertHeadParts(traits->mHeadParts, usedHeadPartTypes); + insertHeadParts(traits.mHeadParts, usedHeadPartTypes); insertHeadParts(isFemale ? race->mHeadPartIdsFemale : race->mHeadPartIdsMale, usedHeadPartTypes); } } diff --git a/apps/openmw/mwrender/esm4npcanimation.hpp b/apps/openmw/mwrender/esm4npcanimation.hpp index 7bb3fe1103..274c060b06 100644 --- a/apps/openmw/mwrender/esm4npcanimation.hpp +++ b/apps/openmw/mwrender/esm4npcanimation.hpp @@ -3,6 +3,11 @@ #include "animation.hpp" +namespace ESM4 +{ + struct Npc; +} + namespace MWRender { class ESM4NpcAnimation : public Animation @@ -18,8 +23,8 @@ namespace MWRender void insertHeadParts(const std::vector& partIds, std::set& usedHeadPartTypes); void updateParts(); - void updatePartsTES4(); - void updatePartsTES5(); + void updatePartsTES4(const ESM4::Npc& traits); + void updatePartsTES5(const ESM4::Npc& traits); }; } diff --git a/apps/openmw/mwrender/fogmanager.cpp b/apps/openmw/mwrender/fogmanager.cpp index ef8de1cb2e..b75fb507ed 100644 --- a/apps/openmw/mwrender/fogmanager.cpp +++ b/apps/openmw/mwrender/fogmanager.cpp @@ -9,7 +9,7 @@ #include #include -#include +#include "apps/openmw/mwworld/cell.hpp" namespace MWRender { diff --git a/apps/openmw/mwrender/globalmap.hpp b/apps/openmw/mwrender/globalmap.hpp index e0582b20fa..07d7731e31 100644 --- a/apps/openmw/mwrender/globalmap.hpp +++ b/apps/openmw/mwrender/globalmap.hpp @@ -111,8 +111,6 @@ namespace MWRender ImageDestMap mPendingImageDest; - std::vector> mExploredCells; - osg::ref_ptr mBaseTexture; osg::ref_ptr mAlphaTexture; diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 669a6fae45..84522ee86e 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -48,7 +48,7 @@ namespace { - std::string getVampireHead(const ESM::RefId& race, bool female, const VFS::Manager& vfs) + std::string getVampireHead(const ESM::RefId& race, bool female) { static std::map, const ESM::BodyPart*> sVampireMapping; @@ -78,7 +78,7 @@ namespace const ESM::BodyPart* bodyPart = sVampireMapping[thisCombination]; if (!bodyPart) return std::string(); - return Misc::ResourceHelpers::correctMeshPath(bodyPart->mModel, &vfs); + return Misc::ResourceHelpers::correctMeshPath(bodyPart->mModel); } } @@ -345,11 +345,9 @@ namespace MWRender bin->drawImplementation(renderInfo, previous); auto primaryFBO = postProcessor->getPrimaryFbo(frameId); + primaryFBO->apply(*state); - if (postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)) - postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)->apply(*state); - else - primaryFBO->apply(*state); + postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)->apply(*state); // depth accumulation pass osg::ref_ptr restore = bin->getStateSet(); @@ -357,8 +355,7 @@ namespace MWRender bin->drawImplementation(renderInfo, previous); bin->setStateSet(restore); - if (postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)) - primaryFBO->apply(*state); + primaryFBO->apply(*state); state->checkGLErrors("after DepthClearCallback::drawImplementation"); } @@ -470,7 +467,7 @@ namespace MWRender { const ESM::BodyPart* bp = store.get().search(headName); if (bp) - mHeadModel = Misc::ResourceHelpers::correctMeshPath(bp->mModel, mResourceSystem->getVFS()); + mHeadModel = Misc::ResourceHelpers::correctMeshPath(bp->mModel); else Log(Debug::Warning) << "Warning: Failed to load body part '" << headName << "'"; } @@ -479,12 +476,12 @@ namespace MWRender { const ESM::BodyPart* bp = store.get().search(hairName); if (bp) - mHairModel = Misc::ResourceHelpers::correctMeshPath(bp->mModel, mResourceSystem->getVFS()); + mHairModel = Misc::ResourceHelpers::correctMeshPath(bp->mModel); else Log(Debug::Warning) << "Warning: Failed to load body part '" << hairName << "'"; } - const std::string vampireHead = getVampireHead(mNpc->mRace, isFemale, *mResourceSystem->getVFS()); + const std::string vampireHead = getVampireHead(mNpc->mRace, isFemale); if (!isWerewolf && isVampire && !vampireHead.empty()) mHeadModel = vampireHead; @@ -495,10 +492,13 @@ namespace MWRender getActorSkeleton(is1stPerson, isFemale, isBeast, isWerewolf), mResourceSystem->getVFS()); std::string smodel = defaultSkeleton; + bool isBase = !isWerewolf; if (!is1stPerson && !isWerewolf && !mNpc->mModel.empty()) - smodel = Misc::ResourceHelpers::correctActorModelPath( - Misc::ResourceHelpers::correctMeshPath(mNpc->mModel, mResourceSystem->getVFS()), - mResourceSystem->getVFS()); + { + std::string model = Misc::ResourceHelpers::correctMeshPath(mNpc->mModel); + isBase = isDefaultActorSkeleton(model); + smodel = Misc::ResourceHelpers::correctActorModelPath(model, mResourceSystem->getVFS()); + } setObjectRoot(smodel, true, true, false); @@ -507,24 +507,30 @@ namespace MWRender if (!is1stPerson) { const std::string& base = Settings::models().mXbaseanim; - if (smodel != base && !isWerewolf) + if (!isWerewolf) addAnimSource(base, smodel); - if (smodel != defaultSkeleton && base != defaultSkeleton) + if (!isBase) + { addAnimSource(defaultSkeleton, smodel); + addAnimSource(smodel, smodel); + } + else if (base != defaultSkeleton) + { + addAnimSource(defaultSkeleton, smodel); + } - addAnimSource(smodel, smodel); - - if (!isWerewolf && mNpc->mRace.contains("argonian")) + if (!isWerewolf && isBeast && mNpc->mRace.contains("argonian")) addAnimSource("meshes\\xargonian_swimkna.nif", smodel); } else { const std::string& base = Settings::models().mXbaseanim1st; - if (smodel != base && !isWerewolf) + if (!isWerewolf) addAnimSource(base, smodel); - addAnimSource(smodel, smodel); + if (!isBase) + addAnimSource(smodel, smodel); mObjectRoot->setNodeMask(Mask_FirstPerson); mObjectRoot->addCullCallback(new OverrideFieldOfViewCallback(mFirstPersonFieldOfView)); @@ -649,9 +655,8 @@ namespace MWRender if (store != inv.end() && (part = *store).getType() == ESM::Light::sRecordId) { const ESM::Light* light = part.get()->mBase; - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1, - Misc::ResourceHelpers::correctMeshPath(light->mModel, vfs), false, nullptr, true); + Misc::ResourceHelpers::correctMeshPath(light->mModel), false, nullptr, true); if (mObjectParts[ESM::PRT_Shield]) addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), SceneUtil::LightCommon(*light)); } @@ -669,13 +674,9 @@ namespace MWRender { if (mPartPriorities[part] < 1) { - const ESM::BodyPart* bodypart = parts[part]; - if (bodypart) - { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + if (const ESM::BodyPart* bodypart = parts[part]) addOrReplaceIndividualPart(static_cast(part), -1, 1, - Misc::ResourceHelpers::correctMeshPath(bodypart->mModel, vfs)); - } + Misc::ResourceHelpers::correctMeshPath(bodypart->mModel)); } } @@ -851,7 +852,7 @@ namespace MWRender if (type == ESM::PRT_Weapon) src = mWeaponAnimationTime; else - src = std::make_shared(); + src = mAnimationTimePtr[0]; SceneUtil::AssignControllerSourcesVisitor assignVisitor(src); node->accept(assignVisitor); } @@ -904,11 +905,8 @@ namespace MWRender } if (bodypart) - { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); addOrReplaceIndividualPart(static_cast(part.mPart), group, priority, - Misc::ResourceHelpers::correctMeshPath(bodypart->mModel, vfs), enchantedGlow, glowColor); - } + Misc::ResourceHelpers::correctMeshPath(bodypart->mModel), enchantedGlow, glowColor); else reserveIndividualPart((ESM::PartReferenceType)part.mPart, group, priority); } @@ -923,13 +921,18 @@ namespace MWRender if (mViewMode == VM_FirstPerson) { - NodeMap::iterator found = mNodeMap.find("bip01 neck"); - if (found != mNodeMap.end()) + // If there is no active animation, then the bip01 neck node will not be updated each frame, and the + // RotateController will accumulate rotations. + if (mStates.size() > 0) { - osg::MatrixTransform* node = found->second.get(); - mFirstPersonNeckController = new RotateController(mObjectRoot.get()); - node->addUpdateCallback(mFirstPersonNeckController); - mActiveControllers.emplace_back(node, mFirstPersonNeckController); + NodeMap::iterator found = mNodeMap.find("bip01 neck"); + if (found != mNodeMap.end()) + { + osg::MatrixTransform* node = found->second.get(); + mFirstPersonNeckController = new RotateController(mObjectRoot.get()); + node->addUpdateCallback(mFirstPersonNeckController); + mActiveControllers.emplace_back(node, mFirstPersonNeckController); + } } } else if (mViewMode == VM_Normal) diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index 99ebb94647..f1bccc13c9 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -623,7 +623,7 @@ namespace MWRender std::string model = getModel(type, ref.mRefID, store); if (model.empty()) continue; - model = Misc::ResourceHelpers::correctMeshPath(model, mSceneManager->getVFS()); + model = Misc::ResourceHelpers::correctMeshPath(model); if (activeGrid && type != ESM::REC_STAT) { diff --git a/apps/openmw/mwrender/pingpongcanvas.cpp b/apps/openmw/mwrender/pingpongcanvas.cpp index 4fecdf87f9..9c8b08adfd 100644 --- a/apps/openmw/mwrender/pingpongcanvas.cpp +++ b/apps/openmw/mwrender/pingpongcanvas.cpp @@ -242,6 +242,10 @@ namespace MWRender if (mTextureNormals) node.mRootStateSet->setTextureAttribute(PostProcessor::TextureUnits::Unit_Normals, mTextureNormals); + if (mTextureDistortion) + node.mRootStateSet->setTextureAttribute( + PostProcessor::TextureUnits::Unit_Distortion, mTextureDistortion); + state.pushStateSet(node.mRootStateSet); state.apply(); diff --git a/apps/openmw/mwrender/pingpongcanvas.hpp b/apps/openmw/mwrender/pingpongcanvas.hpp index a03e3591ae..5a37b7fbc9 100644 --- a/apps/openmw/mwrender/pingpongcanvas.hpp +++ b/apps/openmw/mwrender/pingpongcanvas.hpp @@ -48,6 +48,8 @@ namespace MWRender void setTextureNormals(osg::ref_ptr tex) { mTextureNormals = tex; } + void setTextureDistortion(osg::ref_ptr tex) { mTextureDistortion = tex; } + void setCalculateAvgLum(bool enabled) { mAvgLum = enabled; } void setPostProcessing(bool enabled) { mPostprocessing = enabled; } @@ -59,7 +61,7 @@ namespace MWRender bool mPostprocessing = false; fx::DispatchArray mPasses; - fx::FlagsType mMask; + fx::FlagsType mMask = 0; osg::ref_ptr mFallbackProgram; osg::ref_ptr mMultiviewResolveProgram; @@ -69,6 +71,7 @@ namespace MWRender osg::ref_ptr mTextureScene; osg::ref_ptr mTextureDepth; osg::ref_ptr mTextureNormals; + osg::ref_ptr mTextureDistortion; mutable bool mDirty = false; mutable std::vector mDirtyAttachments; diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp index 2c77981244..1aaeb460b7 100644 --- a/apps/openmw/mwrender/postprocessor.cpp +++ b/apps/openmw/mwrender/postprocessor.cpp @@ -29,7 +29,9 @@ #include "../mwgui/postprocessorhud.hpp" +#include "distortion.hpp" #include "pingpongcull.hpp" +#include "renderbin.hpp" #include "renderingmanager.hpp" #include "sky.hpp" #include "transparentpass.hpp" @@ -103,6 +105,8 @@ namespace return Stereo::createMultiviewCompatibleAttachment(texture); } + + constexpr float DistortionRatio = 0.25; } namespace MWRender @@ -118,6 +122,7 @@ namespace MWRender , mUsePostProcessing(Settings::postProcessing().mEnabled) , mSamples(Settings::video().mAntialiasing) , mPingPongCull(new PingPongCull(this)) + , mDistortionCallback(new DistortionCallback) { auto& shaderManager = mRendering.getResourceSystem()->getSceneManager()->getShaderManager(); @@ -141,18 +146,45 @@ namespace MWRender mHUDCamera->setCullCallback(new HUDCullCallback); mViewer->getCamera()->addCullCallback(mPingPongCull); - if (Settings::shaders().mSoftParticles || Settings::postProcessing().mTransparentPostpass) - { - mTransparentDepthPostPass - = new TransparentDepthBinCallback(shaderManager, Settings::postProcessing().mTransparentPostpass); - osgUtil::RenderBin::getRenderBinPrototype("DepthSortedBin")->setDrawCallback(mTransparentDepthPostPass); - } + // resolves the multisampled depth buffer and optionally draws an additional depth postpass + mTransparentDepthPostPass + = new TransparentDepthBinCallback(mRendering.getResourceSystem()->getSceneManager()->getShaderManager(), + Settings::postProcessing().mTransparentPostpass); + osgUtil::RenderBin::getRenderBinPrototype("DepthSortedBin")->setDrawCallback(mTransparentDepthPostPass); + + osg::ref_ptr distortionRenderBin + = new osgUtil::RenderBin(osgUtil::RenderBin::SORT_BACK_TO_FRONT); + // This is silly to have to do, but if nothing is drawn then the drawcallback is never called and the distortion + // texture will never be cleared + osg::ref_ptr dummyNodeToClear = new osg::Node; + dummyNodeToClear->setCullingActive(false); + dummyNodeToClear->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Distortion, "Distortion"); + rootNode->addChild(dummyNodeToClear); + distortionRenderBin->setDrawCallback(mDistortionCallback); + distortionRenderBin->getStateSet()->setDefine("DISTORTION", "1", osg::StateAttribute::ON); + + // Give the renderbin access to the opaque depth sampler so it can write its occlusion + // Distorted geometry is drawn with ALWAYS depth function and depths writes disbled. + const int unitSoftEffect + = shaderManager.reserveGlobalTextureUnits(Shader::ShaderManager::Slot::OpaqueDepthTexture); + distortionRenderBin->getStateSet()->addUniform(new osg::Uniform("opaqueDepthTex", unitSoftEffect)); + + osgUtil::RenderBin::addRenderBinPrototype("Distortion", distortionRenderBin); + + auto defines = shaderManager.getGlobalDefines(); + defines["distorionRTRatio"] = std::to_string(DistortionRatio); + shaderManager.setGlobalDefines(defines); createObjectsForFrame(0); createObjectsForFrame(1); populateTechniqueFiles(); + auto distortion = loadTechnique("internal_distortion"); + distortion->setInternal(true); + distortion->setLocked(true); + mInternalTechniques.push_back(distortion); + osg::GraphicsContext* gc = viewer->getCamera()->getGraphicsContext(); osg::GLExtensions* ext = gc->getState()->get(); @@ -171,19 +203,6 @@ namespace MWRender else Log(Debug::Error) << "'glDisablei' unsupported, pass normals will not be available to shaders."; - if (Settings::shaders().mSoftParticles) - { - for (int i = 0; i < 2; ++i) - { - if (Stereo::getMultiview()) - mTextures[i][Tex_OpaqueDepth] = new osg::Texture2DArray; - else - mTextures[i][Tex_OpaqueDepth] = new osg::Texture2D; - mTextures[i][Tex_OpaqueDepth]->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mTextures[i][Tex_OpaqueDepth]->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - } - } - mGLSLVersion = ext->glslLanguageVersion * 100; mUBO = ext->isUniformBufferObjectSupported && mGLSLVersion >= 330; mStateUpdater = new fx::StateUpdater(mUBO); @@ -281,17 +300,15 @@ namespace MWRender mCanvases[frameId]->setCalculateAvgLum(mHDR); mCanvases[frameId]->setTextureScene(getTexture(Tex_Scene, frameId)); - if (mTransparentDepthPostPass) - mCanvases[frameId]->setTextureDepth(getTexture(Tex_OpaqueDepth, frameId)); - else - mCanvases[frameId]->setTextureDepth(getTexture(Tex_Depth, frameId)); + mCanvases[frameId]->setTextureDepth(getTexture(Tex_OpaqueDepth, frameId)); + mCanvases[frameId]->setTextureDistortion(getTexture(Tex_Distortion, frameId)); - if (mTransparentDepthPostPass) - { - mTransparentDepthPostPass->mFbo[frameId] = mFbos[frameId][FBO_Primary]; - mTransparentDepthPostPass->mMsaaFbo[frameId] = mFbos[frameId][FBO_Multisample]; - mTransparentDepthPostPass->mOpaqueFbo[frameId] = mFbos[frameId][FBO_OpaqueDepth]; - } + mTransparentDepthPostPass->mFbo[frameId] = mFbos[frameId][FBO_Primary]; + mTransparentDepthPostPass->mMsaaFbo[frameId] = mFbos[frameId][FBO_Multisample]; + mTransparentDepthPostPass->mOpaqueFbo[frameId] = mFbos[frameId][FBO_OpaqueDepth]; + + mDistortionCallback->setFBO(mFbos[frameId][FBO_Distortion], frameId); + mDistortionCallback->setOriginalFBO(mFbos[frameId][FBO_Primary], frameId); size_t frame = cv->getTraversalNumber(); @@ -441,6 +458,13 @@ namespace MWRender textures[Tex_Normal]->setSourceFormat(GL_RGB); textures[Tex_Normal]->setInternalFormat(GL_RGB); + textures[Tex_Distortion]->setSourceFormat(GL_RGB); + textures[Tex_Distortion]->setInternalFormat(GL_RGB); + + Stereo::setMultiviewCompatibleTextureSize( + textures[Tex_Distortion], width * DistortionRatio, height * DistortionRatio); + textures[Tex_Distortion]->dirtyTextureObject(); + auto setupDepth = [](osg::Texture* tex) { tex->setSourceFormat(GL_DEPTH_STENCIL_EXT); tex->setSourceType(SceneUtil::AutoDepth::depthSourceType()); @@ -448,16 +472,8 @@ namespace MWRender }; setupDepth(textures[Tex_Depth]); - - if (!mTransparentDepthPostPass) - { - textures[Tex_OpaqueDepth] = nullptr; - } - else - { - setupDepth(textures[Tex_OpaqueDepth]); - textures[Tex_OpaqueDepth]->setName("opaqueTexMap"); - } + setupDepth(textures[Tex_OpaqueDepth]); + textures[Tex_OpaqueDepth]->setName("opaqueTexMap"); auto& fbos = mFbos[frameId]; @@ -487,6 +503,7 @@ namespace MWRender auto normalRB = createFrameBufferAttachmentFromTemplate( Usage::RENDER_BUFFER, width, height, textures[Tex_Normal], mSamples); fbos[FBO_Multisample]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER1, normalRB); + fbos[FBO_FirstPerson]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER1, normalRB); } auto depthRB = createFrameBufferAttachmentFromTemplate( Usage::RENDER_BUFFER, width, height, textures[Tex_Depth], mSamples); @@ -510,12 +527,13 @@ namespace MWRender Stereo::createMultiviewCompatibleAttachment(textures[Tex_Normal])); } - if (textures[Tex_OpaqueDepth]) - { - fbos[FBO_OpaqueDepth] = new osg::FrameBufferObject; - fbos[FBO_OpaqueDepth]->setAttachment(osg::FrameBufferObject::BufferComponent::PACKED_DEPTH_STENCIL_BUFFER, - Stereo::createMultiviewCompatibleAttachment(textures[Tex_OpaqueDepth])); - } + fbos[FBO_OpaqueDepth] = new osg::FrameBufferObject; + fbos[FBO_OpaqueDepth]->setAttachment(osg::FrameBufferObject::BufferComponent::PACKED_DEPTH_STENCIL_BUFFER, + Stereo::createMultiviewCompatibleAttachment(textures[Tex_OpaqueDepth])); + + fbos[FBO_Distortion] = new osg::FrameBufferObject; + fbos[FBO_Distortion]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + Stereo::createMultiviewCompatibleAttachment(textures[Tex_Distortion])); #ifdef __APPLE__ if (textures[Tex_OpaqueDepth]) @@ -575,6 +593,7 @@ namespace MWRender node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerLastShader", Unit_LastShader)); node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerLastPass", Unit_LastPass)); node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerDepth", Unit_Depth)); + node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerDistortion", Unit_Distortion)); if (mNormals) node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerNormals", Unit_Normals)); @@ -582,6 +601,8 @@ namespace MWRender if (technique->getHDR()) node.mRootStateSet->addUniform(new osg::Uniform("omw_EyeAdaptation", Unit_EyeAdaptation)); + node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerDistortion", Unit_Distortion)); + int texUnit = Unit_NextFree; // user-defined samplers @@ -681,7 +702,7 @@ namespace MWRender disableTechnique(technique, false); - int pos = std::min(location.value_or(mTechniques.size()), mTechniques.size()); + int pos = std::min(location.value_or(mTechniques.size()) + mInternalTechniques.size(), mTechniques.size()); mTechniques.insert(mTechniques.begin() + pos, technique); dirtyTechniques(Settings::ShaderManager::get().getMode() == Settings::ShaderManager::Mode::Debug); @@ -747,6 +768,11 @@ namespace MWRender { mTechniques.clear(); + for (const auto& technique : mInternalTechniques) + { + mTechniques.push_back(technique); + } + for (const std::string& techniqueName : Settings::postProcessing().mChain.get()) { if (techniqueName.empty()) @@ -764,7 +790,7 @@ namespace MWRender for (const auto& technique : mTechniques) { - if (!technique || technique->getDynamic()) + if (!technique || technique->getDynamic() || technique->getInternal()) continue; chain.push_back(technique->getName()); } diff --git a/apps/openmw/mwrender/postprocessor.hpp b/apps/openmw/mwrender/postprocessor.hpp index e9f19bf6b5..2630467f95 100644 --- a/apps/openmw/mwrender/postprocessor.hpp +++ b/apps/openmw/mwrender/postprocessor.hpp @@ -50,12 +50,13 @@ namespace MWRender class PingPongCull; class PingPongCanvas; class TransparentDepthBinCallback; + class DistortionCallback; class PostProcessor : public osg::Group { public: - using FBOArray = std::array, 5>; - using TextureArray = std::array, 5>; + using FBOArray = std::array, 6>; + using TextureArray = std::array, 6>; using TechniqueList = std::vector>; enum TextureIndex @@ -64,7 +65,8 @@ namespace MWRender Tex_Scene_LDR, Tex_Depth, Tex_OpaqueDepth, - Tex_Normal + Tex_Normal, + Tex_Distortion, }; enum FBOIndex @@ -73,7 +75,8 @@ namespace MWRender FBO_Multisample, FBO_FirstPerson, FBO_OpaqueDepth, - FBO_Intercept + FBO_Intercept, + FBO_Distortion, }; enum TextureUnits @@ -83,6 +86,7 @@ namespace MWRender Unit_Depth, Unit_EyeAdaptation, Unit_Normals, + Unit_Distortion, Unit_NextFree }; @@ -223,6 +227,7 @@ namespace MWRender TechniqueList mTechniques; TechniqueList mTemplates; TechniqueList mQueuedTemplates; + TechniqueList mInternalTechniques; std::unordered_map mTechniqueFileMap; @@ -258,6 +263,7 @@ namespace MWRender osg::ref_ptr mPingPongCull; std::array, 2> mCanvases; osg::ref_ptr mTransparentDepthPostPass; + osg::ref_ptr mDistortionCallback; fx::DispatchArray mTemplateData; }; diff --git a/apps/openmw/mwrender/renderbin.hpp b/apps/openmw/mwrender/renderbin.hpp index c14f611426..6f4ae0819b 100644 --- a/apps/openmw/mwrender/renderbin.hpp +++ b/apps/openmw/mwrender/renderbin.hpp @@ -13,7 +13,8 @@ namespace MWRender RenderBin_DepthSorted = 10, // osg::StateSet::TRANSPARENT_BIN RenderBin_OcclusionQuery = 11, RenderBin_FirstPerson = 12, - RenderBin_SunGlare = 13 + RenderBin_SunGlare = 13, + RenderBin_Distortion = 14, }; } diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index c175817fa8..fa92fa1420 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -502,6 +502,7 @@ namespace MWRender sceneRoot->getOrCreateStateSet()->setAttribute(defaultMat); sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("emissiveMult", 1.f)); sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("specStrength", 1.f)); + sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("distortionStrength", 0.f)); mFog = std::make_unique(); @@ -692,8 +693,9 @@ namespace MWRender osg::Vec4f diffuse = SceneUtil::colourFromRGB(cell.getMood().mDirectionalColor); setSunColour(diffuse, diffuse, 1.f); - - const osg::Vec4f interiorSunPos = osg::Vec4f(-0.15f, 0.15f, 1.f, 0.f); + // This is total nonsense but it's what Morrowind uses + static const osg::Vec4f interiorSunPos + = osg::Vec4f(-1.f, osg::DegreesToRadians(45.f), osg::DegreesToRadians(45.f), 0.f); mPostProcessor->getStateUpdater()->setSunPos(interiorSunPos, false); mSunLight->setPosition(interiorSunPos); } @@ -847,6 +849,7 @@ namespace MWRender float rainIntensity = mSky->getPrecipitationAlpha(); mWater->setRainIntensity(rainIntensity); + mWater->setRainRipplesEnabled(mSky->getRainRipplesEnabled()); mWater->update(dt, paused); if (!paused) diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index a38030738a..6df3734252 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -257,6 +257,8 @@ namespace MWRender , mRainMaxHeight(0.f) , mRainEntranceSpeed(1.f) , mRainMaxRaindrops(0) + , mRainRipplesEnabled(Fallback::Map::getBool("Weather_Rain_Ripples")) + , mSnowRipplesEnabled(Fallback::Map::getBool("Weather_Snow_Ripples")) , mWindSpeed(0.f) , mBaseWindSpeed(0.f) , mEnabled(true) @@ -516,6 +518,20 @@ namespace MWRender return mRainNode != nullptr; } + bool SkyManager::getRainRipplesEnabled() const + { + if (!mEnabled || mIsStorm) + return false; + + if (hasRain()) + return mRainRipplesEnabled; + + if (mParticleNode && mCurrentParticleEffect == "meshes\\snow.nif") + return mSnowRipplesEnabled; + + return false; + } + float SkyManager::getPrecipitationAlpha() const { if (mEnabled && !mIsStorm && (hasRain() || mParticleNode)) diff --git a/apps/openmw/mwrender/sky.hpp b/apps/openmw/mwrender/sky.hpp index 75c6a10a50..96af2e6ec9 100644 --- a/apps/openmw/mwrender/sky.hpp +++ b/apps/openmw/mwrender/sky.hpp @@ -79,6 +79,8 @@ namespace MWRender bool hasRain() const; + bool getRainRipplesEnabled() const; + float getPrecipitationAlpha() const; void setRainSpeed(float speed); @@ -194,6 +196,8 @@ namespace MWRender float mRainMaxHeight; float mRainEntranceSpeed; int mRainMaxRaindrops; + bool mRainRipplesEnabled; + bool mSnowRipplesEnabled; float mWindSpeed; float mBaseWindSpeed; diff --git a/apps/openmw/mwrender/skyutil.hpp b/apps/openmw/mwrender/skyutil.hpp index 1018724595..da038e6c58 100644 --- a/apps/openmw/mwrender/skyutil.hpp +++ b/apps/openmw/mwrender/skyutil.hpp @@ -65,6 +65,7 @@ namespace MWRender bool mIsStorm; ESM::RefId mAmbientLoopSoundID; + ESM::RefId mRainLoopSoundID; float mAmbientSoundVolume; std::string mParticleEffect; diff --git a/apps/openmw/mwrender/util.cpp b/apps/openmw/mwrender/util.cpp index cd03784e8c..f1a1177f85 100644 --- a/apps/openmw/mwrender/util.cpp +++ b/apps/openmw/mwrender/util.cpp @@ -11,41 +11,40 @@ namespace MWRender { - - class TextureOverrideVisitor : public osg::NodeVisitor + namespace { - public: - TextureOverrideVisitor(std::string_view texture, Resource::ResourceSystem* resourcesystem) - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mTexture(texture) - , mResourcesystem(resourcesystem) + class TextureOverrideVisitor : public osg::NodeVisitor { - } - - void apply(osg::Node& node) override - { - int index = 0; - osg::ref_ptr nodePtr(&node); - if (node.getUserValue("overrideFx", index)) + public: + TextureOverrideVisitor(std::string_view texture, Resource::ResourceSystem* resourcesystem) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mTexture(texture) + , mResourcesystem(resourcesystem) { - if (index == 1) - overrideTexture(mTexture, mResourcesystem, std::move(nodePtr)); } - traverse(node); - } - std::string_view mTexture; - Resource::ResourceSystem* mResourcesystem; - }; - void overrideFirstRootTexture( - std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::ref_ptr node) - { - TextureOverrideVisitor overrideVisitor(texture, resourceSystem); - node->accept(overrideVisitor); + void apply(osg::Node& node) override + { + int index = 0; + if (node.getUserValue("overrideFx", index)) + { + if (index == 1) + overrideTexture(mTexture, mResourcesystem, node); + } + traverse(node); + } + std::string_view mTexture; + Resource::ResourceSystem* mResourcesystem; + }; } - void overrideTexture( - std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::ref_ptr node) + void overrideFirstRootTexture(std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::Node& node) + { + TextureOverrideVisitor overrideVisitor(texture, resourceSystem); + node.accept(overrideVisitor); + } + + void overrideTexture(std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::Node& node) { if (texture.empty()) return; @@ -58,14 +57,14 @@ namespace MWRender tex->setName("diffuseMap"); osg::ref_ptr stateset; - if (node->getStateSet()) - stateset = new osg::StateSet(*node->getStateSet(), osg::CopyOp::SHALLOW_COPY); + if (const osg::StateSet* const src = node.getStateSet()) + stateset = new osg::StateSet(*src, osg::CopyOp::SHALLOW_COPY); else stateset = new osg::StateSet; stateset->setTextureAttribute(0, tex, osg::StateAttribute::OVERRIDE); - node->setStateSet(stateset); + node.setStateSet(stateset); } bool shouldAddMSAAIntermediateTarget() diff --git a/apps/openmw/mwrender/util.hpp b/apps/openmw/mwrender/util.hpp index 64edaf8e18..fc43680d67 100644 --- a/apps/openmw/mwrender/util.hpp +++ b/apps/openmw/mwrender/util.hpp @@ -2,9 +2,8 @@ #define OPENMW_MWRENDER_UTIL_H #include -#include -#include +#include namespace osg { @@ -21,11 +20,9 @@ namespace MWRender // Overrides the texture of nodes in the mesh that had the same NiTexturingProperty as the first NiTexturingProperty // of the .NIF file's root node, if it had a NiTexturingProperty. Used for applying "particle textures" to magic // effects. - void overrideFirstRootTexture( - std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::ref_ptr node); + void overrideFirstRootTexture(std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::Node& node); - void overrideTexture( - std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::ref_ptr node); + void overrideTexture(std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::Node& node); // Node callback to entirely skip the traversal. class NoTraverseCallback : public osg::NodeCallback diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 85df70adfc..553bdeeaaa 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -205,21 +205,25 @@ namespace MWRender } }; - class RainIntensityUpdater : public SceneUtil::StateSetUpdater + class RainSettingsUpdater : public SceneUtil::StateSetUpdater { public: - RainIntensityUpdater() + RainSettingsUpdater() : mRainIntensity(0.f) + , mEnableRipples(false) { } void setRainIntensity(float rainIntensity) { mRainIntensity = rainIntensity; } + void setRipplesEnabled(bool enableRipples) { mEnableRipples = enableRipples; } protected: void setDefaults(osg::StateSet* stateset) override { osg::ref_ptr rainIntensityUniform = new osg::Uniform("rainIntensity", 0.0f); stateset->addUniform(rainIntensityUniform.get()); + osg::ref_ptr enableRainRipplesUniform = new osg::Uniform("enableRainRipples", false); + stateset->addUniform(enableRainRipplesUniform.get()); } void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override @@ -227,10 +231,14 @@ namespace MWRender osg::ref_ptr rainIntensityUniform = stateset->getUniform("rainIntensity"); if (rainIntensityUniform != nullptr) rainIntensityUniform->set(mRainIntensity); + osg::ref_ptr enableRainRipplesUniform = stateset->getUniform("enableRainRipples"); + if (enableRainRipplesUniform != nullptr) + enableRainRipplesUniform->set(mEnableRipples); } private: float mRainIntensity; + bool mEnableRipples; }; class Refraction : public SceneUtil::RTTNode @@ -430,7 +438,7 @@ namespace MWRender Water::Water(osg::Group* parent, osg::Group* sceneRoot, Resource::ResourceSystem* resourceSystem, osgUtil::IncrementalCompileOperation* ico) - : mRainIntensityUpdater(nullptr) + : mRainSettingsUpdater(nullptr) , mParent(parent) , mSceneRoot(sceneRoot) , mResourceSystem(resourceSystem) @@ -579,7 +587,7 @@ namespace MWRender node->setStateSet(stateset); node->setUpdateCallback(nullptr); - mRainIntensityUpdater = nullptr; + mRainSettingsUpdater = nullptr; // Add animated textures std::vector> textures; @@ -711,8 +719,8 @@ namespace MWRender normalMap->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR); normalMap->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - mRainIntensityUpdater = new RainIntensityUpdater(); - node->setUpdateCallback(mRainIntensityUpdater); + mRainSettingsUpdater = new RainSettingsUpdater(); + node->setUpdateCallback(mRainSettingsUpdater); mShaderWaterStateSetUpdater = new ShaderWaterStateSetUpdater(this, mReflection, mRefraction, mRipples, std::move(program), normalMap); @@ -801,8 +809,14 @@ namespace MWRender void Water::setRainIntensity(float rainIntensity) { - if (mRainIntensityUpdater) - mRainIntensityUpdater->setRainIntensity(rainIntensity); + if (mRainSettingsUpdater) + mRainSettingsUpdater->setRainIntensity(rainIntensity); + } + + void Water::setRainRipplesEnabled(bool enableRipples) + { + if (mRainSettingsUpdater) + mRainSettingsUpdater->setRipplesEnabled(enableRipples); } void Water::update(float dt, bool paused) diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index 0204cb4303..d3241bf3a7 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -46,13 +46,13 @@ namespace MWRender class Refraction; class Reflection; class RippleSimulation; - class RainIntensityUpdater; + class RainSettingsUpdater; class Ripples; /// Water rendering class Water { - osg::ref_ptr mRainIntensityUpdater; + osg::ref_ptr mRainSettingsUpdater; osg::ref_ptr mParent; osg::ref_ptr mSceneRoot; @@ -113,6 +113,7 @@ namespace MWRender void changeCell(const MWWorld::CellStore* store); void setHeight(const float height); void setRainIntensity(const float rainIntensity); + void setRainRipplesEnabled(bool enableRipples); void update(float dt, bool paused); diff --git a/apps/openmw/mwscript/animationextensions.cpp b/apps/openmw/mwscript/animationextensions.cpp index 32d7e46527..8d439ec82b 100644 --- a/apps/openmw/mwscript/animationextensions.cpp +++ b/apps/openmw/mwscript/animationextensions.cpp @@ -91,7 +91,7 @@ namespace MWScript throw std::runtime_error("animation mode out of range"); } - MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(ptr, group, mode, loops + 1, true); + MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(ptr, group, mode, loops, true); } }; diff --git a/apps/openmw/mwscript/containerextensions.cpp b/apps/openmw/mwscript/containerextensions.cpp index 4a0d302cd8..3822a247bd 100644 --- a/apps/openmw/mwscript/containerextensions.cpp +++ b/apps/openmw/mwscript/containerextensions.cpp @@ -109,7 +109,7 @@ namespace MWScript return; if (item == "gold_005" || item == "gold_010" || item == "gold_025" || item == "gold_100") - item = ESM::RefId::stringRefId("gold_001"); + item = MWWorld::ContainerStore::sGoldId; // Check if "item" can be placed in a container MWWorld::ManualRef manualRef(*MWBase::Environment::get().getESMStore(), item, 1); @@ -195,7 +195,7 @@ namespace MWScript runtime.pop(); if (item == "gold_005" || item == "gold_010" || item == "gold_025" || item == "gold_100") - item = ESM::RefId::stringRefId("gold_001"); + item = MWWorld::ContainerStore::sGoldId; MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); @@ -231,7 +231,7 @@ namespace MWScript return; if (item == "gold_005" || item == "gold_010" || item == "gold_025" || item == "gold_100") - item = ESM::RefId::stringRefId("gold_001"); + item = MWWorld::ContainerStore::sGoldId; // Explicit calls to non-unique actors affect the base record if (!R::implicit && ptr.getClass().isActor() @@ -460,7 +460,7 @@ namespace MWScript it != invStore.cend(); ++it) { if (it->getCellRef().getSoul() == name) - count += it->getRefData().getCount(); + count += it->getCellRef().getCount(); } runtime.push(count); } diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 687b512106..9d75334f00 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -715,7 +715,7 @@ namespace MWScript MWWorld::ConstContainerStoreIterator it = store.getSlot(slot); if (it != store.end() && it->getCellRef().getRefId() == item) { - numNotEquipped -= it->getRefData().getCount(); + numNotEquipped -= it->getCellRef().getCount(); } } @@ -724,7 +724,7 @@ namespace MWScript MWWorld::ContainerStoreIterator it = store.getSlot(slot); if (it != store.end() && it->getCellRef().getRefId() == item) { - int numToRemove = std::min(amount - numNotEquipped, it->getRefData().getCount()); + int numToRemove = std::min(amount - numNotEquipped, it->getCellRef().getCount()); store.unequipItemQuantity(*it, numToRemove); numNotEquipped += numToRemove; } @@ -1346,8 +1346,10 @@ namespace MWScript { MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats(player).setBounty(0); - MWBase::Environment::get().getWorld()->confiscateStolenItems(player); - MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + world->confiscateStolenItems(player); + world->getPlayer().recordCrimeId(); + world->getPlayer().setDrawState(MWMechanics::DrawState::Nothing); } }; @@ -1416,7 +1418,7 @@ namespace MWScript if (ptr.getRefData().isDeletedByContentFile()) msg << "[Deleted by content file]" << std::endl; - if (!ptr.getRefData().getCount()) + if (!ptr.getCellRef().getCount()) msg << "[Deleted]" << std::endl; msg << "RefID: " << ptr.getCellRef().getRefId() << std::endl; diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index 745286e109..4bc59e1524 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -85,7 +85,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats(ptr).getLevel(); + Interpreter::Type_Integer value = -1; + if (ptr.getClass().isActor()) + value = ptr.getClass().getCreatureStats(ptr).getLevel(); runtime.push(value); } @@ -102,7 +104,8 @@ namespace MWScript Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); - ptr.getClass().getCreatureStats(ptr).setLevel(value); + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).setLevel(value); } }; @@ -121,7 +124,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Float value = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex).getModified(); + Interpreter::Type_Float value = 0.f; + if (ptr.getClass().isActor()) + value = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex).getModified(); runtime.push(value); } @@ -145,6 +150,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex); attribute.setBase(value, true); ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); @@ -169,6 +177,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex); modStat(attribute, value); ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); @@ -189,14 +200,14 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Float value; + Interpreter::Type_Float value = 0.f; if (mIndex == 0 && ptr.getClass().hasItemHealth(ptr)) { // health is a special case value = static_cast(ptr.getClass().getItemMaxHealth(ptr)); } - else + else if (ptr.getClass().isActor()) { value = ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex).getCurrent(); // GetMagicka shouldn't return negative values @@ -225,6 +236,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::DynamicStat stat(ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex)); stat.setBase(value); @@ -254,6 +268,9 @@ namespace MWScript Interpreter::Type_Float diff = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + // workaround broken endgame scripts that kill dagoth ur if (!R::implicit && ptr.getCellRef().getRefId() == "dagoth_ur_1") { @@ -301,6 +318,9 @@ namespace MWScript Interpreter::Type_Float diff = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); Interpreter::Type_Float current = stats.getDynamic(mIndex).getCurrent(); @@ -336,6 +356,13 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); + + if (!ptr.getClass().isActor()) + { + runtime.push(0.f); + return; + } + const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); runtime.push(stats.getDynamic(mIndex).getRatio()); @@ -357,6 +384,12 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); + if (!ptr.getClass().isActor()) + { + runtime.push(0.f); + return; + } + Interpreter::Type_Float value = ptr.getClass().getSkill(ptr, mId); runtime.push(value); @@ -381,6 +414,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isNpc()) + return; + MWMechanics::NpcStats& stats = ptr.getClass().getNpcStats(ptr); stats.getSkill(mId).setBase(value, true); @@ -405,6 +441,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isNpc()) + return; + MWMechanics::SkillValue& skill = ptr.getClass().getNpcStats(ptr).getSkill(mId); modStat(skill, value); } @@ -445,10 +484,12 @@ namespace MWScript { MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - - player.getClass().getNpcStats(player).setBounty( - static_cast(runtime[0].mFloat) + player.getClass().getNpcStats(player).getBounty()); + int bounty = std::max( + 0, static_cast(runtime[0].mFloat) + player.getClass().getNpcStats(player).getBounty()); + player.getClass().getNpcStats(player).setBounty(bounty); runtime.pop(); + if (bounty == 0) + MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); } }; @@ -463,6 +504,9 @@ namespace MWScript ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); + if (!ptr.getClass().isActor()) + return; + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(id); MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); @@ -489,6 +533,9 @@ namespace MWScript ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); creatureStats.getSpells().remove(id); @@ -512,7 +559,8 @@ namespace MWScript ESM::RefId spellid = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffects(ptr, spellid); + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffects(ptr, spellid); } }; @@ -527,7 +575,8 @@ namespace MWScript Interpreter::Type_Integer effectId = runtime[0].mInteger; runtime.pop(); - ptr.getClass().getCreatureStats(ptr).getActiveSpells().purgeEffect(ptr, effectId); + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).getActiveSpells().purgeEffect(ptr, effectId); } }; @@ -843,7 +892,10 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - runtime.push(ptr.getClass().getCreatureStats(ptr).hasCommonDisease()); + if (ptr.getClass().isActor()) + runtime.push(ptr.getClass().getCreatureStats(ptr).hasCommonDisease()); + else + runtime.push(0); } }; @@ -855,7 +907,10 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - runtime.push(ptr.getClass().getCreatureStats(ptr).hasBlightDisease()); + if (ptr.getClass().isActor()) + runtime.push(ptr.getClass().getCreatureStats(ptr).hasBlightDisease()); + else + runtime.push(0); } }; @@ -870,9 +925,16 @@ namespace MWScript ESM::RefId race = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - const ESM::RefId& npcRace = ptr.get()->mBase->mRace; + if (ptr.getClass().isNpc()) + { + const ESM::RefId& npcRace = ptr.get()->mBase->mRace; - runtime.push(race == npcRace); + runtime.push(race == npcRace); + } + else + { + runtime.push(0); + } } }; @@ -1041,10 +1103,15 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats(ptr).hasDied(); + Interpreter::Type_Integer value = 0; + if (ptr.getClass().isActor()) + { + auto& stats = ptr.getClass().getCreatureStats(ptr); + value = stats.hasDied(); - if (value) - ptr.getClass().getCreatureStats(ptr).clearHasDied(); + if (value) + stats.clearHasDied(); + } runtime.push(value); } @@ -1058,10 +1125,15 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats(ptr).hasBeenMurdered(); + Interpreter::Type_Integer value = 0; + if (ptr.getClass().isActor()) + { + auto& stats = ptr.getClass().getCreatureStats(ptr); + value = stats.hasBeenMurdered(); - if (value) - ptr.getClass().getCreatureStats(ptr).clearHasBeenMurdered(); + if (value) + stats.clearHasBeenMurdered(); + } runtime.push(value); } @@ -1075,7 +1147,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats(ptr).getKnockedDownOneFrame(); + Interpreter::Type_Integer value = 0; + if (ptr.getClass().isActor()) + value = ptr.getClass().getCreatureStats(ptr).getKnockedDownOneFrame(); runtime.push(value); } @@ -1088,7 +1162,10 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - runtime.push(ptr.getClass().getNpcStats(ptr).isWerewolf()); + if (ptr.getClass().isNpc()) + runtime.push(ptr.getClass().getNpcStats(ptr).isWerewolf()); + else + runtime.push(0); } }; @@ -1099,7 +1176,8 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - MWBase::Environment::get().getMechanicsManager()->setWerewolf(ptr, set); + if (ptr.getClass().isNpc()) + MWBase::Environment::get().getMechanicsManager()->setWerewolf(ptr, set); } }; @@ -1110,7 +1188,8 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - MWBase::Environment::get().getMechanicsManager()->applyWerewolfAcrobatics(ptr); + if (ptr.getClass().isNpc()) + MWBase::Environment::get().getMechanicsManager()->applyWerewolfAcrobatics(ptr); } }; @@ -1122,6 +1201,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); + if (!ptr.getClass().isActor()) + return; + if (ptr == MWMechanics::getPlayer()) { MWBase::Environment::get().getMechanicsManager()->resurrect(ptr); @@ -1190,6 +1272,12 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); + if (!ptr.getClass().isActor()) + { + runtime.push(0); + return; + } + const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); float currentValue = effects.getOrDefault(mPositiveEffect).getMagnitude(); if (mNegativeEffect != -1) @@ -1224,6 +1312,13 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); + + int arg = runtime[0].mInteger; + runtime.pop(); + + if (!ptr.getClass().isActor()) + return; + MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); float currentValue = effects.getOrDefault(mPositiveEffect).getMagnitude(); if (mNegativeEffect != -1) @@ -1237,8 +1332,6 @@ namespace MWScript if (mPositiveEffect == ESM::MagicEffect::ResistFrost) currentValue += effects.getOrDefault(ESM::MagicEffect::FrostShield).getMagnitude(); - int arg = runtime[0].mInteger; - runtime.pop(); effects.modifyBase(mPositiveEffect, (arg - static_cast(currentValue))); } }; @@ -1259,10 +1352,14 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); int arg = runtime[0].mInteger; runtime.pop(); + + if (!ptr.getClass().isActor()) + return; + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); stats.getMagicEffects().modifyBase(mPositiveEffect, arg); } }; diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openal_output.cpp index 363a0d06b5..99003d5ce3 100644 --- a/apps/openmw/mwsound/openal_output.cpp +++ b/apps/openmw/mwsound/openal_output.cpp @@ -1034,65 +1034,6 @@ namespace MWSound return ret; } - void OpenAL_Output::setHrtf(const std::string& hrtfname, HrtfMode hrtfmode) - { - if (!mDevice || !ALC.SOFT_HRTF) - { - Log(Debug::Info) << "HRTF extension not present"; - return; - } - - LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; - getALCFunc(alcGetStringiSOFT, mDevice, "alcGetStringiSOFT"); - - LPALCRESETDEVICESOFT alcResetDeviceSOFT = nullptr; - getALCFunc(alcResetDeviceSOFT, mDevice, "alcResetDeviceSOFT"); - - std::vector attrs; - attrs.reserve(15); - - attrs.push_back(ALC_HRTF_SOFT); - attrs.push_back(hrtfmode == HrtfMode::Disable ? ALC_FALSE - : hrtfmode == HrtfMode::Enable ? ALC_TRUE - : - /*hrtfmode == HrtfMode::Auto ?*/ ALC_DONT_CARE_SOFT); - if (!hrtfname.empty()) - { - ALCint index = -1; - ALCint num_hrtf; - alcGetIntegerv(mDevice, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); - for (ALCint i = 0; i < num_hrtf; ++i) - { - const ALCchar* entry = alcGetStringiSOFT(mDevice, ALC_HRTF_SPECIFIER_SOFT, i); - if (hrtfname == entry) - { - index = i; - break; - } - } - - if (index < 0) - Log(Debug::Warning) << "Failed to find HRTF name \"" << hrtfname << "\", using default"; - else - { - attrs.push_back(ALC_HRTF_ID_SOFT); - attrs.push_back(index); - } - } - attrs.push_back(0); - alcResetDeviceSOFT(mDevice, attrs.data()); - - ALCint hrtf_state; - alcGetIntegerv(mDevice, ALC_HRTF_SOFT, 1, &hrtf_state); - if (!hrtf_state) - Log(Debug::Info) << "HRTF disabled"; - else - { - const ALCchar* hrtf = alcGetString(mDevice, ALC_HRTF_SPECIFIER_SOFT); - Log(Debug::Info) << "Enabled HRTF " << hrtf; - } - } - std::pair OpenAL_Output::loadSound(const std::string& fname) { getALError(); diff --git a/apps/openmw/mwsound/openal_output.hpp b/apps/openmw/mwsound/openal_output.hpp index eed23ac659..7636f7bda9 100644 --- a/apps/openmw/mwsound/openal_output.hpp +++ b/apps/openmw/mwsound/openal_output.hpp @@ -84,7 +84,6 @@ namespace MWSound void deinit() override; std::vector enumerateHrtf() override; - void setHrtf(const std::string& hrtfname, HrtfMode hrtfmode) override; std::pair loadSound(const std::string& fname) override; size_t unloadSound(Sound_Handle data) override; diff --git a/apps/openmw/mwsound/sound_output.hpp b/apps/openmw/mwsound/sound_output.hpp index 8eec20bcba..df95f0909e 100644 --- a/apps/openmw/mwsound/sound_output.hpp +++ b/apps/openmw/mwsound/sound_output.hpp @@ -38,7 +38,6 @@ namespace MWSound virtual void deinit() = 0; virtual std::vector enumerateHrtf() = 0; - virtual void setHrtf(const std::string& hrtfname, HrtfMode hrtfmode) = 0; virtual std::pair loadSound(const std::string& fname) = 0; virtual size_t unloadSound(Sound_Handle data) = 0; diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 826c0dbba6..4358c4094e 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -64,6 +64,7 @@ void MWState::StateManager::cleanup(bool force) mState = State_NoGame; mCharacterManager.setCurrentCharacter(nullptr); mTimePlayed = 0; + mLastSavegame.clear(); MWMechanics::CreatureStats::cleanup(); } @@ -119,14 +120,27 @@ void MWState::StateManager::askLoadRecent() if (!mAskLoadRecent) { - const MWState::Character* character = getCurrentCharacter(); - if (!character || character->begin() == character->end()) // no saves + if (mLastSavegame.empty()) // no saves { MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); } else { - MWState::Slot lastSave = *character->begin(); + std::string saveName = Files::pathToUnicodeString(mLastSavegame.filename()); + // Assume the last saved game belongs to the current character's slot list. + const Character* character = getCurrentCharacter(); + if (character) + { + for (const auto& slot : *character) + { + if (slot.mPath == mLastSavegame) + { + saveName = slot.mProfile.mDescription; + break; + } + } + } + std::vector buttons; buttons.emplace_back("#{Interface:Yes}"); buttons.emplace_back("#{Interface:No}"); @@ -134,7 +148,7 @@ void MWState::StateManager::askLoadRecent() = MWBase::Environment::get().getL10nManager()->getMessage("OMWEngine", "AskLoadLastSave"); std::string_view tag = "%s"; size_t pos = message.find(tag); - message.replace(pos, tag.length(), lastSave.mProfile.mDescription); + message.replace(pos, tag.length(), saveName); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); mAskLoadRecent = true; } @@ -322,6 +336,7 @@ void MWState::StateManager::saveGame(std::string_view description, const Slot* s throw std::runtime_error("Write operation failed (file stream)"); Settings::saves().mCharacter.set(Files::pathToUnicodeString(slot->mPath.parent_path().filename())); + mLastSavegame = slot->mPath; const auto finish = std::chrono::steady_clock::now(); @@ -411,10 +426,25 @@ void MWState::StateManager::loadGame(const Character* character, const std::file ESM::ESMReader reader; reader.open(filepath); - if (reader.getFormatVersion() > ESM::CurrentSaveGameFormatVersion) - throw VersionMismatchError( - "This save file was created using a newer version of OpenMW and is thus not supported. Please upgrade " - "to the newest OpenMW version to load this file."); + ESM::FormatVersion version = reader.getFormatVersion(); + if (version > ESM::CurrentSaveGameFormatVersion) + throw VersionMismatchError("#{OMWEngine:LoadingRequiresNewVersionError}"); + else if (version < ESM::MinSupportedSaveGameFormatVersion) + { + const char* release; + // Report the last version still capable of reading this save + if (version <= ESM::OpenMW0_48SaveGameFormatVersion) + release = "OpenMW 0.48.0"; + else + { + // Insert additional else if statements above to cover future releases + static_assert(ESM::MinSupportedSaveGameFormatVersion <= ESM::OpenMW0_49SaveGameFormatVersion); + release = "OpenMW 0.49.0"; + } + auto l10n = MWBase::Environment::get().getL10nManager()->getContext("OMWEngine"); + std::string message = l10n->formatMessage("LoadingRequiresOldVersionError", { "version" }, { release }); + throw VersionMismatchError(message); + } std::map contentFileMap = buildContentFileIndexMap(reader); reader.setContentFileMapping(&contentFileMap); @@ -457,7 +487,6 @@ void MWState::StateManager::loadGame(const Character* character, const std::file break; case ESM::REC_JOUR: - case ESM::REC_JOUR_LEGACY: case ESM::REC_QUES: MWBase::Environment::get().getJournal()->readRecord(reader, n.toInt()); @@ -547,6 +576,7 @@ void MWState::StateManager::loadGame(const Character* character, const std::file if (character) Settings::saves().mCharacter.set(Files::pathToUnicodeString(character->getPath().filename())); + mLastSavegame = filepath; MWBase::Environment::get().getWindowManager()->setNewGame(false); MWBase::Environment::get().getWorld()->saveLoaded(); @@ -607,11 +637,7 @@ void MWState::StateManager::loadGame(const Character* character, const std::file std::vector buttons; buttons.emplace_back("#{Interface:OK}"); - std::string error; - if (typeid(e) == typeid(VersionMismatchError)) - error = "#{OMWEngine:LoadingFailed}: #{OMWEngine:LoadingRequiresNewVersionError}"; - else - error = "#{OMWEngine:LoadingFailed}: " + std::string(e.what()); + std::string error = "#{OMWEngine:LoadingFailed}: " + std::string(e.what()); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(error, buttons); } @@ -629,7 +655,15 @@ 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); + if (mLastSavegame == savePath) + { + if (character->begin() != character->end()) + mLastSavegame = character->begin()->mPath; + else + mLastSavegame.clear(); + } } MWState::Character* MWState::StateManager::getCurrentCharacter() @@ -660,9 +694,9 @@ void MWState::StateManager::update(float duration) { mAskLoadRecent = false; // Load last saved game for current character - - MWState::Slot lastSave = *curCharacter->begin(); - loadGame(curCharacter, lastSave.mPath); + // loadGame resets the game state along with mLastSavegame so we want to preserve it + const std::filesystem::path filePath = std::move(mLastSavegame); + loadGame(curCharacter, filePath); } else if (iButton == 1) { diff --git a/apps/openmw/mwstate/statemanagerimp.hpp b/apps/openmw/mwstate/statemanagerimp.hpp index c293209f34..a76b829e38 100644 --- a/apps/openmw/mwstate/statemanagerimp.hpp +++ b/apps/openmw/mwstate/statemanagerimp.hpp @@ -17,6 +17,7 @@ namespace MWState State mState; CharacterManager mCharacterManager; double mTimePlayed; + std::filesystem::path mLastSavegame; private: void cleanup(bool force = false); diff --git a/apps/openmw/mwworld/actionharvest.cpp b/apps/openmw/mwworld/actionharvest.cpp index 708260d395..30f316c2db 100644 --- a/apps/openmw/mwworld/actionharvest.cpp +++ b/apps/openmw/mwworld/actionharvest.cpp @@ -37,7 +37,7 @@ namespace MWWorld if (!it->getClass().showsInInventory(*it)) continue; - int itemCount = it->getRefData().getCount(); + int itemCount = it->getCellRef().getCount(); // Note: it is important to check for crime before move an item from container. Otherwise owner check will // not work for a last item in the container - empty harvested containers are considered as "allowed to // use". diff --git a/apps/openmw/mwworld/actiontake.cpp b/apps/openmw/mwworld/actiontake.cpp index 83d6e729c9..1f37499e8f 100644 --- a/apps/openmw/mwworld/actiontake.cpp +++ b/apps/openmw/mwworld/actiontake.cpp @@ -30,10 +30,12 @@ namespace MWWorld } } - MWBase::Environment::get().getMechanicsManager()->itemTaken( - actor, getTarget(), MWWorld::Ptr(), getTarget().getRefData().getCount()); - MWWorld::Ptr newitem - = *actor.getClass().getContainerStore(actor).add(getTarget(), getTarget().getRefData().getCount()); + int count = getTarget().getCellRef().getCount(); + if (getTarget().getClass().isGold(getTarget())) + count *= getTarget().getClass().getValue(getTarget()); + + MWBase::Environment::get().getMechanicsManager()->itemTaken(actor, getTarget(), MWWorld::Ptr(), count); + MWWorld::Ptr newitem = *actor.getClass().getContainerStore(actor).add(getTarget(), count); MWBase::Environment::get().getWorld()->deleteObject(getTarget()); setTarget(newitem); } diff --git a/apps/openmw/mwworld/cellref.cpp b/apps/openmw/mwworld/cellref.cpp index b7a3713eba..854348c2a8 100644 --- a/apps/openmw/mwworld/cellref.cpp +++ b/apps/openmw/mwworld/cellref.cpp @@ -8,10 +8,10 @@ #include #include -#include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwmechanics/spellutil.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" namespace MWWorld { @@ -377,17 +377,19 @@ namespace MWWorld } } - void CellRef::setGoldValue(int value) + void CellRef::setCount(int value) { - if (value != getGoldValue()) + if (value != getCount(false)) { mChanged = true; std::visit(ESM::VisitOverload{ - [&](ESM4::Reference& /*ref*/) {}, - [&](ESM4::ActorCharacter&) {}, - [&](ESM::CellRef& ref) { ref.mGoldValue = value; }, + [&](ESM4::Reference& ref) { ref.mCount = value; }, + [&](ESM4::ActorCharacter& ref) { ref.mCount = value; }, + [&](ESM::CellRef& ref) { ref.mCount = value; }, }, mCellRef.mVariant); + if (value == 0) + MWBase::Environment::get().getWorld()->removeRefScript(this); } } diff --git a/apps/openmw/mwworld/cellref.hpp b/apps/openmw/mwworld/cellref.hpp index 92bc355db4..4dcac4def5 100644 --- a/apps/openmw/mwworld/cellref.hpp +++ b/apps/openmw/mwworld/cellref.hpp @@ -231,18 +231,20 @@ namespace MWWorld } void setTrap(const ESM::RefId& trap); - // This is 5 for Gold_005 references, 100 for Gold_100 and so on. - int getGoldValue() const + int getCount(bool absolute = true) const { struct Visitor { - int operator()(const ESM::CellRef& ref) { return ref.mGoldValue; } - int operator()(const ESM4::Reference& /*ref*/) { return 0; } - int operator()(const ESM4::ActorCharacter&) { throw std::logic_error("Not applicable"); } + int operator()(const ESM::CellRef& ref) { return ref.mCount; } + int operator()(const ESM4::Reference& ref) { return ref.mCount; } + int operator()(const ESM4::ActorCharacter& ref) { return ref.mCount; } }; - return std::visit(Visitor(), mCellRef.mVariant); + int count = std::visit(Visitor(), mCellRef.mVariant); + if (absolute) + return std::abs(count); + return count; } - void setGoldValue(int value); + void setCount(int value); // Write the content of this CellRef into the given ObjectState void writeState(ESM::ObjectState& state) const; diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 340def5859..fd24bb7271 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -152,7 +152,7 @@ namespace if (toIgnore.find(&*iter) != toIgnore.end()) continue; - if (actor.getClass().getCreatureStats(actor).matchesActorId(actorId) && actor.getRefData().getCount() > 0) + if (actor.getClass().getCreatureStats(actor).matchesActorId(actorId) && actor.getCellRef().getCount() > 0) return actor; } @@ -175,7 +175,7 @@ namespace // Reference that came from a content file and has not been changed -> ignore continue; } - if (liveCellRef.mData.getCount() == 0 && !liveCellRef.mRef.hasContentFile()) + if (liveCellRef.mRef.getCount() == 0 && !liveCellRef.mRef.hasContentFile()) { // Deleted reference that did not come from a content file -> ignore continue; @@ -201,8 +201,8 @@ namespace { for (auto& item : state.mInventory.mItems) { - if (item.mCount > 0 && baseItem.mItem == item.mRef.mRefID) - item.mCount = -item.mCount; + if (item.mRef.mCount > 0 && baseItem.mItem == item.mRef.mRefID) + item.mRef.mCount = -item.mRef.mCount; } } } @@ -252,16 +252,16 @@ namespace if (!record) return; - if (state.mVersion < 15) + if (state.mVersion <= ESM::MaxOldRestockingFormatVersion) fixRestocking(record, state); - if (state.mVersion < 17) + if (state.mVersion <= ESM::MaxClearModifiersFormatVersion) { if constexpr (std::is_same_v) MWWorld::convertMagicEffects(state.mCreatureStats, state.mInventory); else if constexpr (std::is_same_v) MWWorld::convertMagicEffects(state.mCreatureStats, state.mInventory, &state.mNpcStats); } - else if (state.mVersion < 20) + else if (state.mVersion <= ESM::MaxOldCreatureStatsFormatVersion) { if constexpr (std::is_same_v || std::is_same_v) MWWorld::convertStats(state.mCreatureStats); @@ -298,9 +298,9 @@ namespace // instance is invalid. But non-storable item are always stored in saves together with their original cell. // If a non-storable item references a content file, but is not found in this content file, // we should drop it. Likewise if this stack is empty. - if (!MWWorld::ContainerStore::isStorableType() || !state.mCount) + if (!MWWorld::ContainerStore::isStorableType() || !state.mRef.mCount) { - if (state.mCount) + if (state.mRef.mCount) Log(Debug::Warning) << "Warning: Dropping reference to " << state.mRef.mRefID << " (invalid content file link)"; return; @@ -676,7 +676,7 @@ namespace MWWorld MWWorld::Ptr actor(base, this); if (!actor.getClass().isActor()) continue; - if (actor.getClass().getCreatureStats(actor).matchesActorId(id) && actor.getRefData().getCount() > 0) + if (actor.getClass().getCreatureStats(actor).matchesActorId(id) && actor.getCellRef().getCount() > 0) return actor; } @@ -1042,7 +1042,7 @@ namespace MWWorld for (const auto& [base, store] : mMovedToAnotherCell) { ESM::RefNum refNum = base->mRef.getRefNum(); - if (base->mData.isDeleted() && !refNum.hasContentFile()) + if (base->isDeleted() && !refNum.hasContentFile()) continue; // filtered out in writeReferenceCollection ESM::RefId movedTo = store->getCell()->getId(); @@ -1168,20 +1168,18 @@ namespace MWWorld { if (mState == State_Loaded) { - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) + for (MWWorld::LiveCellRef& creature : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + Ptr ptr = getCurrentPtr(&creature); + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { MWBase::Environment::get().getMechanicsManager()->restoreDynamicStats(ptr, hours, true); } } - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) + for (MWWorld::LiveCellRef& npc : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + Ptr ptr = getCurrentPtr(&npc); + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { MWBase::Environment::get().getMechanicsManager()->restoreDynamicStats(ptr, hours, true); } @@ -1196,29 +1194,26 @@ namespace MWWorld if (mState == State_Loaded) { - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) + for (MWWorld::LiveCellRef& creature : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + Ptr ptr = getCurrentPtr(&creature); + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { ptr.getClass().getContainerStore(ptr).rechargeItems(duration); } } - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) + for (MWWorld::LiveCellRef& npc : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + Ptr ptr = getCurrentPtr(&npc); + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { ptr.getClass().getContainerStore(ptr).rechargeItems(duration); } } - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) + for (MWWorld::LiveCellRef& container : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCustomData() != nullptr && ptr.getRefData().getCount() > 0 + Ptr ptr = getCurrentPtr(&container); + if (!ptr.isEmpty() && ptr.getRefData().getCustomData() != nullptr && ptr.getCellRef().getCount() > 0 && ptr.getClass().getContainerStore(ptr).isResolved()) { ptr.getClass().getContainerStore(ptr).rechargeItems(duration); @@ -1262,7 +1257,7 @@ namespace MWWorld } forEachType([](Ptr ptr) { // no need to clearCorpse, handled as part of get() - if (!ptr.getRefData().isDeleted()) + if (!ptr.mRef->isDeleted()) ptr.getClass().respawn(ptr); return true; }); @@ -1290,7 +1285,7 @@ namespace MWWorld for (auto& item : list) { Ptr ptr = getCurrentPtr(&item); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { checkItem(ptr); } diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index c80cf56d6a..0c6527ce22 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -118,7 +118,7 @@ namespace MWWorld /// scripting compatibility, and the fact that objects may be "un-deleted" in the original game). static bool isAccessible(const MWWorld::RefData& refdata, const MWWorld::CellRef& cref) { - return !refdata.isDeletedByContentFile() && (cref.hasContentFile() || refdata.getCount() > 0); + return !refdata.isDeletedByContentFile() && (cref.hasContentFile() || cref.getCount() > 0); } /// Moves object from this cell to the given cell. diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index de3c2b011d..d5062d6add 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -124,7 +124,7 @@ namespace MWWorld } void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, - const osg::Vec3f& hitPosition, bool successful) const + const osg::Vec3f& hitPosition, bool successful, const MWMechanics::DamageSourceType sourceType) const { throw std::runtime_error("class cannot be hit"); } @@ -373,7 +373,7 @@ namespace MWWorld { Ptr newPtr = copyToCellImpl(ptr, cell); newPtr.getCellRef().unsetRefNum(); // This RefNum is only valid within the original cell of the reference - newPtr.getRefData().setCount(count); + newPtr.getCellRef().setCount(count); newPtr.getRefData().setLuaScripts(nullptr); MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); return newPtr; @@ -524,11 +524,9 @@ namespace MWWorld const ESM::Enchantment* enchantment = MWBase::Environment::get().getESMStore()->get().search(enchantmentName); - if (!enchantment) + if (!enchantment || enchantment->mEffects.mList.empty()) return result; - assert(enchantment->mEffects.mList.size()); - const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get().search( enchantment->mEffects.mList.front().mEffectID); if (!magicEffect) diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index 7b7e9135ba..87e70b3198 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -13,6 +13,8 @@ #include "ptr.hpp" #include "../mwmechanics/aisetting.hpp" +#include "../mwmechanics/damagesourcetype.hpp" + #include #include @@ -142,11 +144,12 @@ namespace MWWorld /// (default implementation: throw an exception) virtual void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const; + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const; ///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is /// true (else fatigue) by \a object (sword, arrow, etc). \a attacker specifies the - /// actor responsible for the attack, and \a successful specifies if the hit is - /// successful or not. + /// actor responsible for the attack. \a successful specifies if the hit is + /// successful or not. \a sourceType classifies the damage source. virtual void block(const Ptr& ptr) const; ///< Play the appropriate sound for a blocked attack, depending on the currently equipped shield diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index b55a524a48..d30ea21494 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -51,7 +51,7 @@ namespace for (const MWWorld::LiveCellRef& liveCellRef : cellRefList.mList) { - if (const int count = liveCellRef.mData.getCount(); count > 0) + if (const int count = liveCellRef.mRef.getCount(); count > 0) sum += count * liveCellRef.mBase->mData.mWeight; } @@ -65,7 +65,7 @@ namespace for (MWWorld::LiveCellRef& liveCellRef : list.mList) { - if ((liveCellRef.mBase->mId == id) && liveCellRef.mData.getCount()) + if ((liveCellRef.mBase->mId == id) && liveCellRef.mRef.getCount()) { MWWorld::Ptr ptr(&liveCellRef, nullptr); ptr.setContainerStore(store); @@ -132,7 +132,7 @@ void MWWorld::ContainerStore::storeStates( { for (const LiveCellRef& liveCellRef : collection.mList) { - if (liveCellRef.mData.getCount() == 0) + if (liveCellRef.mRef.getCount() == 0) continue; ESM::ObjectState state; storeState(liveCellRef, state); @@ -192,7 +192,7 @@ int MWWorld::ContainerStore::count(const ESM::RefId& id) const int total = 0; for (const auto&& iter : *this) if (iter.getCellRef().getRefId() == id) - total += iter.getRefData().getCount(); + total += iter.getCellRef().getCount(); return total; } @@ -219,9 +219,9 @@ void MWWorld::ContainerStore::setContListener(MWWorld::ContainerStoreListener* l MWWorld::ContainerStoreIterator MWWorld::ContainerStore::unstack(const Ptr& ptr, int count) { resolve(); - if (ptr.getRefData().getCount() <= count) + if (ptr.getCellRef().getCount() <= count) return end(); - MWWorld::ContainerStoreIterator it = addNewStack(ptr, subtractItems(ptr.getRefData().getCount(false), count)); + MWWorld::ContainerStoreIterator it = addNewStack(ptr, subtractItems(ptr.getCellRef().getCount(false), count)); MWWorld::Ptr newPtr = *it; newPtr.getCellRef().unsetRefNum(); @@ -232,7 +232,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::unstack(const Ptr& ptr, if (!script.empty()) MWBase::Environment::get().getWorld()->getLocalScripts().add(script, *it); - remove(ptr, ptr.getRefData().getCount() - count); + remove(ptr, ptr.getCellRef().getCount() - count); return it; } @@ -257,9 +257,9 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::restack(const MWWorld:: { if (stacks(*iter, item)) { - iter->getRefData().setCount( - addItems(iter->getRefData().getCount(false), item.getRefData().getCount(false))); - item.getRefData().setCount(0); + iter->getCellRef().setCount( + addItems(iter->getCellRef().getCount(false), item.getCellRef().getCount(false))); + item.getCellRef().setCount(0); retval = iter; break; } @@ -385,22 +385,24 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addImp(const Ptr& ptr, // gold needs special handling: when it is inserted into a container, the base object automatically becomes Gold_001 // this ensures that gold piles of different sizes stack with each other (also, several scripts rely on Gold_001 for // detecting player gold) + // Note that adding 1 gold_100 is equivalent to adding 1 gold_001. Morrowind.exe resolves gold in leveled lists to + // gold_001 and TESCS disallows adding gold other than gold_001 to inventories. If a content file defines a + // container containing gold_100 anyway, the item is not turned to gold_001 until the player puts it down in the + // world and picks it up again. We just turn it into gold_001 here and ignore that oddity. if (ptr.getClass().isGold(ptr)) { - int realCount = count * ptr.getClass().getValue(ptr); - for (MWWorld::ContainerStoreIterator iter(begin(type)); iter != end(); ++iter) { if (iter->getCellRef().getRefId() == MWWorld::ContainerStore::sGoldId) { - iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), realCount)); + iter->getCellRef().setCount(addItems(iter->getCellRef().getCount(false), count)); flagAsModified(); return iter; } } - MWWorld::ManualRef ref(esmStore, MWWorld::ContainerStore::sGoldId, realCount); - return addNewStack(ref.getPtr(), realCount); + MWWorld::ManualRef ref(esmStore, MWWorld::ContainerStore::sGoldId, count); + return addNewStack(ref.getPtr(), count); } // determine whether to stack or not @@ -414,7 +416,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addImp(const Ptr& ptr, if (stacks(*iter, ptr)) { // stack - iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), count)); + iter->getCellRef().setCount(addItems(iter->getCellRef().getCount(false), count)); flagAsModified(); return iter; @@ -480,7 +482,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addNewStack(const Const break; } - it->getRefData().setCount(count); + it->getCellRef().setCount(count); flagAsModified(); return it; @@ -562,7 +564,7 @@ int MWWorld::ContainerStore::remove(const Ptr& item, int count, bool equipReplac resolve(); int toRemove = count; - RefData& itemRef = item.getRefData(); + CellRef& itemRef = item.getCellRef(); if (itemRef.getCount() <= toRemove) { @@ -667,7 +669,7 @@ void MWWorld::ContainerStore::addInitialItemImp( void MWWorld::ContainerStore::clear() { for (auto&& iter : *this) - iter.getRefData().setCount(0); + iter.getCellRef().setCount(0); flagAsModified(); mModified = true; @@ -690,7 +692,7 @@ void MWWorld::ContainerStore::resolve() if (!mResolved && !container.isEmpty() && container.getType() == ESM::REC_CONT) { for (const auto&& ptr : *this) - ptr.getRefData().setCount(0); + ptr.getCellRef().setCount(0); Misc::Rng::Generator prng{ mSeed }; fill(container.get()->mBase->mInventory, ESM::RefId(), prng); addScripts(*this, container.mCell); @@ -712,7 +714,7 @@ MWWorld::ResolutionHandle MWWorld::ContainerStore::resolveTemporarily() if (!mResolved && !container.isEmpty() && container.getType() == ESM::REC_CONT) { for (const auto&& ptr : *this) - ptr.getRefData().setCount(0); + ptr.getCellRef().setCount(0); Misc::Rng::Generator prng{ mSeed }; fill(container.get()->mBase->mInventory, ESM::RefId(), prng); addScripts(*this, container.mCell); @@ -729,7 +731,7 @@ void MWWorld::ContainerStore::unresolve() if (mResolved && !container.isEmpty() && container.getType() == ESM::REC_CONT) { for (const auto&& ptr : *this) - ptr.getRefData().setCount(0); + ptr.getCellRef().setCount(0); fillNonRandom(container.get()->mBase->mInventory, ESM::RefId(), mSeed); addScripts(*this, container.mCell); mResolved = false; @@ -1332,7 +1334,7 @@ MWWorld::ContainerStoreIteratorBase& MWWorld::ContainerStoreIteratorBas { if (incIterator()) nextType(); - } while (mType != -1 && !(**this).getRefData().getCount()); + } while (mType != -1 && !(**this).getCellRef().getCount()); return *this; } @@ -1384,7 +1386,7 @@ MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase(int mas { nextType(); - if (mType == -1 || (**this).getRefData().getCount()) + if (mType == -1 || (**this).getCellRef().getCount()) return; ++*this; diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 0b661b4442..7ecaaa217d 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -83,12 +83,22 @@ namespace throw std::runtime_error("List of NPC classes is empty!"); } + const ESM::RefId& getDefaultRace(const MWWorld::Store& races) + { + auto it = races.begin(); + if (it != races.end()) + return it->mId; + throw std::runtime_error("List of NPC races is empty!"); + } + std::vector getNPCsToReplace(const MWWorld::Store& factions, - const MWWorld::Store& classes, const MWWorld::Store& scripts, - const std::unordered_map& npcs) + const MWWorld::Store& classes, const MWWorld::Store& races, + const MWWorld::Store& scripts, const std::unordered_map& npcs) { // Cache first class from store - we will use it if current class is not found const ESM::RefId& defaultCls = getDefaultClass(classes); + // Same for races + const ESM::RefId& defaultRace = getDefaultRace(races); // Validate NPCs for non-existing class and faction. // We will replace invalid entries by fixed ones @@ -113,8 +123,7 @@ namespace } } - const ESM::RefId& npcClass = npc.mClass; - const ESM::Class* cls = classes.search(npcClass); + const ESM::Class* cls = classes.search(npc.mClass); if (!cls) { Log(Debug::Verbose) << "NPC " << npc.mId << " (" << npc.mName << ") has nonexistent class " @@ -123,6 +132,15 @@ namespace changed = true; } + const ESM::Race* race = races.search(npc.mRace); + if (!race) + { + Log(Debug::Verbose) << "NPC " << npc.mId << " (" << npc.mName << ") has nonexistent race " << npc.mRace + << ", using " << defaultRace << " race as replacement."; + npc.mRace = defaultRace; + changed = true; + } + if (!npc.mScript.empty() && !scripts.search(npc.mScript)) { Log(Debug::Verbose) << "NPC " << npc.mId << " (" << npc.mName << ") has nonexistent script " @@ -580,8 +598,8 @@ namespace MWWorld void ESMStore::validate() { auto& npcs = getWritable(); - std::vector npcsToReplace = getNPCsToReplace( - getWritable(), getWritable(), getWritable(), npcs.mStatic); + std::vector npcsToReplace = getNPCsToReplace(getWritable(), getWritable(), + getWritable(), getWritable(), npcs.mStatic); for (const ESM::NPC& npc : npcsToReplace) { @@ -623,8 +641,8 @@ namespace MWWorld auto& npcs = getWritable(); auto& scripts = getWritable(); - std::vector npcsToReplace = getNPCsToReplace( - getWritable(), getWritable(), getWritable(), npcs.mDynamic); + std::vector npcsToReplace = getNPCsToReplace(getWritable(), getWritable(), + getWritable(), getWritable(), npcs.mDynamic); for (const ESM::NPC& npc : npcsToReplace) npcs.insert(npc); diff --git a/apps/openmw/mwworld/groundcoverstore.cpp b/apps/openmw/mwworld/groundcoverstore.cpp index 85b9376f0d..f45b49babe 100644 --- a/apps/openmw/mwworld/groundcoverstore.cpp +++ b/apps/openmw/mwworld/groundcoverstore.cpp @@ -9,8 +9,6 @@ #include #include -#include - #include "store.hpp" namespace MWWorld @@ -26,8 +24,6 @@ namespace MWWorld const ::EsmLoader::EsmData content = ::EsmLoader::loadEsmData(query, groundcoverFiles, fileCollections, readers, encoder, listener); - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - static constexpr std::string_view prefix = "grass\\"; for (const ESM::Static& stat : statics) { @@ -35,7 +31,7 @@ namespace MWWorld std::replace(model.begin(), model.end(), '/', '\\'); if (!model.starts_with(prefix)) continue; - mMeshCache[stat.mId] = Misc::ResourceHelpers::correctMeshPath(model, vfs); + mMeshCache[stat.mId] = Misc::ResourceHelpers::correctMeshPath(model); } for (const ESM::Static& stat : content.mStatics) @@ -44,7 +40,7 @@ namespace MWWorld std::replace(model.begin(), model.end(), '/', '\\'); if (!model.starts_with(prefix)) continue; - mMeshCache[stat.mId] = Misc::ResourceHelpers::correctMeshPath(model, vfs); + mMeshCache[stat.mId] = Misc::ResourceHelpers::correctMeshPath(model); } for (const ESM::Cell& cell : content.mCells) diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index 095f5d3cc1..f48f4e6e31 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -79,11 +79,11 @@ void MWWorld::InventoryStore::readEquipmentState( slot = allowedSlots.first.front(); // unstack if required - if (!allowedSlots.second && iter->getRefData().getCount() > 1) + if (!allowedSlots.second && iter->getCellRef().getCount() > 1) { - int count = iter->getRefData().getCount(false); + int count = iter->getCellRef().getCount(false); MWWorld::ContainerStoreIterator newIter = addNewStack(*iter, count > 0 ? 1 : -1); - iter->getRefData().setCount(subtractItems(count, 1)); + iter->getCellRef().setCount(subtractItems(count, 1)); mSlots[slot] = newIter; } else @@ -171,7 +171,7 @@ void MWWorld::InventoryStore::equip(int slot, const ContainerStoreIterator& iter // unstack item pointed to by iterator if required if (iterator != end() && !slots_.second - && iterator->getRefData().getCount() > 1) // if slots.second is true, item can stay stacked when equipped + && iterator->getCellRef().getCount() > 1) // if slots.second is true, item can stay stacked when equipped { unstack(*iterator); } @@ -355,7 +355,7 @@ void MWWorld::InventoryStore::autoEquipWeapon(TSlots& slots_) { if (!itemsSlots.second) { - if (weapon->getRefData().getCount() > 1) + if (weapon->getCellRef().getCount() > 1) { unstack(*weapon); } @@ -478,7 +478,7 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_) if (!itemsSlots.second) // if itemsSlots.second is true, item can stay stacked when equipped { // unstack item pointed to by iterator if required - if (iter->getRefData().getCount() > 1) + if (iter->getCellRef().getCount() > 1) { unstack(*iter); } @@ -590,7 +590,7 @@ int MWWorld::InventoryStore::remove(const Ptr& item, int count, bool equipReplac int retCount = ContainerStore::remove(item, count, equipReplacement, resolve); bool wasEquipped = false; - if (!item.getRefData().getCount()) + if (!item.getCellRef().getCount()) { for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { @@ -618,7 +618,7 @@ int MWWorld::InventoryStore::remove(const Ptr& item, int count, bool equipReplac autoEquip(); } - if (item.getRefData().getCount() == 0 && mSelectedEnchantItem != end() && *mSelectedEnchantItem == item) + if (item.getCellRef().getCount() == 0 && mSelectedEnchantItem != end() && *mSelectedEnchantItem == item) { mSelectedEnchantItem = end(); } @@ -643,7 +643,7 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, b // empty this slot mSlots[slot] = end(); - if (it->getRefData().getCount()) + if (it->getCellRef().getCount()) { retval = restack(*it); @@ -690,10 +690,10 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItemQuantity(con throw std::runtime_error("attempt to unequip an item that is not currently equipped"); if (count <= 0) throw std::runtime_error("attempt to unequip nothing (count <= 0)"); - if (count > item.getRefData().getCount()) + if (count > item.getCellRef().getCount()) throw std::runtime_error("attempt to unequip more items than equipped"); - if (count == item.getRefData().getCount()) + if (count == item.getCellRef().getCount()) return unequipItem(item); // Move items to an existing stack if possible, otherwise split count items out into a new stack. @@ -702,13 +702,13 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItemQuantity(con { if (stacks(*iter, item) && !isEquipped(*iter)) { - iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), count)); - item.getRefData().setCount(subtractItems(item.getRefData().getCount(false), count)); + iter->getCellRef().setCount(addItems(iter->getCellRef().getCount(false), count)); + item.getCellRef().setCount(subtractItems(item.getCellRef().getCount(false), count)); return iter; } } - return unstack(item, item.getRefData().getCount() - count); + return unstack(item, item.getCellRef().getCount() - count); } MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() const diff --git a/apps/openmw/mwworld/livecellref.cpp b/apps/openmw/mwworld/livecellref.cpp index d4e2ac40c0..61b838bbf0 100644 --- a/apps/openmw/mwworld/livecellref.cpp +++ b/apps/openmw/mwworld/livecellref.cpp @@ -106,6 +106,11 @@ unsigned int MWWorld::LiveCellRefBase::getType() const return mClass->getType(); } +bool MWWorld::LiveCellRefBase::isDeleted() const +{ + return mData.isDeletedByContentFile() || mRef.getCount(false) == 0; +} + namespace MWWorld { std::string makeDynamicCastErrorMessage(const LiveCellRefBase* value, std::string_view recordType) diff --git a/apps/openmw/mwworld/livecellref.hpp b/apps/openmw/mwworld/livecellref.hpp index 1e4d1441f5..c95dd589b2 100644 --- a/apps/openmw/mwworld/livecellref.hpp +++ b/apps/openmw/mwworld/livecellref.hpp @@ -59,6 +59,9 @@ namespace MWWorld template static LiveCellRef* dynamicCast(LiveCellRefBase* value); + /// Returns true if the object was either deleted by the content file or by gameplay. + bool isDeleted() const; + protected: void loadImp(const ESM::ObjectState& state); ///< Load state into a LiveCellRef, that has already been initialised with base and diff --git a/apps/openmw/mwworld/localscripts.cpp b/apps/openmw/mwworld/localscripts.cpp index 8d9a282791..955e1a91f8 100644 --- a/apps/openmw/mwworld/localscripts.cpp +++ b/apps/openmw/mwworld/localscripts.cpp @@ -24,7 +24,7 @@ namespace bool operator()(const MWWorld::Ptr& ptr) { - if (ptr.getRefData().isDeleted()) + if (ptr.mRef->isDeleted()) return true; const ESM::RefId& script = ptr.getClass().getScript(ptr); @@ -152,10 +152,10 @@ void MWWorld::LocalScripts::clearCell(CellStore* cell) } } -void MWWorld::LocalScripts::remove(RefData* ref) +void MWWorld::LocalScripts::remove(const MWWorld::CellRef* ref) { for (auto iter = mScripts.begin(); iter != mScripts.end(); ++iter) - if (&(iter->second.getRefData()) == ref) + if (&(iter->second.getCellRef()) == ref) { if (iter == mIter) ++mIter; diff --git a/apps/openmw/mwworld/localscripts.hpp b/apps/openmw/mwworld/localscripts.hpp index 4bd2abeb84..09a913e655 100644 --- a/apps/openmw/mwworld/localscripts.hpp +++ b/apps/openmw/mwworld/localscripts.hpp @@ -41,7 +41,7 @@ namespace MWWorld void clearCell(CellStore* cell); ///< Remove all scripts belonging to \a cell. - void remove(RefData* ref); + void remove(const MWWorld::CellRef* ref); void remove(const Ptr& ptr); ///< Remove script for given reference (ignored if reference does not have a script listed). diff --git a/apps/openmw/mwworld/manualref.cpp b/apps/openmw/mwworld/manualref.cpp index e9fd02e6f5..c6c0444754 100644 --- a/apps/openmw/mwworld/manualref.cpp +++ b/apps/openmw/mwworld/manualref.cpp @@ -101,5 +101,5 @@ MWWorld::ManualRef::ManualRef(const MWWorld::ESMStore& store, const ESM::RefId& throw std::logic_error("failed to create manual cell ref for " + name.toDebugString() + " (unknown type)"); } - mPtr.getRefData().setCount(count); + mPtr.getCellRef().setCount(count); } diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index 0d7afb559f..b498bb488b 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -334,12 +334,7 @@ namespace MWWorld if (player.mObject.mNpcStats.mIsWerewolf) { - if (player.mObject.mNpcStats.mWerewolfDeprecatedData) - { - saveStats(); - setWerewolfStats(); - } - else if (reader.getFormatVersion() <= ESM::MaxOldSkillsAndAttributesFormatVersion) + if (reader.getFormatVersion() <= ESM::MaxOldSkillsAndAttributesFormatVersion) { setWerewolfStats(); if (player.mSetWerewolfAcrobatics) diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index d873f16a59..0584a9fe94 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -209,8 +209,6 @@ namespace MWWorld if (state.mIdMagic.size() > 1) { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - for (size_t iter = 1; iter != state.mIdMagic.size(); ++iter) { std::ostringstream nodeName; @@ -222,7 +220,7 @@ namespace MWWorld attachTo->accept(findVisitor); if (findVisitor.mFoundNode) mResourceSystem->getSceneManager()->getInstance( - Misc::ResourceHelpers::correctMeshPath(weapon->mModel, vfs), findVisitor.mFoundNode); + Misc::ResourceHelpers::correctMeshPath(weapon->mModel), findVisitor.mFoundNode); } } @@ -254,7 +252,7 @@ namespace MWWorld SceneUtil::AssignControllerSourcesVisitor assignVisitor(state.mEffectAnimationTime); state.mNode->accept(assignVisitor); - MWRender::overrideFirstRootTexture(texture, mResourceSystem, std::move(projectile)); + MWRender::overrideFirstRootTexture(texture, mResourceSystem, *projectile); } void ProjectileManager::update(State& state, float duration) @@ -330,8 +328,7 @@ namespace MWWorld if (state.mIdMagic.size() > 1) { model = Misc::ResourceHelpers::correctMeshPath( - MWBase::Environment::get().getESMStore()->get().find(state.mIdMagic[1])->mModel, - MWBase::Environment::get().getResourceSystem()->getVFS()); + MWBase::Environment::get().getESMStore()->get().find(state.mIdMagic[1])->mModel); } state.mProjectileId = mPhysics->addProjectile(caster, pos, model, true); state.mToDelete = false; @@ -437,7 +434,7 @@ namespace MWWorld MWWorld::Ptr caster = magicBoltState.getCaster(); if (!caster.isEmpty() && caster.getClass().isActor()) { - if (caster.getRefData().getCount() <= 0 || caster.getClass().getCreatureStats(caster).isDead()) + if (caster.getCellRef().getCount() <= 0 || caster.getClass().getCreatureStats(caster).isDead()) { cleanupMagicBolt(magicBoltState); continue; @@ -453,7 +450,7 @@ namespace MWWorld { const auto npc = caster.get()->mBase; const auto race = store.get().find(npc->mRace); - speed *= npc->isMale() ? race->mData.mWeight.mMale : race->mData.mWeight.mFemale; + speed *= npc->isMale() ? race->mData.mMaleWeight : race->mData.mFemaleWeight; } osg::Vec3f direction = orient * osg::Vec3f(0, 1, 0); direction.normalize(); diff --git a/apps/openmw/mwworld/ptr.cpp b/apps/openmw/mwworld/ptr.cpp index 1421b51a2a..25715a26f1 100644 --- a/apps/openmw/mwworld/ptr.cpp +++ b/apps/openmw/mwworld/ptr.cpp @@ -10,7 +10,7 @@ namespace MWWorld std::string Ptr::toString() const { std::string res = "object"; - if (getRefData().isDeleted()) + if (mRef->isDeleted()) res = "deleted object"; res.append(getCellRef().getRefNum().toString()); res.append(" ("); diff --git a/apps/openmw/mwworld/refdata.cpp b/apps/openmw/mwworld/refdata.cpp index 87c085ebd4..f7ba76da21 100644 --- a/apps/openmw/mwworld/refdata.cpp +++ b/apps/openmw/mwworld/refdata.cpp @@ -37,7 +37,6 @@ namespace MWWorld mBaseNode = refData.mBaseNode; mLocals = refData.mLocals; mEnabled = refData.mEnabled; - mCount = refData.mCount; mPosition = refData.mPosition; mChanged = refData.mChanged; mDeletedByContentFile = refData.mDeletedByContentFile; @@ -62,7 +61,6 @@ namespace MWWorld , mDeletedByContentFile(false) , mEnabled(true) , mPhysicsPostponed(false) - , mCount(1) , mCustomData(nullptr) , mChanged(false) , mFlags(0) @@ -79,7 +77,6 @@ namespace MWWorld , mDeletedByContentFile(false) , mEnabled(true) , mPhysicsPostponed(false) - , mCount(1) , mPosition(cellRef.mPos) , mCustomData(nullptr) , mChanged(false) @@ -92,7 +89,6 @@ namespace MWWorld , mDeletedByContentFile(ref.mFlags & ESM4::Rec_Deleted) , mEnabled(!(ref.mFlags & ESM4::Rec_Disabled)) , mPhysicsPostponed(false) - , mCount(ref.mCount) , mPosition(ref.mPos) , mCustomData(nullptr) , mChanged(false) @@ -105,7 +101,6 @@ namespace MWWorld , mDeletedByContentFile(ref.mFlags & ESM4::Rec_Deleted) , mEnabled(!(ref.mFlags & ESM4::Rec_Disabled)) , mPhysicsPostponed(false) - , mCount(mDeletedByContentFile ? 0 : 1) , mPosition(ref.mPos) , mCustomData(nullptr) , mChanged(false) @@ -118,7 +113,6 @@ namespace MWWorld , mDeletedByContentFile(deletedByContentFile) , mEnabled(objectState.mEnabled != 0) , mPhysicsPostponed(false) - , mCount(objectState.mCount) , mPosition(objectState.mPosition) , mAnimationState(objectState.mAnimationState) , mCustomData(nullptr) @@ -153,7 +147,6 @@ namespace MWWorld objectState.mHasLocals = mLocals.write(objectState.mLocals, scriptId); objectState.mEnabled = mEnabled; - objectState.mCount = mCount; objectState.mPosition = mPosition; objectState.mFlags = mFlags; @@ -205,39 +198,17 @@ namespace MWWorld return mBaseNode; } - int RefData::getCount(bool absolute) const - { - if (absolute) - return std::abs(mCount); - return mCount; - } - void RefData::setLocals(const ESM::Script& script) { if (mLocals.configure(script) && !mLocals.isEmpty()) mChanged = true; } - void RefData::setCount(int count) - { - if (count == 0) - MWBase::Environment::get().getWorld()->removeRefScript(this); - - mChanged = true; - - mCount = count; - } - void RefData::setDeletedByContentFile(bool deleted) { mDeletedByContentFile = deleted; } - bool RefData::isDeleted() const - { - return mDeletedByContentFile || mCount == 0; - } - bool RefData::isDeletedByContentFile() const { return mDeletedByContentFile; diff --git a/apps/openmw/mwworld/refdata.hpp b/apps/openmw/mwworld/refdata.hpp index a6a45623f5..ae80a0d64e 100644 --- a/apps/openmw/mwworld/refdata.hpp +++ b/apps/openmw/mwworld/refdata.hpp @@ -58,9 +58,6 @@ namespace MWWorld bool mPhysicsPostponed : 1; private: - /// 0: deleted - int mCount; - ESM::Position mPosition; ESM::AnimationState mAnimationState; @@ -110,26 +107,15 @@ namespace MWWorld /// Set base node (can be a null pointer). void setBaseNode(osg::ref_ptr base); - int getCount(bool absolute = true) const; - void setLocals(const ESM::Script& script); MWLua::LocalScripts* getLuaScripts() { return mLuaScripts.get(); } void setLuaScripts(std::shared_ptr&&); - void setCount(int count); - ///< Set object count (an object pile is a simple object with a count >1). - /// - /// \warning Do not call setCount() to add or remove objects from a - /// container or an actor's inventory. Call ContainerStore::add() or - /// ContainerStore::remove() instead. - /// This flag is only used for content stack loading and will not be stored in the savegame. /// If the object was deleted by gameplay, then use setCount(0) instead. void setDeletedByContentFile(bool deleted); - /// Returns true if the object was either deleted by the content file or by gameplay. - bool isDeleted() const; /// Returns true if the object was deleted by a content file. bool isDeletedByContentFile() const; diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 3d96de6749..80f52f2375 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -69,13 +69,13 @@ namespace osg::Quat makeInverseNodeRotation(const MWWorld::Ptr& ptr) { - const auto pos = ptr.getRefData().getPosition(); + const auto& pos = ptr.getRefData().getPosition(); return ptr.getClass().isActor() ? makeActorOsgQuat(pos) : makeInversedOrderObjectOsgQuat(pos); } osg::Quat makeDirectNodeRotation(const MWWorld::Ptr& ptr) { - const auto pos = ptr.getRefData().getPosition(); + const auto& pos = ptr.getRefData().getPosition(); return ptr.getClass().isActor() ? makeActorOsgQuat(pos) : Misc::Convert::makeOsgQuat(pos); } @@ -111,13 +111,16 @@ namespace std::string model = getModel(ptr); const auto rotation = makeDirectNodeRotation(ptr); + // Null node meant to distinguish objects that aren't in the scene from paged objects + // TODO: find a more clever way to make paging exclusion more reliable? + static const osg::ref_ptr pagedNode( + new SceneUtil::PositionAttitudeTransform); + ESM::RefNum refnum = ptr.getCellRef().getRefNum(); if (!refnum.hasContentFile() || !std::binary_search(pagedRefs.begin(), pagedRefs.end(), refnum)) ptr.getClass().insertObjectRendering(ptr, model, rendering); else - ptr.getRefData().setBaseNode( - new SceneUtil::PositionAttitudeTransform); // FIXME remove this when physics code is fixed not to depend - // on basenode + ptr.getRefData().setBaseNode(pagedNode); setNodeRotation(ptr, rendering, rotation); if (ptr.getClass().useAnim()) @@ -226,7 +229,7 @@ namespace { for (MWWorld::Ptr& ptr : mToInsert) { - if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled()) + if (!ptr.mRef->isDeleted() && ptr.getRefData().isEnabled()) { try { @@ -648,7 +651,7 @@ namespace MWWorld if (ptr.mRef->mData.mPhysicsPostponed) { ptr.mRef->mData.mPhysicsPostponed = false; - if (ptr.mRef->mData.isEnabled() && ptr.mRef->mData.getCount() > 0) + if (ptr.mRef->mData.isEnabled() && ptr.mRef->mRef.getCount() > 0) { std::string model = getModel(ptr); if (!model.empty()) diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 5d739a9161..655cd5aa7a 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -180,7 +180,6 @@ namespace MWWorld , mTransitionDelta(Fallback::Map::getFloat("Weather_" + name + "_Transition_Delta")) , mThunderFrequency(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Frequency")) , mThunderThreshold(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Threshold")) - , mThunderSoundID() , mFlashDecrement(Fallback::Map::getFloat("Weather_" + name + "_Flash_Decrement")) , mFlashBrightness(0.0f) { @@ -195,21 +194,19 @@ namespace MWWorld mThunderSoundID[3] = ESM::RefId::stringRefId(Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_3")); - // TODO: support weathers that have both "Ambient Loop Sound ID" and "Rain Loop Sound ID", need to play both - // sounds at the same time. - if (!mRainEffect.empty()) // NOTE: in vanilla, the weathers with rain seem to be hardcoded; changing // Using_Precip has no effect { - mAmbientLoopSoundID + mRainLoopSoundID = ESM::RefId::stringRefId(Fallback::Map::getString("Weather_" + name + "_Rain_Loop_Sound_ID")); - if (mAmbientLoopSoundID.empty()) // default to "rain" if not set - mAmbientLoopSoundID = ESM::RefId::stringRefId("rain"); + if (mRainLoopSoundID.empty()) // default to "rain" if not set + mRainLoopSoundID = ESM::RefId::stringRefId("rain"); + else if (mRainLoopSoundID == "None") + mRainLoopSoundID = ESM::RefId(); } - else - mAmbientLoopSoundID - = ESM::RefId::stringRefId(Fallback::Map::getString("Weather_" + name + "_Ambient_Loop_Sound_ID")); + mAmbientLoopSoundID + = ESM::RefId::stringRefId(Fallback::Map::getString("Weather_" + name + "_Ambient_Loop_Sound_ID")); if (mAmbientLoopSoundID == "None") mAmbientLoopSoundID = ESM::RefId(); } @@ -552,8 +549,6 @@ namespace MWWorld , mQueuedWeather(0) , mRegions() , mResult() - , mAmbientSound(nullptr) - , mPlayingSoundID() { mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration; mTimeSettings.mNightEnd = mSunriseTime; @@ -794,24 +789,62 @@ namespace MWWorld mRendering.getSkyManager()->setWeather(mResult); // Play sounds - if (mPlayingSoundID != mResult.mAmbientLoopSoundID) + if (mPlayingAmbientSoundID != mResult.mAmbientLoopSoundID) { - stopSounds(); + if (mAmbientSound) + { + MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); + mAmbientSound = nullptr; + } if (!mResult.mAmbientLoopSoundID.empty()) mAmbientSound = MWBase::Environment::get().getSoundManager()->playSound(mResult.mAmbientLoopSoundID, mResult.mAmbientSoundVolume, 1.0, MWSound::Type::Sfx, MWSound::PlayMode::Loop); - mPlayingSoundID = mResult.mAmbientLoopSoundID; + mPlayingAmbientSoundID = mResult.mAmbientLoopSoundID; } else if (mAmbientSound) mAmbientSound->setVolume(mResult.mAmbientSoundVolume); + + if (mPlayingRainSoundID != mResult.mRainLoopSoundID) + { + if (mRainSound) + { + MWBase::Environment::get().getSoundManager()->stopSound(mRainSound); + mRainSound = nullptr; + } + if (!mResult.mRainLoopSoundID.empty()) + mRainSound = MWBase::Environment::get().getSoundManager()->playSound(mResult.mRainLoopSoundID, + mResult.mAmbientSoundVolume, 1.0, MWSound::Type::Sfx, MWSound::PlayMode::Loop); + mPlayingRainSoundID = mResult.mRainLoopSoundID; + } + else if (mRainSound) + mRainSound->setVolume(mResult.mAmbientSoundVolume); } void WeatherManager::stopSounds() { + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); if (mAmbientSound) - MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); - mAmbientSound = nullptr; - mPlayingSoundID = ESM::RefId(); + { + sndMgr->stopSound(mAmbientSound); + mAmbientSound = nullptr; + } + mPlayingAmbientSoundID = ESM::RefId(); + + if (mRainSound) + { + sndMgr->stopSound(mRainSound); + mRainSound = nullptr; + } + mPlayingRainSoundID = ESM::RefId(); + + for (ESM::RefId soundId : mWeatherSettings[mCurrentWeather].mThunderSoundID) + if (!soundId.empty() && sndMgr->getSoundPlaying(MWWorld::ConstPtr(), soundId)) + sndMgr->stopSound3D(MWWorld::ConstPtr(), soundId); + + if (inTransition()) + for (ESM::RefId soundId : mWeatherSettings[mNextWeather].mThunderSoundID) + if (!soundId.empty() && sndMgr->getSoundPlaying(MWWorld::ConstPtr(), soundId)) + sndMgr->stopSound3D(MWWorld::ConstPtr(), soundId); } float WeatherManager::getWindSpeed() const @@ -898,36 +931,27 @@ namespace MWWorld { if (ESM::REC_WTHR == type) { - if (reader.getFormatVersion() <= ESM::MaxOldWeatherFormatVersion) + ESM::WeatherState state; + state.load(reader); + + std::swap(mCurrentRegion, state.mCurrentRegion); + mTimePassed = state.mTimePassed; + mFastForward = state.mFastForward; + mWeatherUpdateTime = state.mWeatherUpdateTime; + mTransitionFactor = state.mTransitionFactor; + mCurrentWeather = state.mCurrentWeather; + mNextWeather = state.mNextWeather; + mQueuedWeather = state.mQueuedWeather; + + mRegions.clear(); + importRegions(); + + for (auto it = state.mRegions.begin(); it != state.mRegions.end(); ++it) { - // Weather state isn't really all that important, so to preserve older save games, we'll just discard - // the older weather records, rather than fail to handle the record. - reader.skipRecord(); - } - else - { - ESM::WeatherState state; - state.load(reader); - - std::swap(mCurrentRegion, state.mCurrentRegion); - mTimePassed = state.mTimePassed; - mFastForward = state.mFastForward; - mWeatherUpdateTime = state.mWeatherUpdateTime; - mTransitionFactor = state.mTransitionFactor; - mCurrentWeather = state.mCurrentWeather; - mNextWeather = state.mNextWeather; - mQueuedWeather = state.mQueuedWeather; - - mRegions.clear(); - importRegions(); - - for (auto it = state.mRegions.begin(); it != state.mRegions.end(); ++it) + auto found = mRegions.find(it->first); + if (found != mRegions.end()) { - auto found = mRegions.find(it->first); - if (found != mRegions.end()) - { - found->second = RegionWeather(it->second); - } + found->second = RegionWeather(it->second); } } @@ -1127,6 +1151,7 @@ namespace MWWorld mResult.mCloudSpeed = current.mCloudSpeed; mResult.mGlareView = current.mGlareView; mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; + mResult.mRainLoopSoundID = current.mRainLoopSoundID; mResult.mAmbientSoundVolume = 1.f; mResult.mPrecipitationAlpha = 1.f; @@ -1246,6 +1271,7 @@ namespace MWWorld mResult.mAmbientSoundVolume = 1.f - factor / threshold; mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; + mResult.mRainLoopSoundID = current.mRainLoopSoundID; mResult.mRainDiameter = current.mRainDiameter; mResult.mRainMinHeight = current.mRainMinHeight; mResult.mRainMaxHeight = current.mRainMaxHeight; @@ -1261,6 +1287,7 @@ namespace MWWorld mResult.mAmbientSoundVolume = (factor - threshold) / (1 - threshold); mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID; + mResult.mRainLoopSoundID = other.mRainLoopSoundID; mResult.mRainDiameter = other.mRainDiameter; mResult.mRainMinHeight = other.mRainMinHeight; diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index 65f926c096..0643240dcd 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -156,9 +156,13 @@ namespace MWWorld float FogOffset; } mDL; - // Sound effect + // Sound effects // This is used for Blight, Ashstorm and Blizzard (Bloodmoon) ESM::RefId mAmbientLoopSoundID; + // This is used for Rain and Thunderstorm + ESM::RefId mRainLoopSoundID; + + std::array mThunderSoundID; // Is this an ash storm / blight storm? If so, the following will happen: // - The particles and clouds will be oriented so they appear to come from the Red Mountain. @@ -211,7 +215,6 @@ namespace MWWorld // non-zero values. float mThunderFrequency; float mThunderThreshold; - ESM::RefId mThunderSoundID[4]; float mFlashDecrement; float mFlashBrightness; @@ -369,8 +372,10 @@ namespace MWWorld std::map mRegions; MWRender::WeatherResult mResult; - MWBase::Sound* mAmbientSound; - ESM::RefId mPlayingSoundID; + MWBase::Sound* mAmbientSound{ nullptr }; + ESM::RefId mPlayingAmbientSoundID; + MWBase::Sound* mRainSound{ nullptr }; + ESM::RefId mPlayingRainSoundID; void addWeather( const std::string& name, float dlFactor, float dlOffset, const std::string& particleEffect = ""); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index d945aa5848..1b6af6038e 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -685,7 +685,7 @@ namespace MWWorld return mStore.get().find("sDefaultCellname")->mValue.getString(); } - void World::removeRefScript(MWWorld::RefData* ref) + void World::removeRefScript(const MWWorld::CellRef* ref) { mLocalScripts.remove(ref); } @@ -827,7 +827,7 @@ namespace MWWorld reference.getRefData().enable(); if (mWorldScene->getActiveCells().find(reference.getCell()) != mWorldScene->getActiveCells().end() - && reference.getRefData().getCount()) + && reference.getCellRef().getCount()) mWorldScene->addObjectToScene(reference); if (reference.getCellRef().getRefNum().hasContentFile()) @@ -879,7 +879,7 @@ namespace MWWorld } if (mWorldScene->getActiveCells().find(reference.getCell()) != mWorldScene->getActiveCells().end() - && reference.getRefData().getCount()) + && reference.getCellRef().getCount()) { mWorldScene->removeObjectFromScene(reference); mWorldScene->addPostponedPhysicsObjects(); @@ -1039,12 +1039,12 @@ namespace MWWorld void World::deleteObject(const Ptr& ptr) { - if (!ptr.getRefData().isDeleted() && ptr.getContainerStore() == nullptr) + if (!ptr.mRef->isDeleted() && ptr.getContainerStore() == nullptr) { if (ptr == getPlayerPtr()) throw std::runtime_error("can not delete player object"); - ptr.getRefData().setCount(0); + ptr.getCellRef().setCount(0); if (ptr.isInCell() && mWorldScene->getActiveCells().find(ptr.getCell()) != mWorldScene->getActiveCells().end() @@ -1061,9 +1061,9 @@ namespace MWWorld { if (!ptr.getCellRef().hasContentFile()) return; - if (ptr.getRefData().isDeleted()) + if (ptr.mRef->isDeleted()) { - ptr.getRefData().setCount(1); + ptr.getCellRef().setCount(1); if (mWorldScene->getActiveCells().find(ptr.getCell()) != mWorldScene->getActiveCells().end() && ptr.getRefData().isEnabled()) { @@ -1392,7 +1392,7 @@ namespace MWWorld MWWorld::Ptr World::placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, const ESM::Position& pos) { - return copyObjectToCell(ptr, cell, pos, ptr.getRefData().getCount(), false); + return copyObjectToCell(ptr, cell, pos, ptr.getCellRef().getCount(), false); } MWWorld::Ptr World::safePlaceObject(const ConstPtr& ptr, const ConstPtr& referenceObject, @@ -1443,7 +1443,7 @@ namespace MWWorld ipos.rot[1] = 0; } - MWWorld::Ptr placed = copyObjectToCell(ptr, referenceCell, ipos, ptr.getRefData().getCount(), false); + MWWorld::Ptr placed = copyObjectToCell(ptr, referenceCell, ipos, ptr.getCellRef().getCount(), false); adjustPosition(placed, true); // snap to ground return placed; } @@ -1893,7 +1893,7 @@ namespace MWWorld { MWWorld::LiveCellRef& ref = *static_cast*>(ptr.getBase()); - if (!ref.mData.isEnabled() || ref.mData.isDeleted()) + if (!ref.mData.isEnabled() || ref.isDeleted()) return true; if (ref.mRef.getTeleport()) @@ -2541,7 +2541,7 @@ namespace MWWorld bool operator()(const MWWorld::Ptr& ptr) { - if (ptr.getRefData().isDeleted()) + if (ptr.mRef->isDeleted()) return true; // vanilla Morrowind does not allow to sell items from containers with zero capacity @@ -3337,7 +3337,7 @@ namespace MWWorld >= mSquaredDist) return true; - if (!ptr.getRefData().isEnabled() || ptr.getRefData().isDeleted()) + if (!ptr.getRefData().isEnabled() || ptr.mRef->isDeleted()) return true; // Consider references inside containers as well (except if we are looking for a Creature, they cannot be in @@ -3632,9 +3632,8 @@ namespace MWWorld if (texture.empty()) texture = Fallback::Map::getString("Blood_Texture_0"); - std::string model = Misc::ResourceHelpers::correctMeshPath( - std::string{ Fallback::Map::getString("Blood_Model_" + std::to_string(Misc::Rng::rollDice(3))) }, // [0, 2] - mResourceSystem->getVFS()); + std::string model = Misc::ResourceHelpers::correctMeshPath(std::string{ + Fallback::Map::getString("Blood_Model_" + std::to_string(Misc::Rng::rollDice(3))) } /*[0, 2]*/); mRendering->spawnEffect(model, texture, worldPosition, 1.0f, false); } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 4b9a0ccb98..b5d56753b0 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -275,7 +275,7 @@ namespace MWWorld std::string_view getCellName(const MWWorld::Cell& cell) const override; std::string_view getCellName(const ESM::Cell* cell) const override; - void removeRefScript(MWWorld::RefData* ref) override; + void removeRefScript(const MWWorld::CellRef* ref) override; //< Remove the script attached to ref from mLocalScripts Ptr getPtr(const ESM::RefId& name, bool activeOnly) override; diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 4f93319c96..967511953d 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -28,6 +28,7 @@ file(GLOB UNITTEST_SRC_FILES lua/test_l10n.cpp lua/test_storage.cpp lua/test_async.cpp + lua/test_inputactions.cpp lua/test_ui_content.cpp @@ -94,6 +95,8 @@ file(GLOB UNITTEST_SRC_FILES nifosg/testnifloader.cpp esmterrain/testgridsampling.cpp + + resource/testobjectcache.cpp ) source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) diff --git a/apps/openmw_test_suite/esm3/testsaveload.cpp b/apps/openmw_test_suite/esm3/testsaveload.cpp index 96796defd4..ff68d0d4f1 100644 --- a/apps/openmw_test_suite/esm3/testsaveload.cpp +++ b/apps/openmw_test_suite/esm3/testsaveload.cpp @@ -292,7 +292,7 @@ namespace ESM record.mFactionRank = std::numeric_limits::max(); record.mChargeInt = std::numeric_limits::max(); record.mEnchantmentCharge = std::numeric_limits::max(); - record.mGoldValue = std::numeric_limits::max(); + record.mCount = std::numeric_limits::max(); record.mTeleport = true; generateArray(record.mDoorDest.pos); generateArray(record.mDoorDest.rot); @@ -317,7 +317,7 @@ namespace ESM EXPECT_EQ(record.mFactionRank, result.mFactionRank); EXPECT_EQ(record.mChargeInt, result.mChargeInt); EXPECT_EQ(record.mEnchantmentCharge, result.mEnchantmentCharge); - EXPECT_EQ(record.mGoldValue, result.mGoldValue); + EXPECT_EQ(record.mCount, result.mCount); EXPECT_EQ(record.mTeleport, result.mTeleport); EXPECT_EQ(record.mDoorDest, result.mDoorDest); EXPECT_EQ(record.mDestCell, result.mDestCell); diff --git a/apps/openmw_test_suite/lua/test_inputactions.cpp b/apps/openmw_test_suite/lua/test_inputactions.cpp new file mode 100644 index 0000000000..5bdd39ada1 --- /dev/null +++ b/apps/openmw_test_suite/lua/test_inputactions.cpp @@ -0,0 +1,65 @@ +#include "gmock/gmock.h" +#include + +#include +#include + +#include "../testing_util.hpp" + +namespace +{ + using namespace testing; + using namespace TestingOpenMW; + + TEST(LuaInputActionsTest, MultiTree) + { + { + LuaUtil::InputAction::MultiTree tree; + auto a = tree.insert(); + auto b = tree.insert(); + auto c = tree.insert(); + auto d = tree.insert(); + EXPECT_TRUE(tree.multiEdge(c, { a, b })); + EXPECT_TRUE(tree.multiEdge(a, { d })); + EXPECT_FALSE(tree.multiEdge(d, { c })); + } + + { + LuaUtil::InputAction::MultiTree tree; + auto a = tree.insert(); + auto b = tree.insert(); + auto c = tree.insert(); + EXPECT_TRUE(tree.multiEdge(b, { a })); + EXPECT_TRUE(tree.multiEdge(c, { a, b })); + } + } + + TEST(LuaInputActionsTest, Registry) + { + 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) }); + registry.insert(a); + LuaUtil::InputAction::Info b({ "b", LuaUtil::InputAction::Type::Boolean, "test", "b_name", "b_description", + sol::make_object(lua, false) }); + registry.insert(b); + LuaUtil::Callback bindA({ lua.load("return function() return true end")(), sol::table(lua, sol::create) }); + LuaUtil::Callback bindBToA( + { lua.load("return function(_, _, aValue) return aValue end")(), sol::table(lua, sol::create) }); + EXPECT_TRUE(registry.bind("a", bindA, {})); + EXPECT_TRUE(registry.bind("b", bindBToA, { "a" })); + registry.update(1.0); + sol::object bValue = registry.valueOfType("b", LuaUtil::InputAction::Type::Boolean); + EXPECT_TRUE(bValue.is()); + LuaUtil::Callback badA( + { lua.load("return function() return 'not_a_bool' end")(), sol::table(lua, sol::create) }); + EXPECT_TRUE(registry.bind("a", badA, {})); + testing::internal::CaptureStderr(); + registry.update(1.0); + sol::object aValue = registry.valueOfType("a", LuaUtil::InputAction::Type::Boolean); + EXPECT_TRUE(aValue.is()); + bValue = registry.valueOfType("b", LuaUtil::InputAction::Type::Boolean); + EXPECT_TRUE(bValue.is() && bValue.as() == aValue.as()); + } +} diff --git a/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp b/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp index b97f30b319..a3f0d8d3c0 100644 --- a/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp +++ b/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp @@ -1,4 +1,5 @@ #include "apps/openmw/mwdialogue/keywordsearch.hpp" + #include struct KeywordSearchTest : public ::testing::Test diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp index b623084fb9..dbed262235 100644 --- a/apps/openmw_test_suite/mwscript/test_scripts.cpp +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -19,7 +19,16 @@ namespace mErrorHandler.reset(); std::istringstream input(scriptBody); Compiler::Scanner scanner(mErrorHandler, input, mCompilerContext.getExtensions()); - scanner.scan(mParser); + try + { + scanner.scan(mParser); + } + catch (...) + { + if (!shouldFail) + logErrors(); + throw; + } if (mErrorHandler.isGood()) return CompiledScript(mParser.getProgram(), mParser.getLocals()); else if (!shouldFail) @@ -385,6 +394,12 @@ if (player->GameHour == 10) set player->GameHour to 20 endif +End)mwscript"; + + const std::string sIssue4996 = R"mwscript(---Begin issue4996 + +player-> SetPos, Z, myZ + 50 + End)mwscript"; const std::string sIssue5087 = R"mwscript(Begin Begin @@ -457,6 +472,9 @@ set a to 1 -+'\/.,><$@---!=\/?--------(){}------ show a +( GetDisabled == 1 ) +GetDisabled == 1 + End)mwscript"; TEST_F(MWScriptTest, mwscript_test_invalid) @@ -810,6 +828,12 @@ End)mwscript"; EXPECT_FALSE(!compile(sIssue4888)); } + TEST_F(MWScriptTest, mwscript_test_4996) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue4996)); + } + TEST_F(MWScriptTest, mwscript_test_5087) { registerExtensions(); diff --git a/apps/openmw_test_suite/mwworld/test_store.cpp b/apps/openmw_test_suite/mwworld/test_store.cpp index d8890bc5ab..b63c0902ab 100644 --- a/apps/openmw_test_suite/mwworld/test_store.cpp +++ b/apps/openmw_test_suite/mwworld/test_store.cpp @@ -270,10 +270,8 @@ namespace std::vector result({ ESM::DefaultFormatVersion, ESM::CurrentContentFormatVersion, - ESM::MaxOldWeatherFormatVersion, - ESM::MaxOldDeathAnimationFormatVersion, - ESM::MaxOldForOfWarFormatVersion, - ESM::MaxWerewolfDeprecatedDataFormatVersion, + ESM::MaxOldFogOfWarFormatVersion, + ESM::MaxUnoptimizedCharacterDataFormatVersion, ESM::MaxOldTimeLeftFormatVersion, ESM::MaxIntFallbackFormatVersion, ESM::MaxClearModifiersFormatVersion, diff --git a/apps/openmw_test_suite/nif/node.hpp b/apps/openmw_test_suite/nif/node.hpp index 76cc6ac687..4e21698501 100644 --- a/apps/openmw_test_suite/nif/node.hpp +++ b/apps/openmw_test_suite/nif/node.hpp @@ -53,6 +53,7 @@ namespace Nif::Testing { value.mData = NiSkinDataPtr(nullptr); value.mRoot = NiAVObjectPtr(nullptr); + value.mPartitions = NiSkinPartitionPtr(nullptr); } inline void init(NiTimeController& value) diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp index 7dce21bac6..d44561a68b 100644 --- a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp +++ b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp @@ -144,6 +144,12 @@ namespace Resource return stream << "}}"; } + static std::ostream& operator<<(std::ostream& stream, const ScaledTriangleMeshShape& value) + { + return stream << "Resource::ScaledTriangleMeshShape {" << value.getLocalScaling() << ", " + << value.getChildShape() << "}"; + } + static bool operator==(const CollisionBox& l, const CollisionBox& r) { const auto tie = [](const CollisionBox& v) { return std::tie(v.mExtents, v.mCenter); }; @@ -169,6 +175,10 @@ static std::ostream& operator<<(std::ostream& stream, const btCollisionShape& va if (const auto casted = dynamic_cast(&value)) return stream << *casted; break; + case SCALED_TRIANGLE_MESH_SHAPE_PROXYTYPE: + if (const auto casted = dynamic_cast(&value)) + return stream << *casted; + break; } return stream << "btCollisionShape {" << value.getShapeType() << "}"; } @@ -249,6 +259,12 @@ static bool operator==(const btBvhTriangleMeshShape& lhs, const btBvhTriangleMes && lhs.getOwnsBvh() == rhs.getOwnsBvh() && isNear(getTriangles(lhs), getTriangles(rhs)); } +static bool operator==(const btScaledBvhTriangleMeshShape& lhs, const btScaledBvhTriangleMeshShape& rhs) +{ + return isNear(lhs.getLocalScaling(), rhs.getLocalScaling()) + && compareObjects(lhs.getChildShape(), rhs.getChildShape()); +} + static bool operator==(const btCollisionShape& lhs, const btCollisionShape& rhs) { if (lhs.getShapeType() != rhs.getShapeType()) @@ -264,6 +280,11 @@ static bool operator==(const btCollisionShape& lhs, const btCollisionShape& rhs) if (const auto rhsCasted = dynamic_cast(&rhs)) return *lhsCasted == *rhsCasted; return false; + case SCALED_TRIANGLE_MESH_SHAPE_PROXYTYPE: + if (const auto lhsCasted = dynamic_cast(&lhs)) + if (const auto rhsCasted = dynamic_cast(&rhs)) + return *lhsCasted == *rhsCasted; + return false; } return false; } @@ -572,7 +593,9 @@ namespace triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -596,7 +619,9 @@ namespace triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -619,7 +644,9 @@ namespace triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -644,7 +671,9 @@ namespace triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -669,9 +698,13 @@ namespace std::unique_ptr triangles2(new btTriangleMesh(false)); triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + auto triShape = std::make_unique(triangles.release(), true); + auto triShape2 = std::make_unique(triangles2.release(), true); + compound->addChildShape( - btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles2.release(), true)); + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape2.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -695,7 +728,9 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -717,9 +752,8 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); - mesh->setLocalScaling(btVector3(3, 3, 3)); std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(mTransform, mesh.release()); + shape->addChildShape(mTransform, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(3, 3, 3))); Resource::BulletShape expected; expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = { { -1, 0 } }; @@ -744,9 +778,9 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); - mesh->setLocalScaling(btVector3(12, 12, 12)); std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(mTransformScale4, mesh.release()); + shape->addChildShape( + mTransformScale4, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(12, 12, 12))); Resource::BulletShape expected; expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = { { -1, 0 } }; @@ -776,16 +810,14 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); - mesh->setLocalScaling(btVector3(3, 3, 3)); std::unique_ptr triangles2(new btTriangleMesh(false)); triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); std::unique_ptr mesh2(new Resource::TriangleMeshShape(triangles2.release(), true)); - mesh2->setLocalScaling(btVector3(3, 3, 3)); std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(mTransform, mesh.release()); - shape->addChildShape(mTransform, mesh2.release()); + shape->addChildShape(mTransform, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(3, 3, 3))); + shape->addChildShape(mTransform, new Resource::ScaledTriangleMeshShape(mesh2.release(), btVector3(3, 3, 3))); Resource::BulletShape expected; expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = { { -1, 0 } }; @@ -813,9 +845,9 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); - mesh->setLocalScaling(btVector3(12, 12, 12)); std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(mTransformScale4, mesh.release()); + shape->addChildShape( + mTransformScale4, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(12, 12, 12))); Resource::BulletShape expected; expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = { { -1, 0 } }; @@ -849,16 +881,16 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); - mesh->setLocalScaling(btVector3(12, 12, 12)); std::unique_ptr triangles2(new btTriangleMesh(false)); triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); std::unique_ptr mesh2(new Resource::TriangleMeshShape(triangles2.release(), true)); - mesh2->setLocalScaling(btVector3(12, 12, 12)); std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(mTransformScale4, mesh.release()); - shape->addChildShape(mTransformScale4, mesh2.release()); + shape->addChildShape( + mTransformScale4, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(12, 12, 12))); + shape->addChildShape( + mTransformScale4, new Resource::ScaledTriangleMeshShape(mesh2.release(), btVector3(12, 12, 12))); Resource::BulletShape expected; expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = { { -1, 1 } }; @@ -880,14 +912,17 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr triangles2(new btTriangleMesh(false)); triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); + std::unique_ptr mesh2(new Resource::TriangleMeshShape(triangles2.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); compound->addChildShape( - btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles2.release(), true)); + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh2.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -911,9 +946,11 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mAvoidCollisionShape.reset(compound.release()); @@ -973,8 +1010,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1002,8 +1041,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1029,8 +1070,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1057,8 +1100,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1083,8 +1128,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1116,8 +1163,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1178,8 +1227,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); triangles->addTriangle(btVector3(1, 0, 0), btVector3(0, 1, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1267,9 +1318,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); - + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1298,14 +1350,14 @@ namespace std::unique_ptr triangles1(new btTriangleMesh(false)); triangles1->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr mesh1(new Resource::TriangleMeshShape(triangles1.release(), true)); - mesh1->setLocalScaling(btVector3(8, 8, 8)); std::unique_ptr triangles2(new btTriangleMesh(false)); triangles2->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr mesh2(new Resource::TriangleMeshShape(triangles2.release(), true)); - mesh2->setLocalScaling(btVector3(12, 12, 12)); std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(mTransformScale2, mesh1.release()); - shape->addChildShape(mTransformScale3, mesh2.release()); + shape->addChildShape( + mTransformScale2, new Resource::ScaledTriangleMeshShape(mesh1.release(), btVector3(8, 8, 8))); + shape->addChildShape( + mTransformScale3, new Resource::ScaledTriangleMeshShape(mesh2.release(), btVector3(12, 12, 12))); Resource::BulletShape expected; expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = { { -1, 0 } }; diff --git a/apps/openmw_test_suite/openmw/options.cpp b/apps/openmw_test_suite/openmw/options.cpp index d79a387645..fc89264f8c 100644 --- a/apps/openmw_test_suite/openmw/options.cpp +++ b/apps/openmw_test_suite/openmw/options.cpp @@ -1,4 +1,5 @@ -#include +#include "apps/openmw/options.hpp" + #include #include diff --git a/apps/openmw_test_suite/resource/testobjectcache.cpp b/apps/openmw_test_suite/resource/testobjectcache.cpp new file mode 100644 index 0000000000..1c8cff6af0 --- /dev/null +++ b/apps/openmw_test_suite/resource/testobjectcache.cpp @@ -0,0 +1,349 @@ +#include + +#include +#include + +#include + +namespace Resource +{ + namespace + { + using namespace ::testing; + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheShouldReturnNullptrByDefault) + { + osg::ref_ptr> cache(new GenericObjectCache); + EXPECT_EQ(cache->getRefFromObjectCache(42), nullptr); + } + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheOrNoneShouldReturnNulloptByDefault) + { + osg::ref_ptr> cache(new GenericObjectCache); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(42), std::nullopt); + } + + struct Object : osg::Object + { + Object() = default; + + Object(const Object& other, const osg::CopyOp& copyOp = osg::CopyOp()) + : osg::Object(other, copyOp) + { + } + + META_Object(ResourceTest, Object) + }; + + TEST(ResourceGenericObjectCacheTest, shouldStoreValues) + { + osg::ref_ptr> cache(new GenericObjectCache); + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + EXPECT_EQ(cache->getRefFromObjectCache(key), value); + } + + TEST(ResourceGenericObjectCacheTest, shouldStoreNullptrValues) + { + osg::ref_ptr> cache(new GenericObjectCache); + const int key = 42; + cache->addEntryToObjectCache(key, nullptr); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(nullptr)); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldExtendLifetimeForItemsWithZeroTimestamp) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value, 0); + value = nullptr; + + const double referenceTime = 1000; + const double expiryDelay = 1; + cache->update(referenceTime, expiryDelay); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + } + + TEST(ResourceGenericObjectCacheTest, addEntryToObjectCacheShouldReplaceExistingItemByKey) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const int key = 42; + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + cache->addEntryToObjectCache(key, value1); + ASSERT_EQ(cache->getRefFromObjectCache(key), value1); + cache->addEntryToObjectCache(key, value2); + EXPECT_EQ(cache->getRefFromObjectCache(key), value2); + } + + TEST(ResourceGenericObjectCacheTest, addEntryToObjectCacheShouldMarkLifetime) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 2; + + const int key = 42; + cache->addEntryToObjectCache(key, nullptr, referenceTime + expiryDelay); + + cache->update(referenceTime, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + 2 * expiryDelay, expiryDelay); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldRemoveExpiredItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 1; + + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + value = nullptr; + + cache->update(referenceTime, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay, expiryDelay); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldKeepExternallyReferencedItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 1; + + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + + cache->update(referenceTime, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay, expiryDelay); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(value)); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldKeepNotExpiredItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 2; + + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + value = nullptr; + + cache->update(referenceTime + expiryDelay, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay / 2, expiryDelay); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldKeepNotExpiredNullptrItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 2; + + const int key = 42; + cache->addEntryToObjectCache(key, nullptr); + + cache->update(referenceTime + expiryDelay, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay / 2, expiryDelay); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + } + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheOrNoneShouldNotExtendItemLifetime) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 2; + + const int key = 42; + cache->addEntryToObjectCache(key, nullptr); + + cache->update(referenceTime, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay / 2, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay, expiryDelay); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, lowerBoundShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + cache->addEntryToObjectCache("a", nullptr); + cache->addEntryToObjectCache("c", nullptr); + EXPECT_THAT(cache->lowerBound(std::string_view("b")), Optional(Pair("c", _))); + } + + TEST(ResourceGenericObjectCacheTest, shouldSupportRemovingItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + ASSERT_EQ(cache->getRefFromObjectCache(key), value); + cache->removeFromObjectCache(key); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, clearShouldRemoveAllItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const int key1 = 42; + const int key2 = 13; + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + cache->addEntryToObjectCache(key1, value1); + cache->addEntryToObjectCache(key2, value2); + + ASSERT_EQ(cache->getRefFromObjectCache(key1), value1); + ASSERT_EQ(cache->getRefFromObjectCache(key2), value2); + + cache->clear(); + + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key1), std::nullopt); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key2), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, callShouldIterateOverAllItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + osg::ref_ptr value3(new Object); + cache->addEntryToObjectCache(1, value1); + cache->addEntryToObjectCache(2, value2); + cache->addEntryToObjectCache(3, value3); + + std::vector> actual; + cache->call([&](int key, osg::Object* value) { actual.emplace_back(key, value); }); + + EXPECT_THAT(actual, ElementsAre(Pair(1, value1.get()), Pair(2, value2.get()), Pair(3, value3.get()))); + } + + TEST(ResourceGenericObjectCacheTest, getCacheSizeShouldReturnNumberOrAddedItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + cache->addEntryToObjectCache(13, value1); + cache->addEntryToObjectCache(42, value2); + + EXPECT_EQ(cache->getCacheSize(), 2); + } + + TEST(ResourceGenericObjectCacheTest, lowerBoundShouldReturnFirstNotLessThatGivenKey) + { + osg::ref_ptr> cache(new GenericObjectCache); + + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + osg::ref_ptr value3(new Object); + cache->addEntryToObjectCache(1, value1); + cache->addEntryToObjectCache(2, value2); + cache->addEntryToObjectCache(4, value3); + + EXPECT_THAT(cache->lowerBound(3), Optional(Pair(4, value3))); + } + + TEST(ResourceGenericObjectCacheTest, lowerBoundShouldReturnNulloptWhenKeyIsGreaterThanAnyOther) + { + osg::ref_ptr> cache(new GenericObjectCache); + + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + osg::ref_ptr value3(new Object); + cache->addEntryToObjectCache(1, value1); + cache->addEntryToObjectCache(2, value2); + cache->addEntryToObjectCache(3, value3); + + EXPECT_EQ(cache->lowerBound(4), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, addEntryToObjectCacheShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(std::string_view("key"), value); + EXPECT_EQ(cache->getRefFromObjectCache(key), value); + } + + TEST(ResourceGenericObjectCacheTest, addEntryToObjectCacheShouldKeyMoving) + { + osg::ref_ptr> cache(new GenericObjectCache); + std::string key(128, 'a'); + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(std::move(key), value); + EXPECT_EQ(key, ""); + EXPECT_EQ(cache->getRefFromObjectCache(std::string(128, 'a')), value); + } + + TEST(ResourceGenericObjectCacheTest, removeFromObjectCacheShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + ASSERT_EQ(cache->getRefFromObjectCache(key), value); + cache->removeFromObjectCache(std::string_view("key")); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + EXPECT_EQ(cache->getRefFromObjectCache(std::string_view("key")), value); + } + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheOrNoneShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(std::string_view("key")), Optional(value)); + } + + TEST(ResourceGenericObjectCacheTest, checkInObjectCacheShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + EXPECT_TRUE(cache->checkInObjectCache(std::string_view("key"), 0)); + } + } +} diff --git a/apps/openmw_test_suite/settings/testvalues.cpp b/apps/openmw_test_suite/settings/testvalues.cpp index 81af308795..236417b559 100644 --- a/apps/openmw_test_suite/settings/testvalues.cpp +++ b/apps/openmw_test_suite/settings/testvalues.cpp @@ -59,6 +59,33 @@ namespace Settings EXPECT_EQ(values.mCamera.mFieldOfView.get(), 1); } + TEST_F(SettingsValuesTest, constructorWithDefaultShouldDoLookup) + { + Manager::mUserSettings[std::make_pair("category", "value")] = "13"; + Index index; + SettingValue value{ index, "category", "value", 42 }; + EXPECT_EQ(value.get(), 13); + value.reset(); + EXPECT_EQ(value.get(), 42); + } + + TEST_F(SettingsValuesTest, constructorWithDefaultShouldSanitize) + { + Manager::mUserSettings[std::make_pair("category", "value")] = "2"; + Index index; + SettingValue value{ index, "category", "value", -1, Settings::makeClampSanitizerInt(0, 1) }; + EXPECT_EQ(value.get(), 1); + value.reset(); + EXPECT_EQ(value.get(), 0); + } + + TEST_F(SettingsValuesTest, constructorWithDefaultShouldFallbackToDefault) + { + Index index; + const SettingValue value{ index, "category", "value", 42 }; + EXPECT_EQ(value.get(), 42); + } + TEST_F(SettingsValuesTest, moveConstructorShouldSetDefaults) { Index index; @@ -79,6 +106,13 @@ namespace Settings EXPECT_EQ(values.mCamera.mFieldOfView.get(), 1); } + TEST_F(SettingsValuesTest, moveConstructorShouldThrowOnMissingSetting) + { + Index index; + SettingValue defaultValue{ index, "category", "value", 42 }; + EXPECT_THROW([&] { SettingValue value(std::move(defaultValue)); }(), std::runtime_error); + } + TEST_F(SettingsValuesTest, findShouldThrowExceptionOnTypeMismatch) { Index index; diff --git a/apps/openmw_test_suite/testing_util.hpp b/apps/openmw_test_suite/testing_util.hpp index 89fb7f9e73..0c941053a7 100644 --- a/apps/openmw_test_suite/testing_util.hpp +++ b/apps/openmw_test_suite/testing_util.hpp @@ -57,13 +57,13 @@ namespace TestingOpenMW { } - void listResources(std::map& out) override + void listResources(VFS::FileMap& out) override { for (const auto& [key, value] : mFiles) out.emplace(VFS::Path::normalizeFilename(key), value); } - bool contains(const std::string& file) const override { return mFiles.count(file) != 0; } + bool contains(std::string_view file) const override { return mFiles.contains(file); } std::string getDescription() const override { return "TestData"; } }; diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index 588b100ef2..8c459f4f9c 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -34,20 +34,20 @@ set(WIZARD_HEADER ) set(WIZARD_UI - ${CMAKE_SOURCE_DIR}/files/ui/wizard/componentselectionpage.ui - ${CMAKE_SOURCE_DIR}/files/ui/wizard/conclusionpage.ui - ${CMAKE_SOURCE_DIR}/files/ui/wizard/existinginstallationpage.ui - ${CMAKE_SOURCE_DIR}/files/ui/wizard/importpage.ui - ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationtargetpage.ui - ${CMAKE_SOURCE_DIR}/files/ui/wizard/intropage.ui - ${CMAKE_SOURCE_DIR}/files/ui/wizard/languageselectionpage.ui - ${CMAKE_SOURCE_DIR}/files/ui/wizard/methodselectionpage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/componentselectionpage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/conclusionpage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/existinginstallationpage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/importpage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/installationtargetpage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/intropage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/languageselectionpage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/methodselectionpage.ui ) if (OPENMW_USE_UNSHIELD) set (WIZARD ${WIZARD} installationpage.cpp unshield/unshieldworker.cpp) set (WIZARD_HEADER ${WIZARD_HEADER} installationpage.hpp unshield/unshieldworker.hpp) - set (WIZARD_UI ${WIZARD_UI} ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationpage.ui) + set (WIZARD_UI ${WIZARD_UI} ${CMAKE_CURRENT_SOURCE_DIR}/ui/installationpage.ui) add_definitions(-DOPENMW_USE_UNSHIELD) endif (OPENMW_USE_UNSHIELD) diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index e492f4b83a..63f2eff078 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -138,10 +138,10 @@ bool Wizard::ComponentSelectionPage::validatePage() msgBox.setIcon(QMessageBox::Information); msgBox.setStandardButtons(QMessageBox::Cancel); msgBox.setText( - tr("

You are about to install Tribunal

\ -

Bloodmoon is already installed on your computer.

\ -

However, it is recommended that you install Tribunal before Bloodmoon.

\ -

Would you like to re-install Bloodmoon?

")); + tr("

You are about to install Tribunal

" + "

Bloodmoon is already installed on your computer.

" + "

However, it is recommended that you install Tribunal before Bloodmoon.

" + "

Would you like to re-install Bloodmoon?

")); QAbstractButton* reinstallButton = msgBox.addButton(tr("Re-install &Bloodmoon"), QMessageBox::ActionRole); diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp index a184c745ee..4a4a4ef689 100644 --- a/apps/wizard/conclusionpage.cpp +++ b/apps/wizard/conclusionpage.cpp @@ -37,22 +37,23 @@ void Wizard::ConclusionPage::initializePage() if (field(QLatin1String("installation.retailDisc")).toBool() == true) { textLabel->setText( - tr("

The OpenMW Wizard successfully installed Morrowind on your computer.

\ -

Click Finish to close the Wizard.

")); + tr("

The OpenMW Wizard successfully installed Morrowind on your computer.

" + "

Click Finish to close the Wizard.

")); } else { - textLabel->setText(tr( - "

The OpenMW Wizard successfully modified your existing Morrowind installation.

\ -

Click Finish to close the Wizard.

")); + textLabel->setText( + tr("

The OpenMW Wizard successfully modified your existing Morrowind " + "installation.

Click Finish to close the Wizard.

")); } } else { textLabel->setText( - tr("

The OpenMW Wizard failed to install Morrowind on your computer.

\ -

Please report any bugs you might have encountered to our \ - bug tracker.
Make sure to include the installation log.


")); + tr("

The OpenMW Wizard failed to install Morrowind on your computer.

" + "

Please report any bugs you might have encountered to our " + "bug tracker.
Make sure to include the " + "installation log.


")); } } diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 71ae331a61..d5ba009799 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -58,9 +58,9 @@ bool Wizard::ExistingInstallationPage::validatePage() msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Cancel); msgBox.setText( - QObject::tr("
Could not find Morrowind.ini

\ - The Wizard needs to update settings in this file.

\ - Press \"Browse...\" to specify the location manually.
")); + QObject::tr("
Could not find Morrowind.ini

" + "The Wizard needs to update settings in this file.

" + "Press \"Browse...\" to specify the location manually.
")); QAbstractButton* browseButton2 = msgBox.addButton(QObject::tr("B&rowse..."), QMessageBox::ActionRole); @@ -107,8 +107,8 @@ void Wizard::ExistingInstallationPage::on_browseButton_clicked() msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - QObject::tr("Morrowind.bsa is missing!
\ - Make sure your Morrowind installation is complete.")); + QObject::tr("Morrowind.bsa is missing!
" + "Make sure your Morrowind installation is complete.")); msgBox.exec(); return; } @@ -187,8 +187,8 @@ bool Wizard::ExistingInstallationPage::versionIsOK(QString directory_name) msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); msgBox.setText( - QObject::tr("
There may be a more recent version of Morrowind available.

\ - Do you wish to continue anyway?
")); + QObject::tr("
There may be a more recent version of Morrowind available.

" + "Do you wish to continue anyway?
")); int ret = msgBox.exec(); if (ret == QMessageBox::Yes) { diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp index e06972332a..60e9f3ccf9 100644 --- a/apps/wizard/installationpage.cpp +++ b/apps/wizard/installationpage.cpp @@ -1,5 +1,6 @@ #include "installationpage.hpp" +#include #include #include #include @@ -175,8 +176,8 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component) if (path.isEmpty()) { logTextEdit->appendHtml( - tr("


\ - Error: The installation was aborted by the user

")); + tr("


" + "Error: The installation was aborted by the user

")); mWizard->addLogText(QLatin1String("Error: The installation was aborted by the user")); mWizard->mError = true; @@ -205,8 +206,8 @@ void Wizard::InstallationPage::showOldVersionDialog() if (ret == QMessageBox::No) { logTextEdit->appendHtml( - tr("


\ - Error: The installation was aborted by the user

")); + tr("


" + "Error: The installation was aborted by the user

")); mWizard->addLogText(QLatin1String("Error: The installation was aborted by the user")); mWizard->mError = true; @@ -236,14 +237,8 @@ void Wizard::InstallationPage::installationError(const QString& text, const QStr { installProgressLabel->setText(tr("Installation failed!")); - logTextEdit->appendHtml( - tr("


\ - Error: %1

") - .arg(text)); - logTextEdit->appendHtml( - tr("

\ - %1

") - .arg(details)); + logTextEdit->appendHtml(tr("


Error: %1

").arg(text)); + logTextEdit->appendHtml(tr("

%1

").arg(details)); mWizard->addLogText(QLatin1String("Error: ") + text); mWizard->addLogText(details); @@ -254,9 +249,9 @@ void Wizard::InstallationPage::installationError(const QString& text, const QStr msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("

The Wizard has encountered an error

\ -

The error reported was:

%1

\ -

Press "Show Details..." for more information.

") + tr("

The Wizard has encountered an error

" + "

The error reported was:

%1

" + "

Press "Show Details..." for more information.

") .arg(text)); msgBox.setDetailedText(details); diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index c32573184d..dc94d2d002 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -48,9 +48,9 @@ bool Wizard::InstallationTargetPage::validatePage() msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("

Could not create the destination directory

\ -

Please make sure you have the right permissions \ - and try again, or specify a different location.

")); + tr("

Could not create the destination directory

" + "

Please make sure you have the right permissions " + "and try again, or specify a different location.

")); msgBox.exec(); return false; } @@ -65,9 +65,9 @@ bool Wizard::InstallationTargetPage::validatePage() msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("

Could not write to the destination directory

\ -

Please make sure you have the right permissions \ - and try again, or specify a different location.

")); + tr("

Could not write to the destination directory

" + "

Please make sure you have the right permissions " + "and try again, or specify a different location.

")); msgBox.exec(); return false; } @@ -79,9 +79,10 @@ bool Wizard::InstallationTargetPage::validatePage() msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("

The destination directory is not empty

\ -

An existing Morrowind installation is present in the specified location.

\ -

Please specify a different location, or go back and select the location as an existing installation.

")); + tr("

The destination directory is not empty

" + "

An existing Morrowind installation is present in the specified location.

" + "

Please specify a different location, or go back and select the location as an existing " + "installation.

")); msgBox.exec(); return false; } diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index 9808d3c56c..7dcf642dd6 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -9,17 +9,19 @@ Wizard::LanguageSelectionPage::LanguageSelectionPage(QWidget* parent) setupUi(this); - registerField(QLatin1String("installation.language"), languageComboBox); + registerField(QLatin1String("installation.language"), languageComboBox, "currentData", "currentDataChanged"); } void Wizard::LanguageSelectionPage::initializePage() { - QStringList languages; - languages << QLatin1String("English") << QLatin1String("French") << QLatin1String("German") - << QLatin1String("Italian") << QLatin1String("Polish") << QLatin1String("Russian") - << QLatin1String("Spanish"); + QVector> languages = { { "English", tr("English") }, { "French", tr("French") }, + { "German", tr("German") }, { "Italian", tr("Italian") }, { "Polish", tr("Polish") }, + { "Russian", tr("Russian") }, { "Spanish", tr("Spanish") } }; - languageComboBox->addItems(languages); + for (auto lang : languages) + { + languageComboBox->addItem(lang.second, lang.first); + } } int Wizard::LanguageSelectionPage::nextId() const diff --git a/apps/wizard/main.cpp b/apps/wizard/main.cpp index e2b0d3874b..03ac24c8c0 100644 --- a/apps/wizard/main.cpp +++ b/apps/wizard/main.cpp @@ -28,8 +28,6 @@ int main(int argc, char* argv[]) app.setLibraryPaths(libraryPaths); #endif - QDir::setCurrent(dir.absolutePath()); - Wizard::MainWizard wizard; wizard.show(); diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 9abb61cfd7..2f1f373cfd 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -55,9 +55,9 @@ Wizard::MainWizard::MainWizard(QWidget* parent) &MainWizard::importerFinished); mLogError = tr( - "

Could not open %1 for writing

\ -

Please make sure you have the right permissions \ - and try again.

"); + "

Could not open %1 for writing

" + "

Please make sure you have the right permissions " + "and try again.

"); std::filesystem::create_directories(mCfgMgr.getUserConfigPath()); std::filesystem::create_directories(mCfgMgr.getUserDataPath()); @@ -139,9 +139,9 @@ void Wizard::MainWizard::addLogText(const QString& text) void Wizard::MainWizard::setupGameSettings() { QString message( - tr("

Could not open %1 for reading

\ -

Please make sure you have the right permissions \ - and try again.

")); + tr("

Could not open %1 for reading

" + "

Please make sure you have the right permissions " + "and try again.

")); // Load the user config file first, separately // So we can write it properly, uncontaminated @@ -210,9 +210,9 @@ void Wizard::MainWizard::setupLauncherSettings() path.append(QLatin1String(Config::LauncherSettings::sLauncherConfigFileName)); QString message( - tr("

Could not open %1 for reading

\ -

Please make sure you have the right permissions \ - and try again.

")); + tr("

Could not open %1 for reading

" + "

Please make sure you have the right permissions " + "and try again.

")); QFile file(path); @@ -271,7 +271,6 @@ void Wizard::MainWizard::runSettingsImporter() // Set encoding QString language(field(QLatin1String("installation.language")).toString()); - if (language == QLatin1String("Polish")) { arguments.append(QLatin1String("win1250")); @@ -427,9 +426,9 @@ void Wizard::MainWizard::writeSettings() msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("

Could not create %1

\ -

Please make sure you have the right permissions \ - and try again.

") + tr("

Could not create %1

" + "

Please make sure you have the right permissions " + "and try again.

") .arg(userPath)); connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection); msgBox.exec(); @@ -448,9 +447,9 @@ void Wizard::MainWizard::writeSettings() msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("

Could not open %1 for writing

\ -

Please make sure you have the right permissions \ - and try again.

") + tr("

Could not open %1 for writing

" + "

Please make sure you have the right permissions " + "and try again.

") .arg(file.fileName())); connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection); msgBox.exec(); @@ -475,9 +474,9 @@ void Wizard::MainWizard::writeSettings() msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("

Could not open %1 for writing

\ -

Please make sure you have the right permissions \ - and try again.

") + tr("

Could not open %1 for writing

" + "

Please make sure you have the right permissions " + "and try again.

") .arg(file.fileName())); connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection); msgBox.exec(); diff --git a/files/ui/wizard/componentselectionpage.ui b/apps/wizard/ui/componentselectionpage.ui similarity index 100% rename from files/ui/wizard/componentselectionpage.ui rename to apps/wizard/ui/componentselectionpage.ui diff --git a/files/ui/wizard/conclusionpage.ui b/apps/wizard/ui/conclusionpage.ui similarity index 100% rename from files/ui/wizard/conclusionpage.ui rename to apps/wizard/ui/conclusionpage.ui diff --git a/files/ui/wizard/existinginstallationpage.ui b/apps/wizard/ui/existinginstallationpage.ui similarity index 100% rename from files/ui/wizard/existinginstallationpage.ui rename to apps/wizard/ui/existinginstallationpage.ui diff --git a/files/ui/wizard/importpage.ui b/apps/wizard/ui/importpage.ui similarity index 100% rename from files/ui/wizard/importpage.ui rename to apps/wizard/ui/importpage.ui diff --git a/files/ui/wizard/installationpage.ui b/apps/wizard/ui/installationpage.ui similarity index 100% rename from files/ui/wizard/installationpage.ui rename to apps/wizard/ui/installationpage.ui diff --git a/files/ui/wizard/installationtargetpage.ui b/apps/wizard/ui/installationtargetpage.ui similarity index 100% rename from files/ui/wizard/installationtargetpage.ui rename to apps/wizard/ui/installationtargetpage.ui diff --git a/files/ui/wizard/intropage.ui b/apps/wizard/ui/intropage.ui similarity index 100% rename from files/ui/wizard/intropage.ui rename to apps/wizard/ui/intropage.ui diff --git a/files/ui/wizard/languageselectionpage.ui b/apps/wizard/ui/languageselectionpage.ui similarity index 100% rename from files/ui/wizard/languageselectionpage.ui rename to apps/wizard/ui/languageselectionpage.ui diff --git a/files/ui/wizard/methodselectionpage.ui b/apps/wizard/ui/methodselectionpage.ui similarity index 100% rename from files/ui/wizard/methodselectionpage.ui rename to apps/wizard/ui/methodselectionpage.ui diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 7e3c7aea23..f25a4cc621 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -44,7 +44,7 @@ list (APPEND COMPONENT_FILES "${OpenMW_BINARY_DIR}/${VERSION_CPP_FILE}") add_component_dir (lua luastate scriptscontainer asyncpackage utilpackage serialization configuration l10n storage utf8 - shapes/box + shapes/box inputactions ) add_component_dir (l10n @@ -130,7 +130,7 @@ add_component_dir (nif ) add_component_dir (nifosg - nifloader controller particle matrixtransform + nifloader controller particle matrixtransform fog ) add_component_dir (nifbullet @@ -166,7 +166,7 @@ add_component_dir (esm3 inventorystate containerstate npcstate creaturestate dialoguestate statstate npcstats creaturestats weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate debugprofile aisequence magiceffects custommarkerstate stolenitems transport animationstate controlsstate mappings readerscache - infoorder timestamp formatversion landrecorddata + infoorder timestamp formatversion landrecorddata selectiongroup ) add_component_dir (esmterrain @@ -490,7 +490,7 @@ else () ) endif() -set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui +set (ESM_UI ${CMAKE_CURRENT_SOURCE_DIR}/contentselector/contentselector.ui ) if (USE_QT) diff --git a/components/compiler/lineparser.cpp b/components/compiler/lineparser.cpp index 460dcfb2f9..90bdac1610 100644 --- a/components/compiler/lineparser.cpp +++ b/components/compiler/lineparser.cpp @@ -271,18 +271,14 @@ namespace Compiler mCode.insert(mCode.end(), code.begin(), code.end()); extensions->generateInstructionCode(keyword, mCode, mLiterals, mExplicit, optionals); + SkipParser skip(getErrorHandler(), getContext(), true); + scanner.scan(skip); + mState = EndState; return true; } - } - if (const Extensions* extensions = getContext().getExtensions()) - { char returnType; - std::string argumentType; - - bool hasExplicit = mState == ExplicitState; - if (extensions->isFunction(keyword, returnType, argumentType, hasExplicit)) { if (!hasExplicit && mState == ExplicitState) @@ -302,6 +298,9 @@ namespace Compiler int optionals = mExprParser.parseArguments(argumentType, scanner, code, keyword); mCode.insert(mCode.end(), code.begin(), code.end()); extensions->generateFunctionCode(keyword, mCode, mLiterals, mExplicit, optionals); + + SkipParser skip(getErrorHandler(), getContext(), true); + scanner.scan(skip); } mState = EndState; return true; diff --git a/components/compiler/scanner.hpp b/components/compiler/scanner.hpp index d5cea61e7f..34b122413e 100644 --- a/components/compiler/scanner.hpp +++ b/components/compiler/scanner.hpp @@ -70,7 +70,11 @@ namespace Compiler && mData[3] == 0; } - bool isDigit() const { return std::isdigit(mData[0]) && mData[1] == 0 && mData[2] == 0 && mData[3] == 0; } + bool isDigit() const + { + return std::isdigit(static_cast(mData[0])) && mData[1] == 0 && mData[2] == 0 + && mData[3] == 0; + } bool isMinusSign() const { @@ -85,7 +89,8 @@ namespace Compiler if (isMinusSign()) return false; - return std::isalpha(mData[0]) || mData[1] != 0 || mData[2] != 0 || mData[3] != 0; + return std::isalpha(static_cast(mData[0])) || mData[1] != 0 || mData[2] != 0 + || mData[3] != 0; } void appendTo(std::string& str) const diff --git a/components/compiler/skipparser.cpp b/components/compiler/skipparser.cpp index 0036e93dda..39d834c8ca 100644 --- a/components/compiler/skipparser.cpp +++ b/components/compiler/skipparser.cpp @@ -1,39 +1,55 @@ #include "skipparser.hpp" +#include "errorhandler.hpp" #include "scanner.hpp" namespace Compiler { - SkipParser::SkipParser(ErrorHandler& errorHandler, const Context& context) + SkipParser::SkipParser(ErrorHandler& errorHandler, const Context& context, bool reportStrayArguments) : Parser(errorHandler, context) + , mReportStrayArguments(reportStrayArguments) { } + void SkipParser::reportStrayArgument(const TokenLoc& loc) + { + if (mReportStrayArguments) + getErrorHandler().warning("Extra argument", loc); + } + bool SkipParser::parseInt(int value, const TokenLoc& loc, Scanner& scanner) { + reportStrayArgument(loc); return true; } bool SkipParser::parseFloat(float value, const TokenLoc& loc, Scanner& scanner) { + reportStrayArgument(loc); return true; } bool SkipParser::parseName(const std::string& name, const TokenLoc& loc, Scanner& scanner) { + reportStrayArgument(loc); return true; } bool SkipParser::parseKeyword(int keyword, const TokenLoc& loc, Scanner& scanner) { + reportStrayArgument(loc); return true; } bool SkipParser::parseSpecial(int code, const TokenLoc& loc, Scanner& scanner) { if (code == Scanner::S_newline) + { + if (mReportStrayArguments) + scanner.putbackSpecial(code, loc); return false; - + } + reportStrayArgument(loc); return true; } } diff --git a/components/compiler/skipparser.hpp b/components/compiler/skipparser.hpp index fdc5effc13..304ed40330 100644 --- a/components/compiler/skipparser.hpp +++ b/components/compiler/skipparser.hpp @@ -11,8 +11,12 @@ namespace Compiler class SkipParser : public Parser { + bool mReportStrayArguments; + + void reportStrayArgument(const TokenLoc& loc); + public: - SkipParser(ErrorHandler& errorHandler, const Context& context); + SkipParser(ErrorHandler& errorHandler, const Context& context, bool reportStrayArguments = false); bool parseInt(int value, const TokenLoc& loc, Scanner& scanner) override; ///< Handle an int token. diff --git a/files/ui/contentselector.ui b/components/contentselector/contentselector.ui similarity index 100% rename from files/ui/contentselector.ui rename to components/contentselector/contentselector.ui diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index 7b4f5db158..0aab06ac90 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -2,11 +2,15 @@ #include "esmfile.hpp" #include +#include #include #include +#include #include #include +#include +#include #include #include @@ -126,7 +130,7 @@ Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex& index continue; noGameFiles = false; - if (isChecked(depFile->filePath())) + if (mCheckedFiles.contains(depFile)) { gamefileChecked = true; break; @@ -164,20 +168,14 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex& index, int return isLoadOrderError(file) ? mWarningIcon : QVariant(); } - case Qt::BackgroundRole: + case Qt::FontRole: { if (isNew(file->fileName())) { - return QVariant(QColor(Qt::green)); - } - return QVariant(); - } - - case Qt::ForegroundRole: - { - if (isNew(file->fileName())) - { - return QVariant(QColor(Qt::black)); + auto font = QFont(); + font.setBold(true); + font.setItalic(true); + return font; } return QVariant(); } @@ -219,7 +217,7 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex& index, int if (file == mGameFile) return QVariant(); - return mCheckStates[file->filePath()]; + return mCheckedFiles.contains(file) ? Qt::Checked : Qt::Unchecked; } case Qt::UserRole: @@ -233,7 +231,7 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex& index, int } case Qt::UserRole + 1: - return isChecked(file->filePath()); + return mCheckedFiles.contains(file); } return QVariant(); } @@ -281,12 +279,12 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex& index, const { int checkValue = value.toInt(); bool setState = false; - if ((checkValue == Qt::Checked) && !isChecked(file->filePath())) + if (checkValue == Qt::Checked && !mCheckedFiles.contains(file)) { setState = true; success = true; } - else if ((checkValue == Qt::Checked) && isChecked(file->filePath())) + else if (checkValue == Qt::Checked && mCheckedFiles.contains(file)) setState = true; else if (checkValue == Qt::Unchecked) setState = true; @@ -319,34 +317,12 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex& index, const bool ContentSelectorModel::ContentModel::insertRows(int position, int rows, const QModelIndex& parent) { - if (parent.isValid()) - return false; - - beginInsertRows(parent, position, position + rows - 1); - { - for (int row = 0; row < rows; ++row) - mFiles.insert(position, new EsmFile); - } - endInsertRows(); - - return true; + return false; } bool ContentSelectorModel::ContentModel::removeRows(int position, int rows, const QModelIndex& parent) { - if (parent.isValid()) - return false; - - beginRemoveRows(parent, position, position + rows - 1); - { - for (int row = 0; row < rows; ++row) - delete mFiles.takeAt(position); - } - endRemoveRows(); - - // at this point we know that drag and drop has finished. - checkForLoadOrderErrors(); - return true; + return false; } Qt::DropActions ContentSelectorModel::ContentModel::supportedDropActions() const @@ -362,13 +338,14 @@ QStringList ContentSelectorModel::ContentModel::mimeTypes() const QMimeData* ContentSelectorModel::ContentModel::mimeData(const QModelIndexList& indexes) const { QByteArray encodedData; + QDataStream stream(&encodedData, QIODevice::WriteOnly); for (const QModelIndex& index : indexes) { if (!index.isValid()) continue; - encodedData.append(item(index.row())->encodedData()); + stream << index.row(); } QMimeData* mimeData = new QMimeData(); @@ -400,26 +377,31 @@ bool ContentSelectorModel::ContentModel::dropMimeData( QByteArray encodedData = data->data(mMimeType); QDataStream stream(&encodedData, QIODevice::ReadOnly); + std::vector toMove; while (!stream.atEnd()) { - - QString value; - QStringList values; - QStringList gamefiles; - - for (int i = 0; i < EsmFile::FileProperty_GameFile; ++i) - { - stream >> value; - values << value; - } - - stream >> gamefiles; - - insertRows(beginRow, 1); - - QModelIndex idx = index(beginRow++, 0, QModelIndex()); - setData(idx, QStringList() << values << gamefiles, Qt::EditRole); + int sourceRow; + stream >> sourceRow; + toMove.emplace_back(mFiles.at(sourceRow)); } + int minRow = mFiles.size(); + int maxRow = 0; + for (EsmFile* file : toMove) + { + int from = mFiles.indexOf(file); + int to = beginRow; + if (from < beginRow) + to--; + else if (from > beginRow) + beginRow++; + minRow = std::min(minRow, std::min(to, from)); + maxRow = std::max(maxRow, std::max(to, from)); + mFiles.move(from, to); + } + + dataChanged(index(minRow, 0), index(maxRow, 0)); + // at this point we know that drag and drop has finished. + checkForLoadOrderErrors(); return true; } @@ -452,26 +434,37 @@ void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newf { QFileInfo info(dir.absoluteFilePath(path2)); - if (item(info.fileName())) - continue; - // Enabled by default in system openmw.cfg; shouldn't be shown in content list. if (info.fileName().compare("builtin.omwscripts", Qt::CaseInsensitive) == 0) continue; + EsmFile* file = const_cast(item(info.fileName())); + bool add = file == nullptr; + std::unique_ptr newFile; + if (add) + { + newFile = std::make_unique(path2); + file = newFile.get(); + } + else + { + // We've found the same file in a higher priority dir, update our existing entry + file->setFileName(path2); + file->setGameFiles({}); + } + if (info.fileName().endsWith(".omwscripts", Qt::CaseInsensitive)) { - EsmFile* file = new EsmFile(path2); file->setDate(info.lastModified()); file->setFilePath(info.absoluteFilePath()); - addFile(file); + if (add) + addFile(newFile.release()); setNew(file->fileName(), newfiles); continue; } try { - EsmFile* file = new EsmFile(path2); file->setDate(info.lastModified()); file->setFilePath(info.absoluteFilePath()); std::filesystem::path filepath = Files::pathFromQString(info.absoluteFilePath()); @@ -494,7 +487,7 @@ void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newf fileReader.setEncoder(&encoder); fileReader.open(std::move(stream), filepath); file->setAuthor(QString::fromUtf8(fileReader.getAuthor().c_str())); - file->setFormat(fileReader.getFormatVersion()); + file->setFormat(QString::number(fileReader.esmVersionF())); file->setDescription(QString::fromUtf8(fileReader.getDesc().c_str())); for (const auto& master : fileReader.getGameFiles()) file->addGameFile(QString::fromUtf8(master.name.c_str())); @@ -510,11 +503,11 @@ void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newf case ESM::Format::Tes4: { ToUTF8::StatelessUtf8Encoder encoder(ToUTF8::calculateEncoding(mEncoding.toStdString())); - ESM4::Reader reader(std::move(stream), filepath, nullptr, &encoder, true); - file->setAuthor(QString::fromUtf8(reader.getAuthor().c_str())); - file->setFormat(reader.esmVersion()); - file->setDescription(QString::fromUtf8(reader.getDesc().c_str())); - for (const auto& master : reader.getGameFiles()) + ESM4::Reader fileReader(std::move(stream), filepath, nullptr, &encoder, true); + file->setAuthor(QString::fromUtf8(fileReader.getAuthor().c_str())); + file->setFormat(QString::number(fileReader.esmVersionF())); + file->setDescription(QString::fromUtf8(fileReader.getDesc().c_str())); + for (const auto& master : fileReader.getGameFiles()) file->addGameFile(QString::fromUtf8(master.name.c_str())); break; } @@ -527,14 +520,14 @@ void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newf } // Put the file in the table - addFile(file); + if (add) + addFile(newFile.release()); setNew(file->fileName(), newfiles); } catch (std::runtime_error& e) { // An error occurred while reading the .esp qWarning() << "Error reading addon file: " << e.what(); - continue; } } } @@ -559,6 +552,7 @@ void ContentSelectorModel::ContentModel::clearFiles() if (filesCount > 0) { beginRemoveRows(QModelIndex(), 0, filesCount - 1); + qDeleteAll(mFiles); mFiles.clear(); endRemoveRows(); } @@ -621,14 +615,6 @@ void ContentSelectorModel::ContentModel::sortFiles() emit layoutChanged(); } -bool ContentSelectorModel::ContentModel::isChecked(const QString& filepath) const -{ - const auto it = mCheckStates.find(filepath); - if (it == mCheckStates.end()) - return false; - return it.value() == Qt::Checked; -} - bool ContentSelectorModel::ContentModel::isEnabled(const QModelIndex& index) const { return (flags(index) & Qt::ItemIsEnabled); @@ -689,7 +675,7 @@ void ContentSelectorModel::ContentModel::checkForLoadOrderErrors() { for (int row = 0; row < mFiles.count(); ++row) { - EsmFile* file = item(row); + EsmFile* file = mFiles.at(row); bool isRowInError = checkForLoadOrderErrors(file, row).count() != 0; if (isRowInError) { @@ -716,7 +702,7 @@ QList ContentSelectorModel::ContentModel:: } else { - if (!isChecked(dependentFile->filePath())) + if (!mCheckedFiles.contains(dependentFile)) { errors.append(LoadOrderError(LoadOrderError::ErrorCode_InactiveDependency, dependentfileName)); } @@ -766,19 +752,18 @@ bool ContentSelectorModel::ContentModel::setCheckState(const QString& filepath, if (!file) return false; - Qt::CheckState state = Qt::Unchecked; - if (checkState) - state = Qt::Checked; + mCheckedFiles.insert(file); + else + mCheckedFiles.erase(file); - mCheckStates[filepath] = state; emit dataChanged(indexFromItem(item(filepath)), indexFromItem(item(filepath))); if (file->isGameFile()) refreshModel(); // if we're checking an item, ensure all "upstream" files (dependencies) are checked as well. - if (state == Qt::Checked) + if (checkState) { for (const QString& upstreamName : file->gameFiles()) { @@ -787,14 +772,13 @@ bool ContentSelectorModel::ContentModel::setCheckState(const QString& filepath, if (!upstreamFile) continue; - if (!isChecked(upstreamFile->filePath())) - mCheckStates[upstreamFile->filePath()] = Qt::Checked; + mCheckedFiles.insert(upstreamFile); emit dataChanged(indexFromItem(upstreamFile), indexFromItem(upstreamFile)); } } // otherwise, if we're unchecking an item (or the file is a game file) ensure all downstream files are unchecked. - if (state == Qt::Unchecked) + else { for (const EsmFile* downstreamFile : mFiles) { @@ -803,8 +787,7 @@ bool ContentSelectorModel::ContentModel::setCheckState(const QString& filepath, if (downstreamFile->gameFiles().contains(filename, Qt::CaseInsensitive)) { - if (mCheckStates.contains(downstreamFile->filePath())) - mCheckStates[downstreamFile->filePath()] = Qt::Unchecked; + mCheckedFiles.erase(downstreamFile); emit dataChanged(indexFromItem(downstreamFile), indexFromItem(downstreamFile)); } @@ -822,7 +805,7 @@ ContentSelectorModel::ContentFileList ContentSelectorModel::ContentModel::checke // First search for game files and next addons, // so we get more or less correct game files vs addons order. for (EsmFile* file : mFiles) - if (isChecked(file->filePath())) + if (mCheckedFiles.contains(file)) list << file; return list; @@ -831,6 +814,6 @@ ContentSelectorModel::ContentFileList ContentSelectorModel::ContentModel::checke void ContentSelectorModel::ContentModel::uncheckAll() { emit layoutAboutToBeChanged(); - mCheckStates.clear(); + mCheckedFiles.clear(); emit layoutChanged(); } diff --git a/components/contentselector/model/contentmodel.hpp b/components/contentselector/model/contentmodel.hpp index d56f8f9a3b..1ba3090a32 100644 --- a/components/contentselector/model/contentmodel.hpp +++ b/components/contentselector/model/contentmodel.hpp @@ -7,6 +7,8 @@ #include #include +#include + namespace ContentSelectorModel { class EsmFile; @@ -57,7 +59,6 @@ namespace ContentSelectorModel void setCurrentGameFile(const EsmFile* file); bool isEnabled(const QModelIndex& index) const; - bool isChecked(const QString& filepath) const; bool setCheckState(const QString& filepath, bool isChecked); bool isNew(const QString& filepath) const; void setNew(const QString& filepath, bool isChecked); @@ -85,7 +86,7 @@ namespace ContentSelectorModel const EsmFile* mGameFile; ContentFileList mFiles; QStringList mArchives; - QHash mCheckStates; + std::set mCheckedFiles; QHash mNewFiles; QSet mPluginsWithLoadOrderError; QString mEncoding; diff --git a/components/contentselector/model/esmfile.cpp b/components/contentselector/model/esmfile.cpp index 741ed173a2..e4280baef7 100644 --- a/components/contentselector/model/esmfile.cpp +++ b/components/contentselector/model/esmfile.cpp @@ -1,17 +1,5 @@ #include "esmfile.hpp" -#include -#include - -int ContentSelectorModel::EsmFile::sPropertyCount = 7; -QString ContentSelectorModel::EsmFile::sToolTip = QString( - "Author: %1
\ - Version: %2
\ - Modified: %3
\ - Path:
%4
\ -
Description:
%5
\ -
Dependencies: %6
"); - ContentSelectorModel::EsmFile::EsmFile(const QString& fileName, ModelItem* parent) : ModelItem(parent) , mFileName(fileName) @@ -33,7 +21,7 @@ void ContentSelectorModel::EsmFile::setDate(const QDateTime& modified) mModified = modified; } -void ContentSelectorModel::EsmFile::setFormat(int format) +void ContentSelectorModel::EsmFile::setFormat(const QString& format) { mVersion = format; } @@ -53,17 +41,6 @@ void ContentSelectorModel::EsmFile::setDescription(const QString& description) mDescription = description; } -QByteArray ContentSelectorModel::EsmFile::encodedData() const -{ - QByteArray encodedData; - QDataStream stream(&encodedData, QIODevice::WriteOnly); - - stream << mFileName << mAuthor << QString::number(mVersion) << mModified.toString() << mPath << mDescription - << mGameFiles; - - return encodedData; -} - bool ContentSelectorModel::EsmFile::isGameFile() const { return (mGameFiles.size() == 0) @@ -121,11 +98,11 @@ void ContentSelectorModel::EsmFile::setFileProperty(const FileProperty prop, con break; case FileProperty_Format: - mVersion = value.toInt(); + mVersion = value; break; case FileProperty_DateModified: - mModified = QDateTime::fromString(value); + mModified = QDateTime::fromString(value, Qt::ISODate); break; case FileProperty_FilePath: diff --git a/components/contentselector/model/esmfile.hpp b/components/contentselector/model/esmfile.hpp index 1ffaa8fe72..606cc3d319 100644 --- a/components/contentselector/model/esmfile.hpp +++ b/components/contentselector/model/esmfile.hpp @@ -30,17 +30,13 @@ namespace ContentSelectorModel }; EsmFile(const QString& fileName = QString(), ModelItem* parent = nullptr); - // EsmFile(const EsmFile &); - - ~EsmFile() {} void setFileProperty(const FileProperty prop, const QString& value); void setFileName(const QString& fileName); void setAuthor(const QString& author); - void setSize(const int size); void setDate(const QDateTime& modified); - void setFormat(const int format); + void setFormat(const QString& format); void setFilePath(const QString& path); void setGameFiles(const QStringList& gameFiles); void setDescription(const QString& description); @@ -51,7 +47,7 @@ namespace ContentSelectorModel QString fileName() const { return mFileName; } QString author() const { return mAuthor; } QDateTime modified() const { return mModified; } - ESM::FormatVersion formatVersion() const { return mVersion; } + QString formatVersion() const { return mVersion; } QString filePath() const { return mPath; } /// @note Contains file names, not paths. @@ -59,7 +55,7 @@ namespace ContentSelectorModel QString description() const { return mDescription; } QString toolTip() const { - return sToolTip.arg(mAuthor) + return mTooltipTemlate.arg(mAuthor) .arg(mVersion) .arg(mModified.toString(Qt::ISODate)) .arg(mPath) @@ -68,17 +64,20 @@ namespace ContentSelectorModel } bool isGameFile() const; - QByteArray encodedData() const; - - public: - static int sPropertyCount; - static QString sToolTip; private: + QString mTooltipTemlate = tr( + "Author: %1
" + "Format version: %2
" + "Modified: %3
" + "Path:
%4
" + "
Description:
%5
" + "
Dependencies: %6
"); + QString mFileName; QString mAuthor; QDateTime mModified; - ESM::FormatVersion mVersion = ESM::DefaultFormatVersion; + QString mVersion = QString::number(ESM::DefaultFormatVersion); QString mPath; QStringList mGameFiles; QString mDescription; diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index 2f01200927..00c32e272d 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -1,5 +1,7 @@ #include "contentselector.hpp" +#include "ui_contentselector.h" + #include #include @@ -9,14 +11,15 @@ ContentSelectorView::ContentSelector::ContentSelector(QWidget* parent, bool showOMWScripts) : QObject(parent) + , ui(std::make_unique()) { - ui.setupUi(parent); - ui.addonView->setDragDropMode(QAbstractItemView::InternalMove); + ui->setupUi(parent); + ui->addonView->setDragDropMode(QAbstractItemView::InternalMove); if (!showOMWScripts) { - ui.languageComboBox->setHidden(true); - ui.refreshButton->setHidden(true); + ui->languageComboBox->setHidden(true); + ui->refreshButton->setHidden(true); } buildContentModel(showOMWScripts); @@ -24,21 +27,23 @@ ContentSelectorView::ContentSelector::ContentSelector(QWidget* parent, bool show buildAddonView(); } +ContentSelectorView::ContentSelector::~ContentSelector() = default; + void ContentSelectorView::ContentSelector::buildContentModel(bool showOMWScripts) { - QIcon warningIcon(ui.addonView->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(QSize(16, 15))); + QIcon warningIcon(ui->addonView->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(QSize(16, 15))); mContentModel = new ContentSelectorModel::ContentModel(this, warningIcon, showOMWScripts); } void ContentSelectorView::ContentSelector::buildGameFileView() { - ui.gameFileView->addItem(""); - ui.gameFileView->setVisible(true); + ui->gameFileView->addItem(tr("")); + ui->gameFileView->setVisible(true); - connect(ui.gameFileView, qOverload(&ComboBox::currentIndexChanged), this, + connect(ui->gameFileView, qOverload(&ComboBox::currentIndexChanged), this, &ContentSelector::slotCurrentGameFileIndexChanged); - ui.gameFileView->setCurrentIndex(0); + ui->gameFileView->setCurrentIndex(0); } class AddOnProxyModel : public QSortFilterProxyModel @@ -60,9 +65,34 @@ public: } }; +bool ContentSelectorView::ContentSelector::isGamefileSelected() const +{ + return ui->gameFileView->currentIndex() > 0; +} + +QWidget* ContentSelectorView::ContentSelector::uiWidget() const +{ + return ui->contentGroupBox; +} + +QComboBox* ContentSelectorView::ContentSelector::languageBox() const +{ + return ui->languageComboBox; +} + +QToolButton* ContentSelectorView::ContentSelector::refreshButton() const +{ + return ui->refreshButton; +} + +QLineEdit* ContentSelectorView::ContentSelector::searchFilter() const +{ + return ui->searchFilter; +} + void ContentSelectorView::ContentSelector::buildAddonView() { - ui.addonView->setVisible(true); + ui->addonView->setVisible(true); mAddonProxyModel = new AddOnProxyModel(this); mAddonProxyModel->setFilterRegularExpression(searchFilter()->text()); @@ -70,23 +100,24 @@ void ContentSelectorView::ContentSelector::buildAddonView() mAddonProxyModel->setDynamicSortFilter(true); mAddonProxyModel->setSourceModel(mContentModel); - connect(ui.searchFilter, &QLineEdit::textEdited, mAddonProxyModel, &QSortFilterProxyModel::setFilterWildcard); - connect(ui.searchFilter, &QLineEdit::textEdited, this, &ContentSelector::slotSearchFilterTextChanged); + connect(ui->searchFilter, &QLineEdit::textEdited, mAddonProxyModel, &QSortFilterProxyModel::setFilterWildcard); + connect(ui->searchFilter, &QLineEdit::textEdited, this, &ContentSelector::slotSearchFilterTextChanged); - ui.addonView->setModel(mAddonProxyModel); + ui->addonView->setModel(mAddonProxyModel); - connect(ui.addonView, &QTableView::activated, this, &ContentSelector::slotAddonTableItemActivated); + connect(ui->addonView, &QTableView::activated, this, &ContentSelector::slotAddonTableItemActivated); connect(mContentModel, &ContentSelectorModel::ContentModel::dataChanged, this, &ContentSelector::signalAddonDataChanged); + connect(mContentModel, &ContentSelectorModel::ContentModel::dataChanged, this, &ContentSelector::slotRowsMoved); buildContextMenu(); } void ContentSelectorView::ContentSelector::buildContextMenu() { - ui.addonView->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui.addonView, &QTableView::customContextMenuRequested, this, &ContentSelector::slotShowContextMenu); + ui->addonView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->addonView, &QTableView::customContextMenuRequested, this, &ContentSelector::slotShowContextMenu); - mContextMenu = new QMenu(ui.addonView); + mContextMenu = new QMenu(ui->addonView); mContextMenu->addAction(tr("&Check Selected"), this, SLOT(slotCheckMultiSelectedItems())); mContextMenu->addAction(tr("&Uncheck Selected"), this, SLOT(slotUncheckMultiSelectedItems())); mContextMenu->addAction(tr("&Copy Path(s) to Clipboard"), this, SLOT(slotCopySelectedItemsPaths())); @@ -116,7 +147,7 @@ void ContentSelectorView::ContentSelector::setGameFile(const QString& filename) if (!filename.isEmpty()) { const ContentSelectorModel::EsmFile* file = mContentModel->item(filename); - index = ui.gameFileView->findText(file->fileName()); + index = ui->gameFileView->findText(file->fileName()); // verify that the current index is also checked in the model if (!mContentModel->setCheckState(filename, true)) @@ -126,7 +157,7 @@ void ContentSelectorView::ContentSelector::setGameFile(const QString& filename) } } - ui.gameFileView->setCurrentIndex(index); + ui->gameFileView->setCurrentIndex(index); } void ContentSelectorView::ContentSelector::clearCheckStates() @@ -143,7 +174,7 @@ void ContentSelectorView::ContentSelector::setContentList(const QStringList& lis { if (list.isEmpty()) { - slotCurrentGameFileIndexChanged(ui.gameFileView->currentIndex()); + slotCurrentGameFileIndexChanged(ui->gameFileView->currentIndex()); } else mContentModel->setContentList(list); @@ -164,14 +195,14 @@ void ContentSelectorView::ContentSelector::addFiles(const QString& path, bool ne // add any game files to the combo box for (const QString& gameFileName : mContentModel->gameFiles()) { - if (ui.gameFileView->findText(gameFileName) == -1) + if (ui->gameFileView->findText(gameFileName) == -1) { - ui.gameFileView->addItem(gameFileName); + ui->gameFileView->addItem(gameFileName); } } - if (ui.gameFileView->currentIndex() != 0) - ui.gameFileView->setCurrentIndex(0); + if (ui->gameFileView->currentIndex() != 0) + ui->gameFileView->setCurrentIndex(0); mContentModel->uncheckAll(); mContentModel->checkForLoadOrderErrors(); @@ -194,10 +225,10 @@ void ContentSelectorView::ContentSelector::clearFiles() QString ContentSelectorView::ContentSelector::currentFile() const { - QModelIndex currentIdx = ui.addonView->currentIndex(); + QModelIndex currentIdx = ui->addonView->currentIndex(); - if (!currentIdx.isValid() && ui.gameFileView->currentIndex() > 0) - return ui.gameFileView->currentText(); + if (!currentIdx.isValid() && ui->gameFileView->currentIndex() > 0) + return ui->gameFileView->currentText(); QModelIndex idx = mContentModel->index(mAddonProxyModel->mapToSource(currentIdx).row(), 0, QModelIndex()); return mContentModel->data(idx, Qt::DisplayRole).toString(); @@ -225,7 +256,7 @@ void ContentSelectorView::ContentSelector::slotCurrentGameFileIndexChanged(int i void ContentSelectorView::ContentSelector::setGameFileSelected(int index, bool selected) { - QString fileName = ui.gameFileView->itemText(index); + QString fileName = ui->gameFileView->itemText(index); const ContentSelectorModel::EsmFile* file = mContentModel->item(fileName); if (file != nullptr) { @@ -253,14 +284,14 @@ void ContentSelectorView::ContentSelector::slotAddonTableItemActivated(const QMo void ContentSelectorView::ContentSelector::slotShowContextMenu(const QPoint& pos) { - QPoint globalPos = ui.addonView->viewport()->mapToGlobal(pos); + QPoint globalPos = ui->addonView->viewport()->mapToGlobal(pos); mContextMenu->exec(globalPos); } void ContentSelectorView::ContentSelector::setCheckStateForMultiSelectedItems(bool checked) { Qt::CheckState checkState = checked ? Qt::Checked : Qt::Unchecked; - for (const QModelIndex& index : ui.addonView->selectionModel()->selectedIndexes()) + for (const QModelIndex& index : ui->addonView->selectionModel()->selectedIndexes()) { QModelIndex sourceIndex = mAddonProxyModel->mapToSource(index); if (mContentModel->data(sourceIndex, Qt::CheckStateRole).toInt() != checkState) @@ -284,7 +315,7 @@ void ContentSelectorView::ContentSelector::slotCopySelectedItemsPaths() { QClipboard* clipboard = QApplication::clipboard(); QString filepaths; - for (const QModelIndex& index : ui.addonView->selectionModel()->selectedIndexes()) + for (const QModelIndex& index : ui->addonView->selectionModel()->selectedIndexes()) { int row = mAddonProxyModel->mapToSource(index).row(); const ContentSelectorModel::EsmFile* file = mContentModel->item(row); @@ -299,5 +330,10 @@ void ContentSelectorView::ContentSelector::slotCopySelectedItemsPaths() void ContentSelectorView::ContentSelector::slotSearchFilterTextChanged(const QString& newText) { - ui.addonView->setDragEnabled(newText.isEmpty()); + ui->addonView->setDragEnabled(newText.isEmpty()); } + +void ContentSelectorView::ContentSelector::slotRowsMoved() +{ + ui->addonView->selectionModel()->clearSelection(); +} \ No newline at end of file diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp index 5919d2e516..2b739645ba 100644 --- a/components/contentselector/view/contentselector.hpp +++ b/components/contentselector/view/contentselector.hpp @@ -1,13 +1,22 @@ #ifndef CONTENTSELECTOR_HPP #define CONTENTSELECTOR_HPP -#include +#include + +#include +#include +#include +#include -#include "ui_contentselector.h" #include class QSortFilterProxyModel; +namespace Ui +{ + class ContentSelector; +} + namespace ContentSelectorView { class ContentSelector : public QObject @@ -23,6 +32,8 @@ namespace ContentSelectorView public: explicit ContentSelector(QWidget* parent = nullptr, bool showOMWScripts = false); + ~ContentSelector() override; + QString currentFile() const; void addFiles(const QString& path, bool newfiles = false); @@ -39,18 +50,18 @@ namespace ContentSelectorView void setGameFile(const QString& filename = QString("")); - bool isGamefileSelected() const { return ui.gameFileView->currentIndex() > 0; } + bool isGamefileSelected() const; - QWidget* uiWidget() const { return ui.contentGroupBox; } + QWidget* uiWidget() const; - QComboBox* languageBox() const { return ui.languageComboBox; } + QComboBox* languageBox() const; - QToolButton* refreshButton() const { return ui.refreshButton; } + QToolButton* refreshButton() const; - QLineEdit* searchFilter() const { return ui.searchFilter; } + QLineEdit* searchFilter() const; private: - Ui::ContentSelector ui; + std::unique_ptr ui; void buildContentModel(bool showOMWScripts); void buildGameFileView(); @@ -74,6 +85,7 @@ namespace ContentSelectorView void slotUncheckMultiSelectedItems(); void slotCopySelectedItemsPaths(); void slotSearchFilterTextChanged(const QString& newText); + void slotRowsMoved(); }; } diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index 3a1b6fd77a..980281240d 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -180,8 +180,8 @@ namespace DetourNavigator if (!playerTileChanged && changedTiles.empty()) return; - const dtNavMeshParams params = *navMeshCacheItem->lockConst()->getImpl().getParams(); - const int maxTiles = std::min(mSettings.get().mMaxTilesNumber, params.maxTiles); + const int maxTiles + = std::min(mSettings.get().mMaxTilesNumber, navMeshCacheItem->lockConst()->getImpl().getParams()->maxTiles); std::unique_lock lock(mMutex); @@ -376,9 +376,10 @@ namespace DetourNavigator return JobStatus::Done; const auto playerTile = *mPlayerTile.lockConst(); - const auto params = *navMeshCacheItem->lockConst()->getImpl().getParams(); + const int maxTiles + = std::min(mSettings.get().mMaxTilesNumber, navMeshCacheItem->lockConst()->getImpl().getParams()->maxTiles); - if (!shouldAddTile(job.mChangedTile, playerTile, std::min(mSettings.get().mMaxTilesNumber, params.maxTiles))) + if (!shouldAddTile(job.mChangedTile, playerTile, maxTiles)) { Log(Debug::Debug) << "Ignore add tile by job " << job.mId << ": too far from player"; navMeshCacheItem->lock()->removeTile(job.mChangedTile); diff --git a/components/detournavigator/recastglobalallocator.hpp b/components/detournavigator/recastglobalallocator.hpp index 956f050a72..f69805eb97 100644 --- a/components/detournavigator/recastglobalallocator.hpp +++ b/components/detournavigator/recastglobalallocator.hpp @@ -3,6 +3,7 @@ #include "recasttempallocator.hpp" +#include #include #include @@ -14,10 +15,14 @@ namespace DetourNavigator public: static void init() { instance(); } - static void* alloc(size_t size, rcAllocHint hint) + static void* recastAlloc(size_t size, rcAllocHint hint) { return alloc(size, hint == RC_ALLOC_TEMP); } + + static void* detourAlloc(size_t size, dtAllocHint hint) { return alloc(size, hint == DT_ALLOC_TEMP); } + + static void* alloc(size_t size, bool temp) { void* result = nullptr; - if (rcLikely(hint == RC_ALLOC_TEMP)) + if (rcLikely(temp)) result = tempAllocator().alloc(size); if (rcUnlikely(!result)) result = allocPerm(size); @@ -38,7 +43,11 @@ namespace DetourNavigator } private: - RecastGlobalAllocator() { rcAllocSetCustom(&RecastGlobalAllocator::alloc, &RecastGlobalAllocator::free); } + RecastGlobalAllocator() + { + rcAllocSetCustom(&RecastGlobalAllocator::recastAlloc, &RecastGlobalAllocator::free); + dtAllocSetCustom(&RecastGlobalAllocator::detourAlloc, &RecastGlobalAllocator::free); + } static RecastGlobalAllocator& instance() { diff --git a/components/detournavigator/recastmeshbuilder.cpp b/components/detournavigator/recastmeshbuilder.cpp index 1684e3e542..6c15b441e7 100644 --- a/components/detournavigator/recastmeshbuilder.cpp +++ b/components/detournavigator/recastmeshbuilder.cpp @@ -189,42 +189,18 @@ namespace DetourNavigator void RecastMeshBuilder::addObject(const btBoxShape& shape, const btTransform& transform, const AreaType areaType) { constexpr std::array indices{ { - 0, - 2, - 3, - 3, - 1, - 0, - 0, - 4, - 6, - 6, - 2, - 0, - 0, - 1, - 5, - 5, - 4, - 0, - 7, - 5, - 1, - 1, - 3, - 7, - 7, - 3, - 2, - 2, - 6, - 7, - 7, - 6, - 4, - 4, - 5, - 7, + 0, 2, 3, // triangle 0 + 3, 1, 0, // triangle 1 + 0, 4, 6, // triangle 2 + 6, 2, 0, // triangle 3 + 0, 1, 5, // triangle 4 + 5, 4, 0, // triangle 5 + 7, 5, 1, // triangle 6 + 1, 3, 7, // triangle 7 + 7, 3, 2, // triangle 8 + 2, 6, 7, // triangle 9 + 7, 6, 4, // triangle 10 + 4, 5, 7, // triangle 11 } }; for (std::size_t i = 0; i < indices.size(); i += 3) diff --git a/components/detournavigator/recasttempallocator.hpp b/components/detournavigator/recasttempallocator.hpp index bb23dc8494..08db5f9fca 100644 --- a/components/detournavigator/recasttempallocator.hpp +++ b/components/detournavigator/recasttempallocator.hpp @@ -52,7 +52,6 @@ namespace DetourNavigator mTop = mPrev; mPrev = getTempPtrPrev(mTop); } - return; } private: diff --git a/components/detournavigator/settingsutils.hpp b/components/detournavigator/settingsutils.hpp index 10e99d002f..c9d8665d51 100644 --- a/components/detournavigator/settingsutils.hpp +++ b/components/detournavigator/settingsutils.hpp @@ -47,6 +47,7 @@ namespace DetourNavigator return position; } + // Returns value in NavMesh coordinates inline float getTileSize(const RecastSettings& settings) { return static_cast(settings.mTileSize) * settings.mCellSize; @@ -62,16 +63,19 @@ namespace DetourNavigator return static_cast(v); } + // Returns integer tile position for position in navmesh coordinates inline TilePosition getTilePosition(const RecastSettings& settings, const osg::Vec2f& position) { return TilePosition(getTilePosition(settings, position.x()), getTilePosition(settings, position.y())); } + // Returns integer tile position for position in navmesh coordinates inline TilePosition getTilePosition(const RecastSettings& settings, const osg::Vec3f& position) { return getTilePosition(settings, osg::Vec2f(position.x(), position.z())); } + // Returns tile bounds in navmesh coordinates inline TileBounds makeTileBounds(const RecastSettings& settings, const TilePosition& tilePosition) { return TileBounds{ @@ -80,6 +84,7 @@ namespace DetourNavigator }; } + // Returns border size relative to cell size inline float getBorderSize(const RecastSettings& settings) { return static_cast(settings.mBorderSize) * settings.mCellSize; @@ -95,6 +100,7 @@ namespace DetourNavigator return std::floor(std::sqrt(settings.mMaxTilesNumber / osg::PI)) - 1; } + // Returns tile bounds in real coordinates inline TileBounds makeRealTileBoundsWithBorder(const RecastSettings& settings, const TilePosition& tilePosition) { TileBounds result = makeTileBounds(settings, tilePosition); diff --git a/components/esm/defs.hpp b/components/esm/defs.hpp index 96d70f6fea..cbc70582c0 100644 --- a/components/esm/defs.hpp +++ b/components/esm/defs.hpp @@ -18,9 +18,9 @@ namespace ESM struct EpochTimeStamp { float mGameHour; - int mDay; - int mMonth; - int mYear; + int32_t mDay; + int32_t mMonth; + int32_t mYear; }; // Pixel color value. Standard four-byte rr,gg,bb,aa format. @@ -146,8 +146,6 @@ namespace ESM // format 0 - saved games REC_SAVE = esm3Recname("SAVE"), - REC_JOUR_LEGACY = esm3Recname("\xa4UOR"), // "\xa4UOR", rather than "JOUR", little oversight when magic numbers - // were calculated by hand, needs to be supported for older files now REC_JOUR = esm3Recname("JOUR"), REC_QUES = esm3Recname("QUES"), REC_GSCR = esm3Recname("GSCR"), @@ -172,6 +170,8 @@ namespace ESM // format 1 REC_FILT = esm3Recname("FILT"), REC_DBGP = esm3Recname("DBGP"), ///< only used in project files + REC_SELG = esm3Recname("SELG"), + REC_LUAL = esm3Recname("LUAL"), // LuaScriptsCfg (only in omwgame or omwaddon) // format 16 - Lua scripts in saved games diff --git a/components/esm/esmcommon.hpp b/components/esm/esmcommon.hpp index e92ae06806..69b877c9c9 100644 --- a/components/esm/esmcommon.hpp +++ b/components/esm/esmcommon.hpp @@ -13,12 +13,6 @@ namespace ESM { - enum Version - { - VER_12 = 0x3f99999a, - VER_13 = 0x3fa66666 - }; - enum RecordFlag { // This flag exists, but is not used to determine if a record has been deleted while loading diff --git a/components/esm3/aisequence.cpp b/components/esm3/aisequence.cpp index 21973acd1d..99c85db1bb 100644 --- a/components/esm3/aisequence.cpp +++ b/components/esm3/aisequence.cpp @@ -15,12 +15,7 @@ namespace ESM { esm.getHNT("DATA", mData.mDistance, mData.mDuration, mData.mTimeOfDay, mData.mIdle, mData.mShouldRepeat); esm.getHNT("STAR", mDurationData.mRemainingDuration, mDurationData.unused); // was mStartTime - mStoredInitialActorPosition = false; - if (esm.isNextSub("POS_")) - { - mStoredInitialActorPosition = true; - esm.getHT(mInitialActorPosition.mValues); - } + mStoredInitialActorPosition = esm.getHNOT("POS_", mInitialActorPosition.mValues); } void AiWander::save(ESMWriter& esm) const @@ -34,7 +29,7 @@ namespace ESM void AiTravel::load(ESMReader& esm) { esm.getHNT("DATA", mData.mX, mData.mY, mData.mZ); - esm.getHNOT(mHidden, "HIDD"); + esm.getHNT(mHidden, "HIDD"); mRepeat = false; esm.getHNOT(mRepeat, "REPT"); } @@ -263,7 +258,7 @@ namespace ESM } } - esm.getHNOT(mLastAiPackage, "LAST"); + esm.getHNT(mLastAiPackage, "LAST"); if (count > 1 && esm.getFormatVersion() <= MaxOldAiPackageFormatVersion) { diff --git a/components/esm3/animationstate.cpp b/components/esm3/animationstate.cpp index 14edf01a83..79dcad7578 100644 --- a/components/esm3/animationstate.cpp +++ b/components/esm3/animationstate.cpp @@ -21,18 +21,7 @@ namespace ESM anim.mGroup = esm.getHString(); esm.getHNOT(anim.mTime, "TIME"); esm.getHNOT(anim.mAbsolute, "ABST"); - - esm.getSubNameIs("COUN"); - // workaround bug in earlier version where size_t was used - esm.getSubHeader(); - if (esm.getSubSize() == 8) - esm.getT(anim.mLoopCount); - else - { - uint32_t loopcount; - esm.getT(loopcount); - anim.mLoopCount = (uint64_t)loopcount; - } + esm.getHNT(anim.mLoopCount, "COUN"); mScriptedAnims.push_back(anim); } diff --git a/components/esm3/cellid.cpp b/components/esm3/cellid.cpp index 3df1336c6c..9a5be3aada 100644 --- a/components/esm3/cellid.cpp +++ b/components/esm3/cellid.cpp @@ -11,17 +11,9 @@ namespace ESM { mWorldspace = esm.getHNString("SPAC"); - if (esm.isNextSub("CIDX")) - { - esm.getHT(mIndex.mX, mIndex.mY); - mPaged = true; - } - else - { - mPaged = false; - mIndex.mX = 0; - mIndex.mY = 0; - } + mIndex.mX = 0; + mIndex.mY = 0; + mPaged = esm.getHNOT("CIDX", mIndex.mX, mIndex.mY); } void CellId::save(ESMWriter& esm) const diff --git a/components/esm3/cellref.cpp b/components/esm3/cellref.cpp index c4c2fca986..93a2ece669 100644 --- a/components/esm3/cellref.cpp +++ b/components/esm3/cellref.cpp @@ -107,7 +107,7 @@ namespace ESM getHTOrSkip(cellRef.mChargeInt); break; case fourCC("NAM9"): - getHTOrSkip(cellRef.mGoldValue); + getHTOrSkip(cellRef.mCount); break; case fourCC("DODT"): if constexpr (load) @@ -116,7 +116,7 @@ namespace ESM cellRef.mTeleport = true; } else - esm.skipHTSized<24, ESM::Position>(); + esm.skipHSub(); break; case fourCC("DNAM"): getHStringOrSkip(cellRef.mDestCell); @@ -134,7 +134,7 @@ namespace ESM if constexpr (load) esm.getHT(cellRef.mPos.pos, cellRef.mPos.rot); else - esm.skipHTSized<24, decltype(cellRef.mPos)>(); + esm.skipHSub(); break; case fourCC("NAM0"): { @@ -219,8 +219,8 @@ namespace ESM if (mChargeInt != -1) esm.writeHNT("INTV", mChargeInt); - if (mGoldValue > 1) - esm.writeHNT("NAM9", mGoldValue); + if (mCount != 1) + esm.writeHNT("NAM9", mCount); if (!inInventory && mTeleport) { @@ -259,7 +259,7 @@ namespace ESM mChargeInt = -1; mChargeIntRemainder = 0.0f; mEnchantmentCharge = -1; - mGoldValue = 1; + mCount = 1; mDestCell.clear(); mLockLevel = 0; mIsLocked = false; diff --git a/components/esm3/cellref.hpp b/components/esm3/cellref.hpp index 55e5afcbf5..84b6ae1d18 100644 --- a/components/esm3/cellref.hpp +++ b/components/esm3/cellref.hpp @@ -65,8 +65,7 @@ namespace ESM // Remaining enchantment charge. This could be -1 if the charge was not touched yet (i.e. full). float mEnchantmentCharge; - // This is 5 for Gold_005 references, 100 for Gold_100 and so on. - int32_t mGoldValue; + int32_t mCount; // For doors - true if this door teleports to somewhere else, false // if it should open through animation. diff --git a/components/esm3/creaturestats.cpp b/components/esm3/creaturestats.cpp index d8fb0d6969..44c3bd993b 100644 --- a/components/esm3/creaturestats.cpp +++ b/components/esm3/creaturestats.cpp @@ -38,23 +38,15 @@ namespace ESM mHitRecovery = false; mBlock = false; mRecalcDynamicStats = false; - if (esm.getFormatVersion() <= MaxWerewolfDeprecatedDataFormatVersion) + if (esm.getFormatVersion() <= MaxUnoptimizedCharacterDataFormatVersion) { esm.getHNOT(mDead, "DEAD"); esm.getHNOT(mDeathAnimationFinished, "DFNT"); - if (esm.getFormatVersion() <= MaxOldDeathAnimationFormatVersion && mDead) - mDeathAnimationFinished = true; esm.getHNOT(mDied, "DIED"); esm.getHNOT(mMurdered, "MURD"); - if (esm.isNextSub("FRHT")) - esm.skipHSub(); // Friendly hits, no longer used esm.getHNOT(mTalkedTo, "TALK"); esm.getHNOT(mAlarmed, "ALRM"); esm.getHNOT(mAttacked, "ATKD"); - if (esm.isNextSub("HOST")) - esm.skipHSub(); // Hostile, no longer used - if (esm.isNextSub("ATCK")) - esm.skipHSub(); // attackingOrSpell, no longer used esm.getHNOT(mKnockdown, "KNCK"); esm.getHNOT(mKnockdownOneFrame, "KNC1"); esm.getHNOT(mKnockdownOverOneFrame, "KNCO"); @@ -82,9 +74,6 @@ namespace ESM mMovementFlags = 0; esm.getHNOT(mMovementFlags, "MOVE"); - if (esm.isNextSub("ASTR")) - esm.skipHSub(); // attackStrength, no longer used - mFallHeight = 0; esm.getHNOT(mFallHeight, "FALL"); @@ -92,7 +81,7 @@ namespace ESM mLastHitAttemptObject = esm.getHNORefId("LHAT"); - if (esm.getFormatVersion() <= MaxWerewolfDeprecatedDataFormatVersion) + if (esm.getFormatVersion() <= MaxUnoptimizedCharacterDataFormatVersion) esm.getHNOT(mRecalcDynamicStats, "CALC"); mDrawState = 0; diff --git a/components/esm3/dialoguestate.cpp b/components/esm3/dialoguestate.cpp index 88fbe9659a..7095e096cb 100644 --- a/components/esm3/dialoguestate.cpp +++ b/components/esm3/dialoguestate.cpp @@ -22,14 +22,6 @@ namespace ESM esm.getHNT(reaction, "INTV"); mChangedFactionReaction[faction][faction2] = reaction; } - - // no longer used - while (esm.isNextSub("REAC")) - { - esm.skipHSub(); - esm.getSubName(); - esm.skipHSub(); - } } } diff --git a/components/esm3/esmreader.cpp b/components/esm3/esmreader.cpp index 77468f22d5..92a04fb487 100644 --- a/components/esm3/esmreader.cpp +++ b/components/esm3/esmreader.cpp @@ -263,7 +263,7 @@ namespace ESM { FormId res; if (wide) - getHNTSized<8>(res, tag); + getHNT(tag, res.mIndex, res.mContentFile); else getHNT(res.mIndex, tag); return res; @@ -496,7 +496,8 @@ namespace ESM case RefIdType::FormId: { FormId formId{}; - getTSized<8>(formId); + getT(formId.mIndex); + getT(formId.mContentFile); if (applyContentFileMapping(formId)) return RefId(formId); else diff --git a/components/esm3/esmreader.hpp b/components/esm3/esmreader.hpp index 63db1634fc..461f154001 100644 --- a/components/esm3/esmreader.hpp +++ b/components/esm3/esmreader.hpp @@ -53,9 +53,9 @@ namespace ESM * *************************************************************************/ - int getVer() const { return mHeader.mData.version; } + int getVer() const { return mHeader.mData.version.ui; } int getRecordCount() const { return mHeader.mData.records; } - float getFVer() const { return (mHeader.mData.version == VER_12) ? 1.2f : 1.3f; } + float esmVersionF() const { return (mHeader.mData.version.f); } const std::string& getAuthor() const { return mHeader.mData.author; } const std::string& getDesc() const { return mHeader.mData.desc; } const std::vector& getGameFiles() const { return mHeader.mMaster; } @@ -131,10 +131,10 @@ namespace ESM ESM::RefId getCellId(); // Read data of a given type, stored in a subrecord of a given name - template >> + template void getHNT(X& x, NAME name) { - getHNTSized(x, name); + getHNT(name, x); } template @@ -149,26 +149,21 @@ namespace ESM } // Optional version of getHNT - template >> + template void getHNOT(X& x, NAME name) { - getHNOTSized(x, name); + getHNOT(name, x); } - // Version with extra size checking, to make sure the compiler - // doesn't mess up our struct padding. - template - void getHNTSized(X& x, NAME name) - { - getSubNameIs(name); - getHTSized(x); - } - - template - void getHNOTSized(X& x, NAME name) + template + bool getHNOT(NAME name, Args&... args) { if (isNextSub(name)) - getHTSized(x); + { + getHT(args...); + return true; + } + return false; } // Get data of a given type/size, including subrecord header @@ -185,37 +180,13 @@ namespace ESM template >> void skipHT() { - skipHTSized(); - } - - // Version with extra size checking, to make sure the compiler - // doesn't mess up our struct padding. - template - void getHTSized(X& x) - { - getSubHeader(); - if (mCtx.leftSub != size) - reportSubSizeMismatch(size, mCtx.leftSub); - getTSized(x); - } - - template - void skipHTSized() - { - static_assert(sizeof(T) == size); + constexpr size_t size = sizeof(T); getSubHeader(); if (mCtx.leftSub != size) reportSubSizeMismatch(size, mCtx.leftSub); skip(size); } - template - void getTSized(X& x) - { - static_assert(sizeof(X) == size); - getExact(&x, size); - } - // Read a string by the given name if it is the next record. std::string getHNOString(NAME name); @@ -333,7 +304,7 @@ namespace ESM mEsm->read(static_cast(x), static_cast(size)); } - void getName(NAME& name) { getTSized<4>(name); } + void getName(NAME& name) { getT(name.mData); } void getUint(uint32_t& u) { getT(u); } std::string getMaybeFixedStringSize(std::size_t size); diff --git a/components/esm3/esmwriter.cpp b/components/esm3/esmwriter.cpp index 66788aa924..ad64ced0a4 100644 --- a/components/esm3/esmwriter.cpp +++ b/components/esm3/esmwriter.cpp @@ -84,12 +84,12 @@ namespace ESM unsigned int ESMWriter::getVersion() const { - return mHeader.mData.version; + return mHeader.mData.version.ui; } void ESMWriter::setVersion(unsigned int ver) { - mHeader.mData.version = ver; + mHeader.mData.version.ui = ver; } void ESMWriter::setType(int type) diff --git a/components/esm3/fogstate.cpp b/components/esm3/fogstate.cpp index 3ee4600c90..2c07438070 100644 --- a/components/esm3/fogstate.cpp +++ b/components/esm3/fogstate.cpp @@ -74,7 +74,7 @@ namespace ESM tex.mImageData.resize(imageSize); esm.getExact(tex.mImageData.data(), imageSize); - if (dataFormat <= MaxOldForOfWarFormatVersion) + if (dataFormat <= MaxOldFogOfWarFormatVersion) convertFogOfWar(tex.mImageData); mFogTextures.push_back(tex); diff --git a/components/esm3/formatversion.hpp b/components/esm3/formatversion.hpp index 12a73fc12b..9f499a7231 100644 --- a/components/esm3/formatversion.hpp +++ b/components/esm3/formatversion.hpp @@ -9,12 +9,11 @@ namespace ESM inline constexpr FormatVersion DefaultFormatVersion = 0; inline constexpr FormatVersion CurrentContentFormatVersion = 1; - inline constexpr FormatVersion MaxOldWeatherFormatVersion = 1; - inline constexpr FormatVersion MaxOldDeathAnimationFormatVersion = 2; - inline constexpr FormatVersion MaxOldForOfWarFormatVersion = 6; - inline constexpr FormatVersion MaxWerewolfDeprecatedDataFormatVersion = 7; + inline constexpr FormatVersion MaxOldFogOfWarFormatVersion = 6; + inline constexpr FormatVersion MaxUnoptimizedCharacterDataFormatVersion = 7; inline constexpr FormatVersion MaxOldTimeLeftFormatVersion = 8; inline constexpr FormatVersion MaxIntFallbackFormatVersion = 10; + inline constexpr FormatVersion MaxOldRestockingFormatVersion = 14; inline constexpr FormatVersion MaxClearModifiersFormatVersion = 16; inline constexpr FormatVersion MaxOldAiPackageFormatVersion = 17; inline constexpr FormatVersion MaxOldSkillsAndAttributesFormatVersion = 18; @@ -25,7 +24,12 @@ namespace ESM inline constexpr FormatVersion MaxNameIsRefIdOnlyFormatVersion = 25; inline constexpr FormatVersion MaxUseEsmCellIdFormatVersion = 26; inline constexpr FormatVersion MaxActiveSpellSlotIndexFormatVersion = 27; - inline constexpr FormatVersion CurrentSaveGameFormatVersion = 29; + inline constexpr FormatVersion MaxOldCountFormatVersion = 30; + inline constexpr FormatVersion CurrentSaveGameFormatVersion = 31; + + inline constexpr FormatVersion MinSupportedSaveGameFormatVersion = 5; + inline constexpr FormatVersion OpenMW0_48SaveGameFormatVersion = 21; + inline constexpr FormatVersion OpenMW0_49SaveGameFormatVersion = CurrentSaveGameFormatVersion; } #endif diff --git a/components/esm3/inventorystate.cpp b/components/esm3/inventorystate.cpp index 84a52ff518..1947be23e9 100644 --- a/components/esm3/inventorystate.cpp +++ b/components/esm3/inventorystate.cpp @@ -15,30 +15,19 @@ namespace ESM void InventoryState::load(ESMReader& esm) { // obsolete - uint32_t index = 0; while (esm.isNextSub("IOBJ")) { esm.skipHT(); ObjectState state; - // obsolete - if (esm.isNextSub("SLOT")) - { - int32_t slot; - esm.getHT(slot); - mEquipmentSlots[index] = slot; - } - state.mRef.loadId(esm, true); state.load(esm); - if (state.mCount == 0) + if (state.mRef.mCount == 0) continue; mItems.push_back(state); - - ++index; } uint32_t itemsCount = 0; @@ -54,26 +43,23 @@ namespace ESM if (!esm.applyContentFileMapping(state.mRef.mRefNum)) state.mRef.mRefNum = FormId(); // content file removed; unset refnum, but keep object. - if (state.mCount == 0) + if (state.mRef.mCount == 0) continue; mItems.push_back(state); } + std::map, int32_t> levelledItemMap; // Next item is Levelled item while (esm.isNextSub("LEVM")) { // Get its name ESM::RefId id = esm.getRefId(); int32_t count; - std::string parentGroup; // Then get its count esm.getHNT(count, "COUN"); - // Old save formats don't have information about parent group; check for that - if (esm.isNextSub("LGRP")) - // Newest saves contain parent group - parentGroup = esm.getHString(); - mLevelledItemMap[std::make_pair(id, parentGroup)] = count; + std::string parentGroup = esm.getHNString("LGRP"); + levelledItemMap[std::make_pair(id, parentGroup)] = count; } while (esm.isNextSub("MAGI")) @@ -125,14 +111,14 @@ namespace ESM // Old saves had restocking levelled items in a special map // This turns items from that map into negative quantities - for (const auto& entry : mLevelledItemMap) + for (const auto& entry : levelledItemMap) { const ESM::RefId& id = entry.first.first; const int count = entry.second; for (auto& item : mItems) { - if (item.mCount == count && id == item.mRef.mRefID) - item.mCount = -count; + if (item.mRef.mCount == count && id == item.mRef.mRefID) + item.mRef.mCount = -count; } } } @@ -149,13 +135,6 @@ namespace ESM } } - for (auto it = mLevelledItemMap.begin(); it != mLevelledItemMap.end(); ++it) - { - esm.writeHNRefId("LEVM", it->first.first); - esm.writeHNT("COUN", it->second); - esm.writeHNString("LGRP", it->first.second); - } - for (const auto& [id, params] : mPermanentMagicEffectMagnitudes) { esm.writeHNRefId("MAGI", id); diff --git a/components/esm3/inventorystate.hpp b/components/esm3/inventorystate.hpp index 050d1eb92f..814236ce46 100644 --- a/components/esm3/inventorystate.hpp +++ b/components/esm3/inventorystate.hpp @@ -22,8 +22,6 @@ namespace ESM // std::map mEquipmentSlots; - std::map, int32_t> mLevelledItemMap; - std::map>> mPermanentMagicEffectMagnitudes; std::optional mSelectedEnchantItem; // For inventories only diff --git a/components/esm3/loadfact.cpp b/components/esm3/loadfact.cpp index 9d4b832f97..1dd1fe1a0e 100644 --- a/components/esm3/loadfact.cpp +++ b/components/esm3/loadfact.cpp @@ -17,6 +17,47 @@ namespace ESM return mSkills.at(index); } + void RankData::load(ESMReader& esm) + { + esm.getT(mAttribute1); + esm.getT(mAttribute2); + esm.getT(mPrimarySkill); + esm.getT(mFavouredSkill); + esm.getT(mFactReaction); + } + + void RankData::save(ESMWriter& esm) const + { + esm.writeT(mAttribute1); + esm.writeT(mAttribute2); + esm.writeT(mPrimarySkill); + esm.writeT(mFavouredSkill); + esm.writeT(mFactReaction); + } + + void Faction::FADTstruct::load(ESMReader& esm) + { + esm.getSubHeader(); + esm.getT(mAttribute); + for (auto& rank : mRankData) + rank.load(esm); + esm.getT(mSkills); + esm.getT(mIsHidden); + if (mIsHidden > 1) + esm.fail("Unknown flag!"); + } + + void Faction::FADTstruct::save(ESMWriter& esm) const + { + esm.startSubRecord("FADT"); + esm.writeT(mAttribute); + for (const auto& rank : mRankData) + rank.save(esm); + esm.writeT(mSkills); + esm.writeT(mIsHidden); + esm.endRecord("FADT"); + } + void Faction::load(ESMReader& esm, bool& isDeleted) { isDeleted = false; @@ -47,9 +88,7 @@ namespace ESM mRanks[rankCounter++] = esm.getHString(); break; case fourCC("FADT"): - esm.getHTSized<240>(mData); - if (mData.mIsHidden > 1) - esm.fail("Unknown flag!"); + mData.load(esm); hasData = true; break; case fourCC("ANAM"): @@ -101,7 +140,7 @@ namespace ESM esm.writeHNString("RNAM", rank, 32); } - esm.writeHNT("FADT", mData, 240); + mData.save(esm); for (auto it = mReactions.begin(); it != mReactions.end(); ++it) { diff --git a/components/esm3/loadfact.hpp b/components/esm3/loadfact.hpp index 2359d276a2..eef2126514 100644 --- a/components/esm3/loadfact.hpp +++ b/components/esm3/loadfact.hpp @@ -30,6 +30,9 @@ namespace ESM int32_t mPrimarySkill, mFavouredSkill; int32_t mFactReaction; // Reaction from faction members + + void load(ESMReader& esm); + void save(ESMWriter& esm) const; }; struct Faction @@ -60,6 +63,9 @@ namespace ESM int32_t getSkill(size_t index, bool ignored = false) const; ///< Throws an exception for invalid values of \a index. + + void load(ESMReader& esm); + void save(ESMWriter& esm) const; }; // 240 bytes FADTstruct mData; diff --git a/components/esm3/loadnpc.cpp b/components/esm3/loadnpc.cpp index d844f7d2bc..4a30649372 100644 --- a/components/esm3/loadnpc.cpp +++ b/components/esm3/loadnpc.cpp @@ -59,23 +59,31 @@ namespace ESM if (esm.getSubSize() == 52) { mNpdtType = NPC_DEFAULT; - esm.getExact(&mNpdt, 52); + esm.getT(mNpdt.mLevel); + esm.getT(mNpdt.mAttributes); + esm.getT(mNpdt.mSkills); + esm.getT(mNpdt.mUnknown1); + esm.getT(mNpdt.mHealth); + esm.getT(mNpdt.mMana); + esm.getT(mNpdt.mFatigue); + esm.getT(mNpdt.mDisposition); + esm.getT(mNpdt.mReputation); + esm.getT(mNpdt.mRank); + esm.getT(mNpdt.mUnknown2); + esm.getT(mNpdt.mGold); } else if (esm.getSubSize() == 12) { - // Reading into temporary NPDTstruct12 object - NPDTstruct12 npdt12; mNpdtType = NPC_WITH_AUTOCALCULATED_STATS; - esm.getExact(&npdt12, 12); // Clearing the mNdpt struct to initialize all values blankNpdt(); - // Swiching to an internal representation - mNpdt.mLevel = npdt12.mLevel; - mNpdt.mDisposition = npdt12.mDisposition; - mNpdt.mReputation = npdt12.mReputation; - mNpdt.mRank = npdt12.mRank; - mNpdt.mGold = npdt12.mGold; + esm.getT(mNpdt.mLevel); + esm.getT(mNpdt.mDisposition); + esm.getT(mNpdt.mReputation); + esm.getT(mNpdt.mRank); + esm.skip(3); + esm.getT(mNpdt.mGold); } else esm.fail("NPC_NPDT must be 12 or 52 bytes long"); @@ -146,20 +154,32 @@ namespace ESM if (mNpdtType == NPC_DEFAULT) { - esm.writeHNT("NPDT", mNpdt, 52); + esm.startSubRecord("NPDT"); + esm.writeT(mNpdt.mLevel); + esm.writeT(mNpdt.mAttributes); + esm.writeT(mNpdt.mSkills); + esm.writeT(mNpdt.mUnknown1); + esm.writeT(mNpdt.mHealth); + esm.writeT(mNpdt.mMana); + esm.writeT(mNpdt.mFatigue); + esm.writeT(mNpdt.mDisposition); + esm.writeT(mNpdt.mReputation); + esm.writeT(mNpdt.mRank); + esm.writeT(mNpdt.mUnknown2); + esm.writeT(mNpdt.mGold); + esm.endRecord("NPDT"); } else if (mNpdtType == NPC_WITH_AUTOCALCULATED_STATS) { - NPDTstruct12 npdt12; - npdt12.mLevel = mNpdt.mLevel; - npdt12.mDisposition = mNpdt.mDisposition; - npdt12.mReputation = mNpdt.mReputation; - npdt12.mRank = mNpdt.mRank; - npdt12.mUnknown1 = 0; - npdt12.mUnknown2 = 0; - npdt12.mUnknown3 = 0; - npdt12.mGold = mNpdt.mGold; - esm.writeHNT("NPDT", npdt12, 12); + esm.startSubRecord("NPDT"); + esm.writeT(mNpdt.mLevel); + esm.writeT(mNpdt.mDisposition); + esm.writeT(mNpdt.mReputation); + esm.writeT(mNpdt.mRank); + constexpr char padding[] = { 0, 0, 0 }; + esm.writeT(padding); + esm.writeT(mNpdt.mGold); + esm.endRecord("NPDT"); } esm.writeHNT("FLAG", ((mBloodType << 10) + mFlags)); @@ -213,8 +233,7 @@ namespace ESM void NPC::blankNpdt() { mNpdt.mLevel = 0; - mNpdt.mStrength = mNpdt.mIntelligence = mNpdt.mWillpower = mNpdt.mAgility = mNpdt.mSpeed = mNpdt.mEndurance - = mNpdt.mPersonality = mNpdt.mLuck = 0; + mNpdt.mAttributes.fill(0); mNpdt.mSkills.fill(0); mNpdt.mReputation = 0; mNpdt.mHealth = mNpdt.mMana = mNpdt.mFatigue = 0; diff --git a/components/esm3/loadnpc.hpp b/components/esm3/loadnpc.hpp index af8c2a8574..76930365c8 100644 --- a/components/esm3/loadnpc.hpp +++ b/components/esm3/loadnpc.hpp @@ -6,6 +6,7 @@ #include #include "aipackage.hpp" +#include "components/esm/attr.hpp" #include "components/esm/defs.hpp" #include "components/esm/refid.hpp" #include "loadcont.hpp" @@ -74,13 +75,10 @@ namespace ESM NPC_DEFAULT = 52 }; -#pragma pack(push) -#pragma pack(1) - struct NPDTstruct52 { int16_t mLevel; - unsigned char mStrength, mIntelligence, mWillpower, mAgility, mSpeed, mEndurance, mPersonality, mLuck; + std::array mAttributes; // mSkill can grow up to 200, it must be unsigned std::array mSkills; @@ -92,18 +90,6 @@ namespace ESM int32_t mGold; }; // 52 bytes - // Structure for autocalculated characters. - // This is only used for load and save operations. - struct NPDTstruct12 - { - int16_t mLevel; - // see above - unsigned char mDisposition, mReputation, mRank; - char mUnknown1, mUnknown2, mUnknown3; - int32_t mGold; - }; // 12 bytes -#pragma pack(pop) - unsigned char mNpdtType; // Worth noting when saving the struct: // Although we might read a NPDTstruct12 in, we use NPDTstruct52 internally diff --git a/components/esm3/loadpgrd.cpp b/components/esm3/loadpgrd.cpp index 8d60d25524..4f0a62a9d4 100644 --- a/components/esm3/loadpgrd.cpp +++ b/components/esm3/loadpgrd.cpp @@ -70,7 +70,12 @@ namespace ESM for (uint16_t i = 0; i < mData.mPoints; ++i) { Point p; - esm.getExact(&p, sizeof(Point)); + esm.getT(p.mX); + esm.getT(p.mY); + esm.getT(p.mZ); + esm.getT(p.mAutogenerated); + esm.getT(p.mConnectionNum); + esm.getT(p.mUnknown); mPoints.push_back(p); edgeCount += p.mConnectionNum; } diff --git a/components/esm3/loadrace.cpp b/components/esm3/loadrace.cpp index eb8faf40e9..0996a5ac48 100644 --- a/components/esm3/loadrace.cpp +++ b/components/esm3/loadrace.cpp @@ -3,16 +3,63 @@ #include "esmreader.hpp" #include "esmwriter.hpp" +#include + namespace ESM { - int Race::MaleFemale::getValue(bool male) const + int32_t Race::RADTstruct::getAttribute(ESM::RefId attribute, bool male) const { - return male ? mMale : mFemale; + int index = ESM::Attribute::refIdToIndex(attribute); + if (index < 0) + return 0; + index *= 2; + if (!male) + index++; + return mAttributeValues[static_cast(index)]; } - float Race::MaleFemaleF::getValue(bool male) const + void Race::RADTstruct::setAttribute(ESM::RefId attribute, bool male, int32_t value) { - return male ? mMale : mFemale; + int index = ESM::Attribute::refIdToIndex(attribute); + if (index < 0) + return; + index *= 2; + if (!male) + index++; + mAttributeValues[static_cast(index)] = value; + } + + void Race::RADTstruct::load(ESMReader& esm) + { + esm.getSubHeader(); + for (auto& bonus : mBonus) + { + esm.getT(bonus.mSkill); + esm.getT(bonus.mBonus); + } + esm.getT(mAttributeValues); + esm.getT(mMaleHeight); + esm.getT(mFemaleHeight); + esm.getT(mMaleWeight); + esm.getT(mFemaleWeight); + esm.getT(mFlags); + } + + void Race::RADTstruct::save(ESMWriter& esm) const + { + esm.startSubRecord("RADT"); + for (const auto& bonus : mBonus) + { + esm.writeT(bonus.mSkill); + esm.writeT(bonus.mBonus); + } + esm.writeT(mAttributeValues); + esm.writeT(mMaleHeight); + esm.writeT(mFemaleHeight); + esm.writeT(mMaleWeight); + esm.writeT(mFemaleWeight); + esm.writeT(mFlags); + esm.endRecord("RADT"); } void Race::load(ESMReader& esm, bool& isDeleted) @@ -37,7 +84,7 @@ namespace ESM mName = esm.getHString(); break; case fourCC("RADT"): - esm.getHTSized<140>(mData); + mData.load(esm); hasData = true; break; case fourCC("DESC"): @@ -71,7 +118,7 @@ namespace ESM } esm.writeHNOCString("FNAM", mName); - esm.writeHNT("RADT", mData, 140); + mData.save(esm); mPowers.save(esm); esm.writeHNOString("DESC", mDescription); } @@ -90,11 +137,10 @@ namespace ESM bonus.mBonus = 0; } - for (auto& attribute : mData.mAttributeValues) - attribute.mMale = attribute.mFemale = 1; + mData.mAttributeValues.fill(1); - mData.mHeight.mMale = mData.mHeight.mFemale = 1; - mData.mWeight.mMale = mData.mWeight.mFemale = 1; + mData.mMaleHeight = mData.mFemaleHeight = 1; + mData.mMaleWeight = mData.mFemaleWeight = 1; mData.mFlags = 0; } diff --git a/components/esm3/loadrace.hpp b/components/esm3/loadrace.hpp index 8cb9d76118..4493240ac8 100644 --- a/components/esm3/loadrace.hpp +++ b/components/esm3/loadrace.hpp @@ -31,20 +31,6 @@ namespace ESM int32_t mBonus; }; - struct MaleFemale - { - int32_t mMale, mFemale; - - int getValue(bool male) const; - }; - - struct MaleFemaleF - { - float mMale, mFemale; - - float getValue(bool male) const; - }; - enum Flags { Playable = 0x01, @@ -57,14 +43,20 @@ namespace ESM std::array mBonus; // Attribute values for male/female - std::array mAttributeValues; + std::array mAttributeValues; // The actual eye level height (in game units) is (probably) given // as 'height' times 128. This has not been tested yet. - MaleFemaleF mHeight, mWeight; + float mMaleHeight, mFemaleHeight, mMaleWeight, mFemaleWeight; int32_t mFlags; // 0x1 - playable, 0x2 - beast race + int32_t getAttribute(ESM::RefId attribute, bool male) const; + void setAttribute(ESM::RefId attribute, bool male, int32_t value); + + void load(ESMReader& esm); + void save(ESMWriter& esm) const; + }; // Size = 140 bytes RADTstruct mData; diff --git a/components/esm3/loadregn.cpp b/components/esm3/loadregn.cpp index 5148a446c2..63e785882e 100644 --- a/components/esm3/loadregn.cpp +++ b/components/esm3/loadregn.cpp @@ -2,6 +2,7 @@ #include "esmreader.hpp" #include "esmwriter.hpp" +#include namespace ESM { @@ -83,7 +84,7 @@ namespace ESM esm.writeHNOCString("FNAM", mName); - if (esm.getVersion() == VER_12) + if (esm.getVersion() == VER_120) esm.writeHNT("WEAT", mData.mProbabilities, mData.mProbabilities.size() - 2); else esm.writeHNT("WEAT", mData.mProbabilities); diff --git a/components/esm3/loadtes3.cpp b/components/esm3/loadtes3.cpp index 86b62234da..eeb2d38761 100644 --- a/components/esm3/loadtes3.cpp +++ b/components/esm3/loadtes3.cpp @@ -9,7 +9,7 @@ namespace ESM void Header::blank() { - mData.version = VER_13; + mData.version.ui = VER_130; mData.type = 0; mData.author.clear(); mData.desc.clear(); @@ -20,15 +20,13 @@ namespace ESM void Header::load(ESMReader& esm) { - if (esm.isNextSub("FORM")) - esm.getHT(mFormatVersion); - else - mFormatVersion = DefaultFormatVersion; + mFormatVersion = DefaultFormatVersion; + esm.getHNOT("FORM", mFormatVersion); if (esm.isNextSub("HEDR")) { esm.getSubHeader(); - esm.getT(mData.version); + esm.getT(mData.version.ui); esm.getT(mData.type); mData.author = esm.getMaybeFixedStringSize(32); mData.desc = esm.getMaybeFixedStringSize(256); @@ -43,10 +41,8 @@ namespace ESM mMaster.push_back(m); } - if (esm.isNextSub("GMDT")) - { - esm.getHTSized<124>(mGameData); - } + esm.getHNOT("GMDT", mGameData.mCurrentHealth, mGameData.mMaximumHealth, mGameData.mHour, mGameData.unknown1, + mGameData.mCurrentCell.mData, mGameData.unknown2, mGameData.mPlayerName.mData); if (esm.isNextSub("SCRD")) { esm.getSubHeader(); diff --git a/components/esm3/loadtes3.hpp b/components/esm3/loadtes3.hpp index 8b14d41645..54fd303485 100644 --- a/components/esm3/loadtes3.hpp +++ b/components/esm3/loadtes3.hpp @@ -3,6 +3,7 @@ #include +#include "components/esm/common.hpp" #include "components/esm/esmcommon.hpp" #include "components/esm3/formatversion.hpp" @@ -11,16 +12,9 @@ namespace ESM class ESMReader; class ESMWriter; -#pragma pack(push) -#pragma pack(1) - struct Data { - /* File format version. This is actually a float, the supported - versions are 1.2 and 1.3. These correspond to: - 1.2 = 0x3f99999a and 1.3 = 0x3fa66666 - */ - uint32_t version; + ESM::ESMVersion version; int32_t type; // 0=esp, 1=esm, 32=ess (unused) std::string author; // Author's name std::string desc; // File description @@ -38,8 +32,6 @@ namespace ESM NAME32 mPlayerName; }; -#pragma pack(pop) - /// \brief File header record struct Header { diff --git a/components/esm3/loadweap.hpp b/components/esm3/loadweap.hpp index e8355d0f55..ba1599b1df 100644 --- a/components/esm3/loadweap.hpp +++ b/components/esm3/loadweap.hpp @@ -24,6 +24,7 @@ namespace ESM enum Type { + First = -4, PickProbe = -4, HandToHand = -3, Spell = -2, @@ -41,7 +42,8 @@ namespace ESM MarksmanCrossbow = 10, MarksmanThrown = 11, Arrow = 12, - Bolt = 13 + Bolt = 13, + Last = 13 }; enum AttackType diff --git a/components/esm3/npcstats.cpp b/components/esm3/npcstats.cpp index a21ba807e4..371f506fb4 100644 --- a/components/esm3/npcstats.cpp +++ b/components/esm3/npcstats.cpp @@ -37,50 +37,13 @@ namespace ESM mDisposition = 0; esm.getHNOT(mDisposition, "DISP"); + mCrimeDispositionModifier = 0; + esm.getHNOT(mCrimeDispositionModifier, "DISM"); + const bool intFallback = esm.getFormatVersion() <= MaxIntFallbackFormatVersion; for (auto& skill : mSkills) skill.load(esm, intFallback); - mWerewolfDeprecatedData = false; - if (esm.getFormatVersion() <= MaxWerewolfDeprecatedDataFormatVersion && esm.peekNextSub("STBA")) - { - // we have deprecated werewolf skills, stored interleaved - // Load into one big vector, then remove every 2nd value - mWerewolfDeprecatedData = true; - std::vector> skills(mSkills.begin(), mSkills.end()); - - for (size_t i = 0; i < std::size(mSkills); ++i) - { - StatState skill; - skill.load(esm, intFallback); - skills.push_back(skill); - } - - int i = 0; - for (std::vector>::iterator it = skills.begin(); it != skills.end(); ++i) - { - if (i % 2 == 1) - it = skills.erase(it); - else - ++it; - } - if (skills.size() != std::size(mSkills)) - throw std::runtime_error( - "Invalid number of skill for werewolf deprecated data: " + std::to_string(skills.size())); - std::copy(skills.begin(), skills.end(), mSkills.begin()); - } - - // No longer used - bool hasWerewolfAttributes = false; - esm.getHNOT(hasWerewolfAttributes, "HWAT"); - if (hasWerewolfAttributes) - { - StatState dummy; - for (int i = 0; i < ESM::Attribute::Length; ++i) - dummy.load(esm, intFallback); - mWerewolfDeprecatedData = true; - } - mIsWerewolf = false; esm.getHNOT(mIsWerewolf, "WOLF"); @@ -93,14 +56,6 @@ namespace ESM mWerewolfKills = 0; esm.getHNOT(mWerewolfKills, "WKIL"); - // No longer used - if (esm.isNextSub("PROF")) - esm.skipHSub(); // int profit - - // No longer used - if (esm.isNextSub("ASTR")) - esm.skipHSub(); // attackStrength - mLevelProgress = 0; esm.getHNOT(mLevelProgress, "LPRO"); @@ -116,14 +71,6 @@ namespace ESM mTimeToStartDrowning = 0; esm.getHNOT(mTimeToStartDrowning, "DRTI"); - // No longer used - float lastDrowningHit = 0; - esm.getHNOT(lastDrowningHit, "DRLH"); - - // No longer used - float levelHealthBonus = 0; - esm.getHNOT(levelHealthBonus, "LVLH"); - mCrimeId = -1; esm.getHNOT(mCrimeId, "CRID"); } @@ -150,6 +97,9 @@ namespace ESM if (mDisposition) esm.writeHNT("DISP", mDisposition); + if (mCrimeDispositionModifier) + esm.writeHNT("DISM", mCrimeDispositionModifier); + for (const auto& skill : mSkills) skill.save(esm); @@ -195,9 +145,9 @@ namespace ESM void NpcStats::blank() { - mWerewolfDeprecatedData = false; mIsWerewolf = false; mDisposition = 0; + mCrimeDispositionModifier = 0; mBounty = 0; mReputation = 0; mWerewolfKills = 0; diff --git a/components/esm3/npcstats.hpp b/components/esm3/npcstats.hpp index ccb58a12ad..b539602e06 100644 --- a/components/esm3/npcstats.hpp +++ b/components/esm3/npcstats.hpp @@ -31,10 +31,9 @@ namespace ESM bool mIsWerewolf; - bool mWerewolfDeprecatedData; - std::map mFactions; int32_t mDisposition; + int32_t mCrimeDispositionModifier; std::array, ESM::Skill::Length> mSkills; int32_t mBounty; int32_t mReputation; diff --git a/components/esm3/objectstate.cpp b/components/esm3/objectstate.cpp index a7fe41d66c..7d26f431d6 100644 --- a/components/esm3/objectstate.cpp +++ b/components/esm3/objectstate.cpp @@ -29,29 +29,18 @@ namespace ESM mEnabled = 1; esm.getHNOT(mEnabled, "ENAB"); - mCount = 1; - esm.getHNOT(mCount, "COUN"); - - if (esm.isNextSub("POS_")) + if (mVersion <= MaxOldCountFormatVersion) { - std::array pos; - esm.getHT(pos); - memcpy(mPosition.pos, pos.data(), sizeof(float) * 3); - memcpy(mPosition.rot, pos.data() + 3, sizeof(float) * 3); + mRef.mCount = 1; + esm.getHNOT(mRef.mCount, "COUN"); } - else - mPosition = mRef.mPos; - if (esm.isNextSub("LROT")) - esm.skipHSub(); // local rotation, no longer used + mPosition = mRef.mPos; + esm.getHNOT("POS_", mPosition.pos, mPosition.rot); mFlags = 0; esm.getHNOT(mFlags, "FLAG"); - // obsolete - int32_t unused; - esm.getHNOT(unused, "LTIM"); - mAnimationState.load(esm); // FIXME: assuming "false" as default would make more sense, but also break compatibility with older save files @@ -74,9 +63,6 @@ namespace ESM if (!mEnabled && !inInventory) esm.writeHNT("ENAB", mEnabled); - if (mCount != 1) - esm.writeHNT("COUN", mCount); - if (!inInventory && mPosition != mRef.mPos) { std::array pos; @@ -99,7 +85,6 @@ namespace ESM mRef.blank(); mHasLocals = 0; mEnabled = false; - mCount = 1; for (int i = 0; i < 3; ++i) { mPosition.pos[i] = 0; diff --git a/components/esm3/objectstate.hpp b/components/esm3/objectstate.hpp index 4c09d16d18..b3f7bd3d45 100644 --- a/components/esm3/objectstate.hpp +++ b/components/esm3/objectstate.hpp @@ -32,7 +32,6 @@ namespace ESM Locals mLocals; LuaScripts mLuaScripts; unsigned char mEnabled; - int32_t mCount; Position mPosition; uint32_t mFlags; @@ -46,7 +45,6 @@ namespace ESM ObjectState() : mHasLocals(0) , mEnabled(0) - , mCount(0) , mFlags(0) , mHasCustomState(true) { diff --git a/components/esm3/player.cpp b/components/esm3/player.cpp index 3b52f8d779..fd280bf12e 100644 --- a/components/esm3/player.cpp +++ b/components/esm3/player.cpp @@ -13,16 +13,11 @@ namespace ESM mCellId = esm.getCellId(); - esm.getHNTSized<12>(mLastKnownExteriorPosition, "LKEP"); + esm.getHNT("LKEP", mLastKnownExteriorPosition); - if (esm.isNextSub("MARK")) - { - mHasMark = true; - esm.getHTSized<24>(mMarkedPosition); + mHasMark = esm.getHNOT("MARK", mMarkedPosition.pos, mMarkedPosition.rot); + if (mHasMark) mMarkedCell = esm.getCellId(); - } - else - mHasMark = false; // Automove, no longer used. if (esm.isNextSub("AMOV")) @@ -35,16 +30,12 @@ namespace ESM mPaidCrimeId = -1; esm.getHNOT(mPaidCrimeId, "PAYD"); - bool checkPrevItems = true; - while (checkPrevItems) + while (esm.peekNextSub("BOUN")) { - ESM::RefId boundItemId = esm.getHNORefId("BOUN"); - ESM::RefId prevItemId = esm.getHNORefId("PREV"); + ESM::RefId boundItemId = esm.getHNRefId("BOUN"); + ESM::RefId prevItemId = esm.getHNRefId("PREV"); - if (!boundItemId.empty()) - mPreviousItems[boundItemId] = prevItemId; - else - checkPrevItems = false; + mPreviousItems[boundItemId] = prevItemId; } if (esm.getFormatVersion() <= MaxOldSkillsAndAttributesFormatVersion) @@ -108,10 +99,10 @@ namespace ESM esm.writeHNT("CURD", mCurrentCrimeId); esm.writeHNT("PAYD", mPaidCrimeId); - for (PreviousItems::const_iterator it = mPreviousItems.begin(); it != mPreviousItems.end(); ++it) + for (const auto& [bound, prev] : mPreviousItems) { - esm.writeHNRefId("BOUN", it->first); - esm.writeHNRefId("PREV", it->second); + esm.writeHNRefId("BOUN", bound); + esm.writeHNRefId("PREV", prev); } esm.writeHNT("WWAT", mSaveAttributes); diff --git a/components/esm3/player.hpp b/components/esm3/player.hpp index 7f9309765c..0f76a3b5eb 100644 --- a/components/esm3/player.hpp +++ b/components/esm3/player.hpp @@ -33,8 +33,7 @@ namespace ESM float mSaveAttributes[Attribute::Length]; float mSaveSkills[Skill::Length]; - typedef std::map PreviousItems; // previous equipped items, needed for bound spells - PreviousItems mPreviousItems; + std::map mPreviousItems; // previous equipped items, needed for bound spells void load(ESMReader& esm); void save(ESMWriter& esm) const; diff --git a/components/esm3/projectilestate.cpp b/components/esm3/projectilestate.cpp index 15ff5fff64..e20cefa882 100644 --- a/components/esm3/projectilestate.cpp +++ b/components/esm3/projectilestate.cpp @@ -17,8 +17,8 @@ namespace ESM void BaseProjectileState::load(ESMReader& esm) { mId = esm.getHNRefId("ID__"); - esm.getHNTSized<12>(mPosition, "VEC3"); - esm.getHNTSized<16>(mOrientation, "QUAT"); + esm.getHNT("VEC3", mPosition.mValues); + esm.getHNT("QUAT", mOrientation.mValues); esm.getHNT(mActorId, "ACTO"); } @@ -37,18 +37,11 @@ namespace ESM BaseProjectileState::load(esm); mSpellId = esm.getHNRefId("SPEL"); - if (esm.isNextSub("SRCN")) // for backwards compatibility - esm.skipHSub(); - EffectList().load(esm); // for backwards compatibility esm.getHNT(mSpeed, "SPED"); if (esm.peekNextSub("ITEM")) mItem = esm.getFormId(true, "ITEM"); if (esm.isNextSub("SLOT")) // for backwards compatibility esm.skipHSub(); - if (esm.isNextSub("STCK")) // for backwards compatibility - esm.skipHSub(); - if (esm.isNextSub("SOUN")) // for backwards compatibility - esm.skipHSub(); } void ProjectileState::save(ESMWriter& esm) const @@ -65,7 +58,7 @@ namespace ESM BaseProjectileState::load(esm); mBowId = esm.getHNRefId("BOW_"); - esm.getHNTSized<12>(mVelocity, "VEL_"); + esm.getHNT("VEL_", mVelocity.mValues); mAttackStrength = 1.f; esm.getHNOT(mAttackStrength, "STR_"); diff --git a/components/esm3/quickkeys.cpp b/components/esm3/quickkeys.cpp index ababa535b7..7477fd24fa 100644 --- a/components/esm3/quickkeys.cpp +++ b/components/esm3/quickkeys.cpp @@ -8,9 +8,6 @@ namespace ESM void QuickKeys::load(ESMReader& esm) { - if (esm.isNextSub("KEY_")) - esm.getSubHeader(); // no longer used, because sub-record hierachies do not work properly in esmreader - while (esm.isNextSub("TYPE")) { QuickKey key; @@ -18,9 +15,6 @@ namespace ESM key.mId = esm.getHNRefId("ID__"); mKeys.push_back(key); - - if (esm.isNextSub("KEY_")) - esm.getSubHeader(); // no longer used, because sub-record hierachies do not work properly in esmreader } } diff --git a/components/esm3/savedgame.cpp b/components/esm3/savedgame.cpp index cec2b5e189..3ffe062d76 100644 --- a/components/esm3/savedgame.cpp +++ b/components/esm3/savedgame.cpp @@ -17,7 +17,7 @@ namespace ESM mPlayerCellName = esm.getHNRefId("PLCE").toString(); else mPlayerCellName = esm.getHNString("PLCE"); - esm.getHNTSized<16>(mInGameTime, "TSTM"); + esm.getHNT("TSTM", mInGameTime.mGameHour, mInGameTime.mDay, mInGameTime.mMonth, mInGameTime.mYear); esm.getHNT(mTimePlayed, "TIME"); mDescription = esm.getHNString("DESC"); diff --git a/components/esm3/selectiongroup.cpp b/components/esm3/selectiongroup.cpp new file mode 100644 index 0000000000..6b819a4bbc --- /dev/null +++ b/components/esm3/selectiongroup.cpp @@ -0,0 +1,38 @@ +#include "selectiongroup.hpp" + +#include "esmreader.hpp" +#include "esmwriter.hpp" + +namespace ESM +{ + void SelectionGroup::load(ESMReader& esm, bool& isDeleted) + { + + while (esm.hasMoreSubs()) + { + esm.getSubName(); + switch (esm.retSubName().toInt()) + { + case fourCC("SELC"): + mId = esm.getRefId(); + break; + case fourCC("SELI"): + selectedInstances.push_back(esm.getRefId().getRefIdString()); + break; + default: + esm.fail("Unknown subrecord"); + break; + } + } + } + + void SelectionGroup::save(ESMWriter& esm, bool isDeleted) const + { + esm.writeHNCRefId("SELC", mId); + for (std::string id : selectedInstances) + esm.writeHNCString("SELI", id); + } + + void SelectionGroup::blank() {} + +} diff --git a/components/esm3/selectiongroup.hpp b/components/esm3/selectiongroup.hpp new file mode 100644 index 0000000000..021f3c95d5 --- /dev/null +++ b/components/esm3/selectiongroup.hpp @@ -0,0 +1,34 @@ +#ifndef COMPONENTS_ESM_SELECTIONGROUP_H +#define COMPONENTS_ESM_SELECTIONGROUP_H + +#include + +#include "components/esm/defs.hpp" +#include "components/esm/refid.hpp" + +namespace ESM +{ + class ESMReader; + class ESMWriter; + + struct SelectionGroup + { + constexpr static RecNameInts sRecordId = REC_SELG; + + static constexpr std::string_view getRecordType() { return "SelectionGroup"; } + + uint32_t mRecordFlags = 0; + + RefId mId; + + std::vector selectedInstances; + + void load(ESMReader& esm, bool& isDeleted); + void save(ESMWriter& esm, bool isDeleted = false) const; + + /// Set record to default state (does not touch the ID). + void blank(); + }; +} + +#endif diff --git a/components/esm3/statstate.cpp b/components/esm3/statstate.cpp index 7477d83e2d..b46c2e34fd 100644 --- a/components/esm3/statstate.cpp +++ b/components/esm3/statstate.cpp @@ -32,10 +32,6 @@ namespace ESM int32_t current = 0; esm.getHNOT(current, "STCU"); mCurrent = static_cast(current); - - int32_t oldDamage = 0; - esm.getHNOT(oldDamage, "STDA"); - mDamage = static_cast(oldDamage); } else { diff --git a/components/esm4/loadachr.cpp b/components/esm4/loadachr.cpp index 1a6d47497e..dc181dda4b 100644 --- a/components/esm4/loadachr.cpp +++ b/components/esm4/loadachr.cpp @@ -82,6 +82,11 @@ void ESM4::ActorCharacter::load(ESM4::Reader& reader) reader.getFormId(mEsp.parent); reader.get(mEsp.flags); break; + case ESM4::SUB_XCNT: + { + reader.get(mCount); + break; + } case ESM4::SUB_XRGD: // ragdoll case ESM4::SUB_XRGB: // ragdoll biped case ESM4::SUB_XHRS: // horse formId @@ -113,7 +118,6 @@ void ESM4::ActorCharacter::load(ESM4::Reader& reader) case ESM4::SUB_XATO: // FONV case ESM4::SUB_MNAM: // FO4 case ESM4::SUB_XATP: // FO4 - case ESM4::SUB_XCNT: // FO4 case ESM4::SUB_XEMI: // FO4 case ESM4::SUB_XFVC: // FO4 case ESM4::SUB_XHLT: // FO4 diff --git a/components/esm4/loadachr.hpp b/components/esm4/loadachr.hpp index dd867bbafd..8abb47c8bc 100644 --- a/components/esm4/loadachr.hpp +++ b/components/esm4/loadachr.hpp @@ -57,6 +57,8 @@ namespace ESM4 EnableParent mEsp; + std::int32_t mCount = 1; + void load(ESM4::Reader& reader); // void save(ESM4::Writer& writer) const; diff --git a/components/esm4/loadglob.cpp b/components/esm4/loadglob.cpp index 39593a4a7d..436f3e34ae 100644 --- a/components/esm4/loadglob.cpp +++ b/components/esm4/loadglob.cpp @@ -44,19 +44,16 @@ void ESM4::GlobalVariable::load(ESM4::Reader& reader) case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM::fourCC("XALG"): // FO76 + reader.get(mExtraFlags2); + break; case ESM4::SUB_FNAM: reader.get(mType); break; case ESM4::SUB_FLTV: reader.get(mValue); break; - case ESM4::SUB_FULL: - case ESM4::SUB_MODL: - case ESM4::SUB_MODB: - case ESM4::SUB_ICON: - case ESM4::SUB_DATA: - case ESM4::SUB_OBND: // TES5 - case ESM4::SUB_VMAD: // TES5 + case ESM::fourCC("NTWK"): // FO76 reader.skipSubRecordData(); break; default: diff --git a/components/esm4/loadglob.hpp b/components/esm4/loadglob.hpp index c9c83f58b4..89959bcab1 100644 --- a/components/esm4/loadglob.hpp +++ b/components/esm4/loadglob.hpp @@ -42,6 +42,7 @@ namespace ESM4 { ESM::FormId mId; // from the header std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + std::uint64_t mExtraFlags2; std::string mEditorId; diff --git a/components/esm4/loadhdpt.cpp b/components/esm4/loadhdpt.cpp index 250a687042..c560ff5fac 100644 --- a/components/esm4/loadhdpt.cpp +++ b/components/esm4/loadhdpt.cpp @@ -48,6 +48,9 @@ void ESM4::HeadPart::load(ESM4::Reader& reader) case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM::fourCC("XALG"): // FO76 + reader.get(mExtraFlags2); + break; case ESM4::SUB_FULL: reader.getLocalizedString(mFullName); break; @@ -58,7 +61,7 @@ void ESM4::HeadPart::load(ESM4::Reader& reader) reader.getZString(mModel); break; case ESM4::SUB_HNAM: - reader.getFormId(mAdditionalPart); + reader.getFormId(mExtraParts.emplace_back()); break; case ESM4::SUB_NAM0: // TES5 { @@ -87,15 +90,25 @@ void ESM4::HeadPart::load(ESM4::Reader& reader) case ESM4::SUB_TNAM: reader.getFormId(mBaseTexture); break; + case ESM4::SUB_CNAM: + reader.getFormId(mColor); + break; + case ESM4::SUB_RNAM: + reader.getFormId(mValidRaces.emplace_back()); + break; case ESM4::SUB_PNAM: reader.get(mType); break; case ESM4::SUB_MODT: // Model data case ESM4::SUB_MODC: case ESM4::SUB_MODS: - case ESM4::SUB_MODF: // Model data end - case ESM4::SUB_RNAM: - case ESM4::SUB_CNAM: + case ESM4::SUB_MODF: + case ESM::fourCC("ENLM"): + case ESM::fourCC("XFLG"): + case ESM::fourCC("ENLT"): + case ESM::fourCC("ENLS"): + case ESM::fourCC("AUUV"): + case ESM::fourCC("MODD"): // Model data end case ESM4::SUB_CTDA: reader.skipSubRecordData(); break; diff --git a/components/esm4/loadhdpt.hpp b/components/esm4/loadhdpt.hpp index aca3b0ca7b..5d17720100 100644 --- a/components/esm4/loadhdpt.hpp +++ b/components/esm4/loadhdpt.hpp @@ -43,6 +43,7 @@ namespace ESM4 { ESM::FormId mId; // from the header std::uint32_t mFlags; // from the header, see enum type RecordFlag for details + std::uint64_t mExtraFlags2; std::string mEditorId; std::string mFullName; @@ -70,10 +71,12 @@ namespace ESM4 Type_Eyelashes = 13, }; - ESM::FormId mAdditionalPart; + std::vector mExtraParts; std::array mTriFile; ESM::FormId mBaseTexture; + ESM::FormId mColor; + std::vector mValidRaces; void load(ESM4::Reader& reader); // void save(ESM4::Writer& writer) const; diff --git a/components/esm4/loadrefr.cpp b/components/esm4/loadrefr.cpp index a193907ac4..fb26e39546 100644 --- a/components/esm4/loadrefr.cpp +++ b/components/esm4/loadrefr.cpp @@ -268,6 +268,11 @@ void ESM4::Reference::load(ESM4::Reader& reader) break; } + case ESM4::SUB_XCNT: + { + reader.get(mCount); + break; + } // lighting case ESM4::SUB_LNAM: // lighting template formId case ESM4::SUB_XLIG: // struct, FOV, fade, etc @@ -279,7 +284,6 @@ void ESM4::Reference::load(ESM4::Reader& reader) // case ESM4::SUB_XPCI: // formId case ESM4::SUB_XLCM: - case ESM4::SUB_XCNT: case ESM4::SUB_ONAM: case ESM4::SUB_VMAD: case ESM4::SUB_XPRM: diff --git a/components/esm4/loadrefr.hpp b/components/esm4/loadrefr.hpp index af04a791c8..ec76928827 100644 --- a/components/esm4/loadrefr.hpp +++ b/components/esm4/loadrefr.hpp @@ -97,7 +97,7 @@ namespace ESM4 EnableParent mEsp; - std::uint32_t mCount = 1; // only if > 1 + std::int32_t mCount = 1; // only if > 1 ESM::FormId mAudioLocation; diff --git a/components/esm4/loadtes4.cpp b/components/esm4/loadtes4.cpp index a2fbe8b139..0cbf91c52e 100644 --- a/components/esm4/loadtes4.cpp +++ b/components/esm4/loadtes4.cpp @@ -100,6 +100,7 @@ void ESM4::Header::load(ESM4::Reader& reader) case ESM4::SUB_OFST: // Oblivion only? case ESM4::SUB_DELE: // Oblivion only? case ESM4::SUB_TNAM: // Fallout 4 (CK only) + case ESM::fourCC("MMSB"): // Fallout 76 reader.skipSubRecordData(); break; default: diff --git a/components/esm4/loadtxst.cpp b/components/esm4/loadtxst.cpp index 3f1aebafb4..3b5f04f265 100644 --- a/components/esm4/loadtxst.cpp +++ b/components/esm4/loadtxst.cpp @@ -44,6 +44,9 @@ void ESM4::TextureSet::load(ESM4::Reader& reader) case ESM4::SUB_EDID: reader.getZString(mEditorId); break; + case ESM::fourCC("FLTR"): // FO76 + reader.getZString(mFilter); + break; case ESM4::SUB_TX00: reader.getZString(mDiffuse); break; @@ -51,29 +54,45 @@ void ESM4::TextureSet::load(ESM4::Reader& reader) reader.getZString(mNormalMap); break; case ESM4::SUB_TX02: + // This is a "wrinkle map" in FO4/76 reader.getZString(mEnvMask); break; case ESM4::SUB_TX03: + // This is a glow map in FO4/76 reader.getZString(mToneMap); break; case ESM4::SUB_TX04: + // This is a height map in FO4/76 reader.getZString(mDetailMap); break; case ESM4::SUB_TX05: reader.getZString(mEnvMap); break; case ESM4::SUB_TX06: - reader.getZString(mUnknown); + reader.getZString(mMultiLayer); break; case ESM4::SUB_TX07: + // This is a "smooth specular" map in FO4/76 reader.getZString(mSpecular); break; + case ESM::fourCC("TX08"): // FO76 + reader.getZString(mSpecular); + break; + case ESM::fourCC("TX09"): // FO76 + reader.getZString(mLighting); + break; + case ESM::fourCC("TX10"): // FO76 + reader.getZString(mFlow); + break; + case ESM4::SUB_DNAM: + reader.get(mDataFlags); + break; case ESM4::SUB_MNAM: reader.getZString(mMaterial); break; - case ESM4::SUB_DNAM: - case ESM4::SUB_DODT: + case ESM4::SUB_DODT: // Decal data case ESM4::SUB_OBND: // object bounds + case ESM::fourCC("OPDS"): // Object placement defaults, FO76 reader.skipSubRecordData(); break; default: diff --git a/components/esm4/loadtxst.hpp b/components/esm4/loadtxst.hpp index 0b55f37f8c..8e628df841 100644 --- a/components/esm4/loadtxst.hpp +++ b/components/esm4/loadtxst.hpp @@ -44,6 +44,7 @@ namespace ESM4 std::uint32_t mFlags; // from the header, see enum type RecordFlag for details std::string mEditorId; + std::string mFilter; std::string mDiffuse; // includes alpha info std::string mNormalMap; // includes specular info (alpha channel) @@ -51,8 +52,12 @@ namespace ESM4 std::string mToneMap; std::string mDetailMap; std::string mEnvMap; - std::string mUnknown; + std::string mMultiLayer; std::string mSpecular; + std::string mSmoothSpecular; + std::string mLighting; + std::string mFlow; + std::uint16_t mDataFlags; std::string mMaterial; void load(ESM4::Reader& reader); diff --git a/components/fallback/validate.cpp b/components/fallback/validate.cpp index 300d2b5dd1..8dfcd97570 100644 --- a/components/fallback/validate.cpp +++ b/components/fallback/validate.cpp @@ -11,7 +11,8 @@ static const std::set allowedKeysInt = { "LightAttenuation_Lin "Water_RippleFrameCount", "Water_SurfaceTileCount", "Water_SurfaceFrameCount", "Weather_Clear_Using_Precip", "Weather_Cloudy_Using_Precip", "Weather_Foggy_Using_Precip", "Weather_Overcast_Using_Precip", "Weather_Rain_Using_Precip", "Weather_Thunderstorm_Using_Precip", "Weather_Ashstorm_Using_Precip", - "Weather_Blight_Using_Precip", "Weather_Snow_Using_Precip", "Weather_Blizzard_Using_Precip" }; + "Weather_Blight_Using_Precip", "Weather_Snow_Using_Precip", "Weather_Blizzard_Using_Precip", "Weather_Rain_Ripples", + "Weather_Snow_Ripples" }; static const std::set allowedKeysFloat = { "General_Werewolf_FOV", "Inventory_DirectionalAmbientB", "Inventory_DirectionalAmbientG", "Inventory_DirectionalAmbientR", "Inventory_DirectionalDiffuseB", @@ -160,7 +161,7 @@ static const std::set allowedKeysNonNumeric = { "Blood_Model_0 "Weather_Rain_Ambient_Sunrise_Color", "Weather_Rain_Ambient_Sunset_Color", "Weather_Rain_Cloud_Texture", "Weather_Rain_Fog_Day_Color", "Weather_Rain_Fog_Night_Color", "Weather_Rain_Fog_Sunrise_Color", "Weather_Rain_Fog_Sunset_Color", "Weather_Rain_Rain_Loop_Sound_ID", "Weather_Rain_Ripple_Radius", - "Weather_Rain_Ripples", "Weather_Rain_Ripple_Scale", "Weather_Rain_Ripple_Speed", "Weather_Rain_Ripples_Per_Drop", + "Weather_Rain_Ripple_Scale", "Weather_Rain_Ripple_Speed", "Weather_Rain_Ripples_Per_Drop", "Weather_Rain_Sky_Day_Color", "Weather_Rain_Sky_Night_Color", "Weather_Rain_Sky_Sunrise_Color", "Weather_Rain_Sky_Sunset_Color", "Weather_Rain_Sun_Day_Color", "Weather_Rain_Sun_Disc_Sunset_Color", "Weather_Rain_Sun_Night_Color", "Weather_Rain_Sun_Sunrise_Color", "Weather_Rain_Sun_Sunset_Color", @@ -168,7 +169,7 @@ static const std::set allowedKeysNonNumeric = { "Blood_Model_0 "Weather_Snow_Ambient_Sunrise_Color", "Weather_Snow_Ambient_Sunset_Color", "Weather_Snow_Cloud_Texture", "Weather_Snow_Fog_Day_Color", "Weather_Snow_Fog_Night_Color", "Weather_Snow_Fog_Sunrise_Color", "Weather_Snow_Fog_Sunset_Color", "Weather_Snow_Gravity_Scale", "Weather_Snow_High_Kill", "Weather_Snow_Low_Kill", - "Weather_Snow_Max_Snowflakes", "Weather_Snow_Ripple_Radius", "Weather_Snow_Ripples", "Weather_Snow_Ripple_Scale", + "Weather_Snow_Max_Snowflakes", "Weather_Snow_Ripple_Radius", "Weather_Snow_Ripple_Scale", "Weather_Snow_Ripple_Speed", "Weather_Snow_Ripples_Per_Flake", "Weather_Snow_Sky_Day_Color", "Weather_Snow_Sky_Night_Color", "Weather_Snow_Sky_Sunrise_Color", "Weather_Snow_Sky_Sunset_Color", "Weather_Snow_Snow_Diameter", "Weather_Snow_Snow_Entrance_Speed", "Weather_Snow_Snow_Height_Max", diff --git a/components/files/macospath.cpp b/components/files/macospath.cpp index 2d0a409782..4b37c2fb26 100644 --- a/components/files/macospath.cpp +++ b/components/files/macospath.cpp @@ -5,13 +5,41 @@ #include #include #include +#include #include #include +#include +#include #include namespace { + std::filesystem::path getBinaryPath() + { + uint32_t bufsize = 0; + _NSGetExecutablePath(nullptr, &bufsize); + + std::vector buf(bufsize); + + if (_NSGetExecutablePath(buf.data(), &bufsize) == 0) + { + std::filesystem::path path = std::filesystem::path(buf.begin(), buf.end()); + + if (std::filesystem::is_symlink(path)) + { + return std::filesystem::read_symlink(path); + } + + return path; + } + else + { + Log(Debug::Warning) << "Not enough buffer size to get executable path: " << bufsize; + throw std::runtime_error("Failed to get executable path"); + } + } + std::filesystem::path getUserHome() { const char* dir = getenv("HOME"); @@ -36,6 +64,11 @@ namespace Files MacOsPath::MacOsPath(const std::string& application_name) : mName(application_name) { + std::filesystem::path binary_path = getBinaryPath(); + std::error_code ec; + std::filesystem::current_path(binary_path.parent_path(), ec); + if (ec.value() != 0) + Log(Debug::Warning) << "Error " << ec.message() << " when changing current directory"; } std::filesystem::path MacOsPath::getUserConfigPath() const diff --git a/components/fx/pass.cpp b/components/fx/pass.cpp index 76b54d55a5..cf50d20fe2 100644 --- a/components/fx/pass.cpp +++ b/components/fx/pass.cpp @@ -91,6 +91,7 @@ uniform @builtinSampler omw_SamplerLastShader; uniform @builtinSampler omw_SamplerLastPass; uniform @builtinSampler omw_SamplerDepth; uniform @builtinSampler omw_SamplerNormals; +uniform @builtinSampler omw_SamplerDistortion; uniform vec4 omw_PointLights[@pointLightCount]; uniform int omw_PointLightsCount; diff --git a/components/fx/technique.hpp b/components/fx/technique.hpp index ed356e0a37..0d17128e56 100644 --- a/components/fx/technique.hpp +++ b/components/fx/technique.hpp @@ -175,6 +175,9 @@ namespace fx void setLocked(bool locked) { mLocked = locked; } bool getLocked() const { return mLocked; } + void setInternal(bool internal) { mInternal = internal; } + bool getInternal() const { return mInternal; } + private: [[noreturn]] void error(const std::string& msg); @@ -295,6 +298,7 @@ namespace fx bool mDynamic = false; bool mLocked = false; + bool mInternal = false; }; template <> diff --git a/components/lua/inputactions.cpp b/components/lua/inputactions.cpp new file mode 100644 index 0000000000..7c7551ba60 --- /dev/null +++ b/components/lua/inputactions.cpp @@ -0,0 +1,296 @@ +#include "inputactions.hpp" + +#include +#include + +#include +#include + +#include "luastate.hpp" + +namespace LuaUtil +{ + namespace InputAction + { + namespace + { + std::string_view typeName(Type actionType) + { + switch (actionType) + { + case Type::Boolean: + return "Boolean"; + case Type::Number: + return "Number"; + case Type::Range: + return "Range"; + default: + throw std::logic_error("Unknown input action type"); + } + } + } + + MultiTree::Node MultiTree::insert() + { + size_t nextId = size(); + mChildren.push_back({}); + mParents.push_back({}); + return nextId; + } + + bool MultiTree::validateTree() const + { + std::vector complete(size(), false); + traverse([&complete](Node node) { complete[node] = true; }); + return std::find(complete.begin(), complete.end(), false) == complete.end(); + } + + template + void MultiTree::traverse(Function callback) const + { + std::queue nodeQueue; + std::vector complete(size(), false); + for (Node root = 0; root < size(); ++root) + { + if (!complete[root]) + nodeQueue.push(root); + while (!nodeQueue.empty()) + { + Node node = nodeQueue.back(); + nodeQueue.pop(); + + bool isComplete = true; + for (Node parent : mParents[node]) + isComplete = isComplete && complete[parent]; + complete[node] = isComplete; + if (isComplete) + { + callback(node); + for (Node child : mChildren[node]) + nodeQueue.push(child); + } + } + } + } + + bool MultiTree::multiEdge(Node target, const std::vector& source) + { + mParents[target].reserve(mParents[target].size() + source.size()); + for (Node s : source) + { + mParents[target].push_back(s); + mChildren[s].push_back(target); + } + bool validTree = validateTree(); + if (!validTree) + { + for (Node s : source) + { + mParents[target].pop_back(); + mChildren[s].pop_back(); + } + } + return validTree; + } + + namespace + { + bool validateActionValue(sol::object value, Type type) + { + switch (type) + { + case Type::Boolean: + return value.get_type() == sol::type::boolean; + case Type::Number: + return value.get_type() == sol::type::number; + case Type::Range: + if (value.get_type() != sol::type::number) + return false; + double d = value.as(); + return 0.0 <= d && d <= 1.0; + } + throw std::invalid_argument("Unknown action type"); + } + } + + void Registry::insert(const Info& info) + { + if (mIds.find(info.mKey) != mIds.end()) + throw std::domain_error(Misc::StringUtils::format("Action key \"%s\" is already in use", info.mKey)); + if (info.mKey.empty()) + throw std::domain_error("Action key can't be an empty string"); + if (info.mL10n.empty()) + throw std::domain_error("Localization context can't be empty"); + if (!validateActionValue(info.mDefaultValue, info.mType)) + throw std::logic_error(Misc::StringUtils::format( + "Invalid value: \"%s\" for action \"%s\"", LuaUtil::toString(info.mDefaultValue), info.mKey)); + Id id = mBindingTree.insert(); + mKeys.push_back(info.mKey); + mIds[std::string(info.mKey)] = id; + mInfo.push_back(info); + mHandlers.push_back({}); + mBindings.push_back({}); + mValues.push_back(info.mDefaultValue); + } + + std::optional Registry::nextKey(std::string_view key) const + { + auto it = mIds.find(key); + if (it == mIds.end()) + return std::nullopt; + auto nextId = it->second + 1; + if (nextId >= mKeys.size()) + return std::nullopt; + return mKeys.at(nextId); + } + + std::optional Registry::operator[](std::string_view actionKey) + { + auto iter = mIds.find(actionKey); + if (iter == mIds.end()) + return std::nullopt; + return mInfo[iter->second]; + } + + Registry::Id Registry::safeIdByKey(std::string_view key) + { + auto iter = mIds.find(key); + if (iter == mIds.end()) + throw std::logic_error(Misc::StringUtils::format("Unknown action key: \"%s\"", key)); + return iter->second; + } + + bool Registry::bind( + std::string_view key, const LuaUtil::Callback& callback, const std::vector& dependencies) + { + Id id = safeIdByKey(key); + std::vector dependencyIds; + dependencyIds.reserve(dependencies.size()); + for (std::string_view s : dependencies) + dependencyIds.push_back(safeIdByKey(s)); + bool validEdge = mBindingTree.multiEdge(id, dependencyIds); + if (validEdge) + mBindings[id].push_back(Binding{ + callback, + std::move(dependencyIds), + }); + return validEdge; + } + + sol::object Registry::valueOfType(std::string_view key, Type type) + { + Id id = safeIdByKey(key); + Info info = mInfo[id]; + if (info.mType != type) + throw std::logic_error( + Misc::StringUtils::format("Attempt to get value of type \"%s\" from action \"%s\" with type \"%s\"", + typeName(type), key, typeName(info.mType))); + return mValues[id]; + } + + void Registry::update(double dt) + { + std::vector dependencyValues; + mBindingTree.traverse([this, &dependencyValues, dt](Id node) { + sol::main_object newValue = mValues[node]; + std::vector& bindings = mBindings[node]; + bindings.erase(std::remove_if(bindings.begin(), bindings.end(), + [&](const Binding& binding) { + if (!binding.mCallback.isValid()) + return true; + + dependencyValues.clear(); + for (Id parent : binding.mDependencies) + dependencyValues.push_back(mValues[parent]); + try + { + newValue = sol::main_object( + binding.mCallback.call(dt, newValue, sol::as_args(dependencyValues))); + } + catch (std::exception& e) + { + if (!validateActionValue(newValue, mInfo[node].mType)) + Log(Debug::Error) << Misc::StringUtils::format( + "Error due to invalid value of action \"%s\"(\"%s\"): ", mKeys[node], + LuaUtil::toString(newValue)) + << e.what(); + else + Log(Debug::Error) << "Error in callback: " << e.what(); + } + return false; + }), + bindings.end()); + + if (!validateActionValue(newValue, mInfo[node].mType)) + Log(Debug::Error) << Misc::StringUtils::format( + "Invalid value of action \"%s\": %s", mKeys[node], LuaUtil::toString(newValue)); + if (mValues[node] != newValue) + { + mValues[node] = sol::object(newValue); + std::vector& handlers = mHandlers[node]; + handlers.erase(std::remove_if(handlers.begin(), handlers.end(), + [&](const LuaUtil::Callback& handler) { + if (!handler.isValid()) + return true; + handler.tryCall(newValue); + return false; + }), + handlers.end()); + } + }); + } + } + + namespace InputTrigger + { + Registry::Id Registry::safeIdByKey(std::string_view key) + { + auto it = mIds.find(key); + if (it == mIds.end()) + throw std::domain_error(Misc::StringUtils::format("Unknown trigger key \"%s\"", key)); + return it->second; + } + + void Registry::insert(const Info& info) + { + if (mIds.find(info.mKey) != mIds.end()) + throw std::domain_error(Misc::StringUtils::format("Trigger key \"%s\" is already in use", info.mKey)); + if (info.mKey.empty()) + throw std::domain_error("Trigger key can't be an empty string"); + if (info.mL10n.empty()) + throw std::domain_error("Localization context can't be empty"); + Id id = mIds.size(); + mIds[info.mKey] = id; + mInfo.push_back(info); + mHandlers.push_back({}); + } + + std::optional Registry::operator[](std::string_view key) + { + auto iter = mIds.find(key); + if (iter == mIds.end()) + return std::nullopt; + return mInfo[iter->second]; + } + + void Registry::registerHandler(std::string_view key, const LuaUtil::Callback& callback) + { + Id id = safeIdByKey(key); + mHandlers[id].push_back(callback); + } + + void Registry::activate(std::string_view key) + { + Id id = safeIdByKey(key); + std::vector& handlers = mHandlers[id]; + handlers.erase(std::remove_if(handlers.begin(), handlers.end(), + [&](const LuaUtil::Callback& handler) { + if (!handler.isValid()) + return true; + handler.tryCall(); + return false; + }), + handlers.end()); + } + } +} diff --git a/components/lua/inputactions.hpp b/components/lua/inputactions.hpp new file mode 100644 index 0000000000..d05bb71f2c --- /dev/null +++ b/components/lua/inputactions.hpp @@ -0,0 +1,149 @@ +#ifndef COMPONENTS_LUA_INPUTACTIONS +#define COMPONENTS_LUA_INPUTACTIONS + +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace LuaUtil::InputAction +{ + enum class Type + { + Boolean, + Number, + Range, + }; + + struct Info + { + std::string mKey; + Type mType; + std::string mL10n; + std::string mName; + std::string mDescription; + sol::main_object mDefaultValue; + }; + + class MultiTree + { + public: + using Node = size_t; + + Node insert(); + bool multiEdge(Node target, const std::vector& source); + size_t size() const { return mParents.size(); } + + template // Function = void(Node) + void traverse(Function callback) const; + + void clear() + { + mParents.clear(); + mChildren.clear(); + } + + private: + std::vector> mParents; + std::vector> mChildren; + + bool validateTree() const; + }; + + class Registry + { + public: + using ConstIterator = std::vector::const_iterator; + void insert(const Info& info); + size_t size() const { return mKeys.size(); } + std::optional firstKey() const { return mKeys.empty() ? std::nullopt : std::optional(mKeys[0]); } + std::optional nextKey(std::string_view key) const; + std::optional operator[](std::string_view actionKey); + bool bind( + std::string_view key, const LuaUtil::Callback& callback, const std::vector& dependencies); + sol::object valueOfType(std::string_view key, Type type); + void update(double dt); + void registerHandler(std::string_view key, const LuaUtil::Callback& handler) + { + mHandlers[safeIdByKey(key)].push_back(handler); + } + void clear() + { + mKeys.clear(); + mIds.clear(); + mInfo.clear(); + mHandlers.clear(); + mBindings.clear(); + mValues.clear(); + mBindingTree.clear(); + } + + private: + using Id = MultiTree::Node; + Id safeIdByKey(std::string_view key); + struct Binding + { + LuaUtil::Callback mCallback; + std::vector mDependencies; + }; + std::vector mKeys; + std::unordered_map> mIds; + std::vector mInfo; + std::vector> mHandlers; + std::vector> mBindings; + std::vector mValues; + MultiTree mBindingTree; + }; +} + +namespace LuaUtil::InputTrigger +{ + struct Info + { + std::string mKey; + std::string mL10n; + std::string mName; + std::string mDescription; + }; + + class Registry + { + public: + std::optional firstKey() const + { + return mIds.empty() ? std::nullopt : std::optional(mIds.begin()->first); + } + std::optional nextKey(std::string_view key) const + { + auto it = mIds.find(key); + if (it == mIds.end() || ++it == mIds.end()) + return std::nullopt; + return it->first; + } + std::optional operator[](std::string_view key); + 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(); + } + + private: + using Id = size_t; + Id safeIdByKey(std::string_view key); + std::unordered_map> mIds; + std::vector mInfo; + std::vector> mHandlers; + }; +} + +#endif // COMPONENTS_LUA_INPUTACTIONS diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp index 95b56fb020..0a350a2d9f 100644 --- a/components/lua/luastate.cpp +++ b/components/lua/luastate.cpp @@ -179,6 +179,10 @@ namespace LuaUtil mSol.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, sol::lib::bit32, sol::lib::string, sol::lib::table, sol::lib::os, sol::lib::debug); +#ifndef NO_LUAJIT + mSol.open_libraries(sol::lib::jit); +#endif // NO_LUAJIT + mSol["math"]["randomseed"](static_cast(std::time(nullptr))); mSol["math"]["randomseed"] = [] {}; diff --git a/components/lua_ui/element.cpp b/components/lua_ui/element.cpp index baa3438982..84383f89e1 100644 --- a/components/lua_ui/element.cpp +++ b/components/lua_ui/element.cpp @@ -280,6 +280,7 @@ namespace LuaUi auto children = parent->children(); auto it = std::find(children.begin(), children.end(), mRoot); mRoot = createWidget(layout(), 0); + assert(it != children.end()); *it = mRoot; parent->setChildren(children); mRoot->updateCoord(); diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index ce552df4f7..aa0e0dec7d 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -159,7 +159,7 @@ std::string Misc::ResourceHelpers::correctActorModelPath(const std::string& resP return mdlname; } -std::string Misc::ResourceHelpers::correctMeshPath(std::string_view resPath, const VFS::Manager* vfs) +std::string Misc::ResourceHelpers::correctMeshPath(std::string_view resPath) { std::string res = "meshes\\"; res.append(resPath); diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp index 478569ed14..f2b576813b 100644 --- a/components/misc/resourcehelpers.hpp +++ b/components/misc/resourcehelpers.hpp @@ -33,7 +33,7 @@ namespace Misc std::string correctActorModelPath(const std::string& resPath, const VFS::Manager* vfs); // Adds "meshes\\". - std::string correctMeshPath(std::string_view resPath, const VFS::Manager* vfs); + std::string correctMeshPath(std::string_view resPath); // Adds "sound\\". std::string correctSoundPath(const std::string& resPath); diff --git a/components/misc/strings/algorithm.hpp b/components/misc/strings/algorithm.hpp index f34801b8d3..28bc696cd3 100644 --- a/components/misc/strings/algorithm.hpp +++ b/components/misc/strings/algorithm.hpp @@ -99,6 +99,13 @@ namespace Misc::StringUtils bool operator()(std::string_view left, std::string_view right) const { return ciLess(left, right); } }; + struct StringHash + { + using is_transparent = void; + [[nodiscard]] size_t operator()(std::string_view sv) const { return std::hash{}(sv); } + [[nodiscard]] size_t operator()(const std::string& s) const { return std::hash{}(s); } + }; + /** @brief Replaces all occurrences of a string in another string. * * @param str The string to operate on. diff --git a/components/nif/data.cpp b/components/nif/data.cpp index e653959bbc..fa66435aee 100644 --- a/components/nif/data.cpp +++ b/components/nif/data.cpp @@ -290,6 +290,17 @@ namespace Nif } } + const Nif::NiSkinPartition* NiSkinInstance::getPartitions() const + { + const Nif::NiSkinPartition* partitions = nullptr; + if (!mPartitions.empty()) + partitions = mPartitions.getPtr(); + else if (!mData.empty() && !mData->mPartitions.empty()) + partitions = mData->mPartitions.getPtr(); + + return partitions; + } + void BSDismemberSkinInstance::read(NIFStream* nif) { NiSkinInstance::read(nif); diff --git a/components/nif/data.hpp b/components/nif/data.hpp index 75c18d657a..c9daeef4d4 100644 --- a/components/nif/data.hpp +++ b/components/nif/data.hpp @@ -208,6 +208,8 @@ namespace Nif void read(NIFStream* nif) override; void post(Reader& nif) override; + + const Nif::NiSkinPartition* getPartitions() const; }; struct BSDismemberSkinInstance : public NiSkinInstance diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index 37e40938d3..d6d063a254 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -512,6 +512,10 @@ namespace Nif void Reader::parse(Files::IStreamPtr&& stream) { + const bool writeDebug = sWriteNifDebugLog; + if (writeDebug) + Log(Debug::Verbose) << "NIF Debug: Reading file: '" << mFilename << "'"; + const std::array fileHash = Files::getHash(mFilename, *stream); mHash.append(reinterpret_cast(fileHash.data()), fileHash.size() * sizeof(std::uint64_t)); @@ -538,15 +542,9 @@ namespace Nif }; const bool supportedVersion = std::find(supportedVers.begin(), supportedVers.end(), mVersion) != supportedVers.end(); - const bool writeDebugLog = sWriteNifDebugLog; - if (!supportedVersion) - { - if (!sLoadUnsupportedFiles) - throw Nif::Exception("Unsupported NIF version: " + versionToString(mVersion), mFilename); - if (writeDebugLog) - Log(Debug::Warning) << " NIFFile Warning: Unsupported NIF version: " << versionToString(mVersion) - << ". Proceed with caution! File: " << mFilename; - } + + if (!supportedVersion && !sLoadUnsupportedFiles) + throw Nif::Exception("Unsupported NIF version: " + versionToString(mVersion), mFilename); const bool hasEndianness = mVersion >= NIFStream::generateVersion(20, 0, 0, 4); const bool hasUserVersion = mVersion >= NIFStream::generateVersion(10, 0, 1, 8); @@ -603,6 +601,17 @@ namespace Nif } } + if (writeDebug) + { + std::stringstream versionInfo; + versionInfo << "NIF Debug: Version: " << versionToString(mVersion); + if (mUserVersion) + versionInfo << "\nUser version: " << mUserVersion; + if (mBethVersion) + versionInfo << "\nBSStream version: " << mBethVersion; + Log(Debug::Verbose) << versionInfo.str(); + } + if (hasRecTypeListings) { // TODO: 20.3.1.2 uses DJB hashes instead of strings @@ -658,9 +667,8 @@ namespace Nif r = entry->second(); - if (!supportedVersion && writeDebugLog) - Log(Debug::Verbose) << "NIF Debug: Reading record of type " << rec << ", index " << i << " (" - << mFilename << ")"; + if (writeDebug) + Log(Debug::Verbose) << "NIF Debug: Reading record of type " << rec << ", index " << i; assert(r != nullptr); assert(r->recType != RC_MISSING); diff --git a/components/nif/node.cpp b/components/nif/node.cpp index 01c0e1597d..b91f143d00 100644 --- a/components/nif/node.cpp +++ b/components/nif/node.cpp @@ -27,6 +27,37 @@ namespace mesh.preallocateIndices(static_cast(data.mNumTriangles) * 3); } + void trianglesToBtTriangleMesh(btTriangleMesh& mesh, const std::vector& triangles) + { + for (std::size_t i = 0; i < triangles.size(); i += 3) + mesh.addTriangleIndices(triangles[i + 0], triangles[i + 1], triangles[i + 2]); + } + + void stripsToBtTriangleMesh(btTriangleMesh& mesh, const std::vector>& strips) + { + for (const auto& strip : strips) + { + if (strip.size() < 3) + continue; + + unsigned short a; + unsigned short b = strip[0]; + unsigned short c = strip[1]; + for (size_t i = 2; i < strip.size(); i++) + { + a = b; + b = c; + c = strip[i]; + if (a == b || b == c || a == c) + continue; + if (i % 2 == 0) + mesh.addTriangleIndices(a, b, c); + else + mesh.addTriangleIndices(a, c, b); + } + } + } + } namespace Nif @@ -243,15 +274,33 @@ namespace Nif if (mData.empty() || mData->mVertices.empty()) return nullptr; + std::vector*> triangleLists; + std::vector>*> stripsLists; auto data = static_cast(mData.getPtr()); - if (data->mNumTriangles == 0 || data->mTriangles.empty()) - return nullptr; + const Nif::NiSkinPartition* partitions = nullptr; + if (!mSkin.empty()) + partitions = mSkin->getPartitions(); + if (partitions) + { + triangleLists.reserve(partitions->mPartitions.size()); + stripsLists.reserve(partitions->mPartitions.size()); + for (auto& partition : partitions->mPartitions) + { + triangleLists.push_back(&partition.mTrueTriangles); + stripsLists.push_back(&partition.mTrueStrips); + } + } + else if (data->mNumTriangles != 0) + triangleLists.push_back(&data->mTriangles); + + // This makes a perhaps dangerous assumption that NiSkinPartition will never have more than 65536 triangles. auto mesh = std::make_unique(); triBasedGeomToBtTriangleMesh(*mesh, *data); - const std::vector& triangles = data->mTriangles; - for (std::size_t i = 0; i < triangles.size(); i += 3) - mesh->addTriangleIndices(triangles[i + 0], triangles[i + 1], triangles[i + 2]); + for (const auto triangles : triangleLists) + trianglesToBtTriangleMesh(*mesh, *triangles); + for (const auto strips : stripsLists) + stripsToBtTriangleMesh(*mesh, *strips); if (mesh->getNumTriangles() == 0) return nullptr; @@ -267,33 +316,32 @@ namespace Nif if (mData.empty() || mData->mVertices.empty()) return nullptr; + std::vector*> triangleLists; + std::vector>*> stripsLists; auto data = static_cast(mData.getPtr()); - if (data->mNumTriangles == 0 || data->mStrips.empty()) - return nullptr; + const Nif::NiSkinPartition* partitions = nullptr; + if (!mSkin.empty()) + partitions = mSkin->getPartitions(); + + if (partitions) + { + triangleLists.reserve(partitions->mPartitions.size()); + stripsLists.reserve(partitions->mPartitions.size()); + for (auto& partition : partitions->mPartitions) + { + triangleLists.push_back(&partition.mTrueTriangles); + stripsLists.push_back(&partition.mTrueStrips); + } + } + else if (data->mNumTriangles != 0) + stripsLists.push_back(&data->mStrips); auto mesh = std::make_unique(); triBasedGeomToBtTriangleMesh(*mesh, *data); - for (const std::vector& strip : data->mStrips) - { - if (strip.size() < 3) - continue; - - unsigned short a; - unsigned short b = strip[0]; - unsigned short c = strip[1]; - for (size_t i = 2; i < strip.size(); i++) - { - a = b; - b = c; - c = strip[i]; - if (a == b || b == c || a == c) - continue; - if (i % 2 == 0) - mesh->addTriangleIndices(a, b, c); - else - mesh->addTriangleIndices(a, c, b); - } - } + for (const auto triangles : triangleLists) + trianglesToBtTriangleMesh(*mesh, *triangles); + for (const auto strips : stripsLists) + stripsToBtTriangleMesh(*mesh, *strips); if (mesh->getNumTriangles() == 0) return nullptr; diff --git a/components/nif/property.hpp b/components/nif/property.hpp index 2506633867..fbc7e8294c 100644 --- a/components/nif/property.hpp +++ b/components/nif/property.hpp @@ -108,6 +108,8 @@ namespace Nif enum BSShaderFlags1 { BSSFlag1_Specular = 0x00000001, + BSSFlag1_Refraction = 0x00008000, + BSSFlag1_FireRefraction = 0x00010000, BSSFlag1_Decal = 0x04000000, BSSFlag1_DepthTest = 0x80000000, }; @@ -148,6 +150,8 @@ namespace Nif bool decal() const { return mShaderFlags1 & BSSFlag1_Decal; } bool depthTest() const { return mShaderFlags1 & BSSFlag1_DepthTest; } bool depthWrite() const { return mShaderFlags2 & BSSFlag2_DepthWrite; } + bool refraction() const { return mShaderFlags1 & BSSFlag1_Refraction; } + bool fireRefraction() const { return mShaderFlags1 & BSSFlag1_FireRefraction; } }; struct BSShaderLightingProperty : BSShaderProperty @@ -461,11 +465,22 @@ namespace Nif struct NiFogProperty : NiProperty { + enum Flags : uint16_t + { + Enabled = 0x02, + Radial = 0x08, + VertexAlpha = 0x10, + }; + uint16_t mFlags; float mFogDepth; osg::Vec3f mColour; void read(NIFStream* nif) override; + + bool enabled() const { return mFlags & Flags::Enabled; } + bool radial() const { return mFlags & Flags::Radial; } + bool vertexAlpha() const { return mFlags & Flags::VertexAlpha; } }; struct NiMaterialProperty : NiProperty diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index 96dff80004..0737d0a165 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -250,11 +250,16 @@ namespace NifBullet const Nif::Parent currentParent{ *ninode, parent }; for (const auto& child : ninode->mChildren) { - if (child.empty()) - continue; - - assert(std::find(child->mParents.begin(), child->mParents.end(), ninode) != child->mParents.end()); - handleNode(child.get(), ¤tParent, args); + if (!child.empty()) + { + assert(std::find(child->mParents.begin(), child->mParents.end(), ninode) != child->mParents.end()); + handleNode(child.get(), ¤tParent, args); + } + // For NiSwitchNodes and NiFltAnimationNodes, only use the first child + // TODO: must synchronize with the rendering scene graph somehow + // Doing this for NiLODNodes is unsafe (the first level might not be the closest) + if (node.recType == Nif::RC_NiSwitchNode || node.recType == Nif::RC_NiFltAnimationNode) + break; } } } @@ -272,7 +277,6 @@ namespace NifBullet if (!niGeometry.mSkin.empty()) args.mAnimated = false; - // TODO: handle NiSkinPartition std::unique_ptr childShape = niGeometry.getCollisionShape(); if (childShape == nullptr) @@ -281,7 +285,20 @@ namespace NifBullet osg::Matrixf transform = niGeometry.mTransform.toMatrix(); for (const Nif::Parent* parent = nodeParent; parent != nullptr; parent = parent->mParent) transform *= parent->mNiNode.mTransform.toMatrix(); - childShape->setLocalScaling(Misc::Convert::toBullet(transform.getScale())); + + if (childShape->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE) + { + auto scaledShape = std::make_unique( + static_cast(childShape.get()), Misc::Convert::toBullet(transform.getScale())); + std::ignore = childShape.release(); + + childShape = std::move(scaledShape); + } + else + { + childShape->setLocalScaling(Misc::Convert::toBullet(transform.getScale())); + } + transform.orthoNormalize(transform); btTransform trans; diff --git a/components/nifosg/controller.cpp b/components/nifosg/controller.cpp index 54e9e2bb16..60f2e9c355 100644 --- a/components/nifosg/controller.cpp +++ b/components/nifosg/controller.cpp @@ -57,7 +57,13 @@ namespace NifOsg } case Nif::NiTimeController::ExtrapolationMode::Constant: default: - return std::clamp(time, mStartTime, mStopTime); + { + if (time < mStartTime) + return mStartTime; + if (time > mStopTime) + return mStopTime; + return time; + } } } @@ -442,7 +448,7 @@ namespace NifOsg } } - MaterialColorController::MaterialColorController() {} + MaterialColorController::MaterialColorController() = default; MaterialColorController::MaterialColorController( const Nif::NiMaterialColorController* ctrl, const osg::Material* baseMaterial) diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp index df6fdb2a24..51cf1cd428 100644 --- a/components/nifosg/controller.hpp +++ b/components/nifosg/controller.hpp @@ -347,7 +347,9 @@ namespace NifOsg private: Vec3Interpolator mData; - Nif::NiMaterialColorController::TargetColor mTargetColor; + Nif::NiMaterialColorController::TargetColor mTargetColor{ + Nif::NiMaterialColorController::TargetColor::Ambient + }; osg::ref_ptr mBaseMaterial; }; diff --git a/components/nifosg/fog.cpp b/components/nifosg/fog.cpp new file mode 100644 index 0000000000..497193ec42 --- /dev/null +++ b/components/nifosg/fog.cpp @@ -0,0 +1,31 @@ +#include "fog.hpp" + +#include +#include + +namespace NifOsg +{ + + Fog::Fog() + : osg::Fog() + { + } + + Fog::Fog(const Fog& copy, const osg::CopyOp& copyop) + : osg::Fog(copy, copyop) + , mDepth(copy.mDepth) + { + } + + void Fog::apply(osg::State& state) const + { + osg::Fog::apply(state); +#ifdef OSG_GL_FIXED_FUNCTION_AVAILABLE + float fov, aspect, near, far; + state.getProjectionMatrix().getPerspective(fov, aspect, near, far); + glFogf(GL_FOG_START, near * mDepth + far * (1.f - mDepth)); + glFogf(GL_FOG_END, far); +#endif + } + +} diff --git a/components/nifosg/fog.hpp b/components/nifosg/fog.hpp new file mode 100644 index 0000000000..5c49392a24 --- /dev/null +++ b/components/nifosg/fog.hpp @@ -0,0 +1,29 @@ +#ifndef OPENMW_COMPONENTS_NIFOSG_FOG_H +#define OPENMW_COMPONENTS_NIFOSG_FOG_H + +#include + +namespace NifOsg +{ + + // osg::Fog-based wrapper for NiFogProperty that autocalculates the fog start and end distance. + class Fog : public osg::Fog + { + public: + Fog(); + Fog(const Fog& copy, const osg::CopyOp& copyop); + + META_StateAttribute(NifOsg, Fog, FOG) + + void setDepth(float depth) { mDepth = depth; } + float getDepth() const { return mDepth; } + + void apply(osg::State& state) const override; + + private: + float mDepth{ 1.f }; + }; + +} + +#endif diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 17c608f2d4..2f7574d68b 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -56,6 +56,7 @@ #include #include +#include "fog.hpp" #include "matrixtransform.hpp" #include "particle.hpp" @@ -1381,17 +1382,7 @@ namespace NifOsg if (!niGeometry->mSkin.empty()) { const Nif::NiSkinInstance* skin = niGeometry->mSkin.getPtr(); - const Nif::NiSkinData* data = nullptr; - const Nif::NiSkinPartition* partitions = nullptr; - if (!skin->mData.empty()) - { - data = skin->mData.getPtr(); - if (!data->mPartitions.empty()) - partitions = data->mPartitions.getPtr(); - } - if (!partitions && !skin->mPartitions.empty()) - partitions = skin->mPartitions.getPtr(); - + const Nif::NiSkinPartition* partitions = skin->getPartitions(); hasPartitions = partitions != nullptr; if (hasPartitions) { @@ -1515,24 +1506,24 @@ namespace NifOsg osg::ref_ptr rig(new SceneUtil::RigGeometry); rig->setSourceGeometry(geom); - // Assign bone weights - osg::ref_ptr map(new SceneUtil::RigGeometry::InfluenceMap); - const Nif::NiSkinInstance* skin = niGeometry->mSkin.getPtr(); const Nif::NiSkinData* data = skin->mData.getPtr(); const Nif::NiAVObjectList& bones = skin->mBones; + + // Assign bone weights + std::vector boneInfo; + std::vector influences; + boneInfo.resize(bones.size()); + influences.resize(bones.size()); for (std::size_t i = 0; i < bones.size(); ++i) { - std::string boneName = Misc::StringUtils::lowerCase(bones[i].getPtr()->mName); - - SceneUtil::RigGeometry::BoneInfluence influence; - influence.mWeights = data->mBones[i].mWeights; - influence.mInvBindMatrix = data->mBones[i].mTransform.toMatrix(); - influence.mBoundSphere = data->mBones[i].mBoundSphere; - - map->mData.emplace_back(boneName, influence); + boneInfo[i].mName = Misc::StringUtils::lowerCase(bones[i].getPtr()->mName); + boneInfo[i].mInvBindMatrix = data->mBones[i].mTransform.toMatrix(); + boneInfo[i].mBoundSphere = data->mBones[i].mBoundSphere; + influences[i] = data->mBones[i].mWeights; } - rig->setInfluenceMap(map); + rig->setBoneInfo(std::move(boneInfo)); + rig->setInfluences(influences); drawable = rig; } @@ -1671,29 +1662,29 @@ namespace NifOsg osg::ref_ptr rig(new SceneUtil::RigGeometry); rig->setSourceGeometry(geometry); - osg::ref_ptr map(new SceneUtil::RigGeometry::InfluenceMap); - - auto skin = static_cast(bsTriShape->mSkin.getPtr()); + const Nif::BSSkinInstance* skin = static_cast(bsTriShape->mSkin.getPtr()); const Nif::BSSkinBoneData* data = skin->mData.getPtr(); const Nif::NiAVObjectList& bones = skin->mBones; - std::vector> vertWeights(data->mBones.size()); - for (size_t i = 0; i < vertices.size(); i++) - for (int j = 0; j < 4; j++) - vertWeights[bsTriShape->mVertData[i].mBoneIndices[j]].emplace_back( - i, halfToFloat(bsTriShape->mVertData[i].mBoneWeights[j])); + std::vector boneInfo; + std::vector influences; + boneInfo.resize(bones.size()); + influences.resize(vertices.size()); for (std::size_t i = 0; i < bones.size(); ++i) { - std::string boneName = Misc::StringUtils::lowerCase(bones[i].getPtr()->mName); - - SceneUtil::RigGeometry::BoneInfluence influence; - influence.mWeights = vertWeights[i]; - influence.mInvBindMatrix = data->mBones[i].mTransform.toMatrix(); - influence.mBoundSphere = data->mBones[i].mBoundSphere; - - map->mData.emplace_back(boneName, influence); + boneInfo[i].mName = Misc::StringUtils::lowerCase(bones[i].getPtr()->mName); + boneInfo[i].mInvBindMatrix = data->mBones[i].mTransform.toMatrix(); + boneInfo[i].mBoundSphere = data->mBones[i].mBoundSphere; } - rig->setInfluenceMap(map); + + for (size_t i = 0; i < vertices.size(); i++) + { + const Nif::BSVertexData& vertData = bsTriShape->mVertData[i]; + for (int j = 0; j < 4; j++) + influences[i].emplace_back(vertData.mBoneIndices[j], halfToFloat(vertData.mBoneWeights[j])); + } + rig->setBoneInfo(std::move(boneInfo)); + rig->setInfluences(influences); drawable = rig; } @@ -2363,6 +2354,7 @@ namespace NifOsg osg::StateSet* stateset = node->getOrCreateStateSet(); handleTextureProperty( texprop, node->getName(), stateset, composite, imageManager, boundTextures, animflags); + node->setUserValue("applyMode", static_cast(texprop->mApplyMode)); break; } case Nif::RC_BSShaderPPLightingProperty: @@ -2379,6 +2371,8 @@ namespace NifOsg textureSet, texprop->mClamp, node->getName(), stateset, imageManager, boundTextures); } handleTextureControllers(texprop, composite, imageManager, stateset, animflags); + if (texprop->refraction()) + SceneUtil::setupDistortion(*node, texprop->mRefraction.mStrength); break; } case Nif::RC_BSShaderNoLightingProperty: @@ -2436,6 +2430,8 @@ namespace NifOsg if (texprop->treeAnim()) stateset->addUniform(new osg::Uniform("useTreeAnim", true)); handleDepthFlags(stateset, texprop->depthTest(), texprop->depthWrite()); + if (texprop->refraction()) + SceneUtil::setupDistortion(*node, texprop->mRefractionStrength); break; } case Nif::RC_BSEffectShaderProperty: @@ -2494,10 +2490,35 @@ namespace NifOsg handleDepthFlags(stateset, texprop->depthTest(), texprop->depthWrite()); break; } + case Nif::RC_NiFogProperty: + { + const Nif::NiFogProperty* fogprop = static_cast(property); + osg::StateSet* stateset = node->getOrCreateStateSet(); + // Vertex alpha mode appears to be broken + if (!fogprop->vertexAlpha() && fogprop->enabled()) + { + osg::ref_ptr fog = new NifOsg::Fog; + fog->setMode(osg::Fog::LINEAR); + fog->setColor(osg::Vec4f(fogprop->mColour, 1.f)); + fog->setDepth(fogprop->mFogDepth); + stateset->setAttributeAndModes(fog, osg::StateAttribute::ON); + // Intentionally ignoring radial fog flag + // We don't really want to override the global setting + } + else + { + osg::ref_ptr fog = new osg::Fog; + // Shaders don't respect glDisable(GL_FOG) + fog->setMode(osg::Fog::LINEAR); + fog->setStart(10000000); + fog->setEnd(10000000); + stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); + } + break; + } // unused by mw case Nif::RC_NiShadeProperty: case Nif::RC_NiDitherProperty: - case Nif::RC_NiFogProperty: { break; } @@ -2578,7 +2599,10 @@ namespace NifOsg emissiveMult = matprop->mEmissiveMult; mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(matprop->mSpecular, 1.f)); - mat->setShininess(osg::Material::FRONT_AND_BACK, matprop->mGlossiness); + // NIFs may provide specular exponents way above OpenGL's limit. + // They can't be used properly, but we don't need OSG to constantly harass us about it. + float glossiness = std::clamp(matprop->mGlossiness, 0.f, 128.f); + mat->setShininess(osg::Material::FRONT_AND_BACK, glossiness); if (!matprop->mController.empty()) { @@ -2693,7 +2717,8 @@ namespace NifOsg mat->setAlpha(osg::Material::FRONT_AND_BACK, shaderprop->mAlpha); mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(shaderprop->mEmissive, 1.f)); mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(shaderprop->mSpecular, 1.f)); - mat->setShininess(osg::Material::FRONT_AND_BACK, shaderprop->mGlossiness); + float glossiness = std::clamp(shaderprop->mGlossiness, 0.f, 128.f); + mat->setShininess(osg::Material::FRONT_AND_BACK, glossiness); emissiveMult = shaderprop->mEmissiveMult; specStrength = shaderprop->mSpecStrength; specEnabled = shaderprop->specular(); diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp index 73e23eb9f9..9489076acb 100644 --- a/components/process/processinvoker.cpp +++ b/components/process/processinvoker.cpp @@ -76,9 +76,9 @@ bool Process::ProcessInvoker::startProcess(const QString& name, const QStringLis msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("

Could not find %1

\ -

The application is not found.

\ -

Please make sure OpenMW is installed correctly and try again.

") + tr("

Could not find %1

" + "

The application is not found.

" + "

Please make sure OpenMW is installed correctly and try again.

") .arg(info.fileName())); msgBox.exec(); return false; @@ -91,9 +91,9 @@ bool Process::ProcessInvoker::startProcess(const QString& name, const QStringLis msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("

Could not start %1

\ -

The application is not executable.

\ -

Please make sure you have the right permissions and try again.

") + tr("

Could not start %1

" + "

The application is not executable.

" + "

Please make sure you have the right permissions and try again.

") .arg(info.fileName())); msgBox.exec(); return false; @@ -109,9 +109,9 @@ bool Process::ProcessInvoker::startProcess(const QString& name, const QStringLis msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("

Could not start %1

\ -

An error occurred while starting %1.

\ -

Press \"Show Details...\" for more information.

") + tr("

Could not start %1

" + "

An error occurred while starting %1.

" + "

Press \"Show Details...\" for more information.

") .arg(info.fileName())); msgBox.setDetailedText(mProcess->errorString()); msgBox.exec(); @@ -168,9 +168,9 @@ void Process::ProcessInvoker::processError(QProcess::ProcessError error) msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("

Executable %1 returned an error

\ -

An error occurred while running %1.

\ -

Press \"Show Details...\" for more information.

") + tr("

Executable %1 returned an error

" + "

An error occurred while running %1.

" + "

Press \"Show Details...\" for more information.

") .arg(mName)); msgBox.setDetailedText(mProcess->errorString()); msgBox.exec(); @@ -191,9 +191,9 @@ void Process::ProcessInvoker::processFinished(int exitCode, QProcess::ExitStatus msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("

Executable %1 returned an error

\ -

An error occurred while running %1.

\ -

Press \"Show Details...\" for more information.

") + tr("

Executable %1 returned an error

" + "

An error occurred while running %1.

" + "

Press \"Show Details...\" for more information.

") .arg(mName)); msgBox.setDetailedText(error); msgBox.exec(); diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp index cc0a63359b..360b92ffc0 100644 --- a/components/resource/bulletshape.cpp +++ b/components/resource/bulletshape.cpp @@ -32,11 +32,11 @@ namespace Resource return newShape; } - if (shape->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE) + if (shape->getShapeType() == SCALED_TRIANGLE_MESH_SHAPE_PROXYTYPE) { - const btBvhTriangleMeshShape* trishape = static_cast(shape); + const btScaledBvhTriangleMeshShape* trishape = static_cast(shape); return CollisionShapePtr(new btScaledBvhTriangleMeshShape( - const_cast(trishape), btVector3(1.f, 1.f, 1.f))); + const_cast(trishape->getChildShape()), trishape->getLocalScaling())); } if (shape->getShapeType() == BOX_SHAPE_PROXYTYPE) diff --git a/components/resource/bulletshape.hpp b/components/resource/bulletshape.hpp index 9610fde49f..0a1b98bf7c 100644 --- a/components/resource/bulletshape.hpp +++ b/components/resource/bulletshape.hpp @@ -10,6 +10,7 @@ #include #include +#include class btCollisionShape; @@ -49,8 +50,8 @@ namespace Resource // collision box for creatures. For now, use one file <-> one resource for simplicity. CollisionBox mCollisionBox; - // Stores animated collision shapes. If any collision nodes in the NIF are animated, then mCollisionShape - // will be a btCompoundShape (which consists of one or more child shapes). + // Stores animated collision shapes. + // mCollisionShape is a btCompoundShape (which consists of one or more child shapes). // In this map, for each animated collision shape, // we store the node's record index mapped to the child index of the shape in the btCompoundShape. std::map mAnimatedShapes; @@ -61,6 +62,7 @@ namespace Resource VisualCollisionType mVisualCollisionType = VisualCollisionType::None; BulletShape() = default; + // Note this is always a shallow copy and the copy will not autodelete underlying vertex data BulletShape(const BulletShape& other, const osg::CopyOp& copyOp = osg::CopyOp()); META_Object(Resource, BulletShape) @@ -70,7 +72,7 @@ namespace Resource bool isAnimated() const { return !mAnimatedShapes.empty(); } }; - // An instance of a BulletShape that may have its own unique scaling set on the mCollisionShape. + // An instance of a BulletShape that may have its own unique scaling set on collision shapes. // Vertex data is shallow-copied where possible. A ref_ptr to the original shape is held to keep vertex pointers // intact. class BulletShapeInstance : public BulletShape @@ -102,6 +104,17 @@ namespace Resource } }; + // btScaledBvhTriangleMeshShape that auto-deletes the child shape + struct ScaledTriangleMeshShape : public btScaledBvhTriangleMeshShape + { + ScaledTriangleMeshShape(btBvhTriangleMeshShape* childShape, const btVector3& localScaling) + : btScaledBvhTriangleMeshShape(childShape, localScaling) + { + } + + ~ScaledTriangleMeshShape() override { delete getChildShape(); } + }; + } #endif diff --git a/components/resource/objectcache.hpp b/components/resource/objectcache.hpp index 881729ffc4..dffa0e9fdb 100644 --- a/components/resource/objectcache.hpp +++ b/components/resource/objectcache.hpp @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -48,48 +49,25 @@ namespace Resource { } - /** For each object in the cache which has an reference count greater than 1 - * (and therefore referenced by elsewhere in the application) set the time stamp - * for that object in the cache to specified time. - * This would typically be called once per frame by applications which are doing database paging, - * and need to prune objects that are no longer required. - * The time used should be taken from the FrameStamp::getReferenceTime().*/ - void updateTimeStampOfObjectsInCacheWithExternalReferences(double referenceTime) - { - // look for objects with external references and update their time stamp. - std::lock_guard lock(_objectCacheMutex); - for (typename ObjectCacheMap::iterator itr = _objectCache.begin(); itr != _objectCache.end(); ++itr) - { - // If ref count is greater than 1, the object has an external reference. - // If the timestamp is yet to be initialized, it needs to be updated too. - if ((itr->second.mValue != nullptr && itr->second.mValue->referenceCount() > 1) - || itr->second.mLastUsage == 0.0) - itr->second.mLastUsage = referenceTime; - } - } - - /** Removed object in the cache which have a time stamp at or before the specified expiry time. - * This would typically be called once per frame by applications which are doing database paging, - * and need to prune objects that are no longer required, and called after the a called - * after the call to updateTimeStampOfObjectsInCacheWithExternalReferences(expirtyTime).*/ - void removeExpiredObjectsInCache(double expiryTime) + // Update last usage timestamp using referenceTime for each cache time if they are not nullptr and referenced + // from somewhere else. Remove items with last usage > expiryTime. Note: last usage might be updated from other + // places so nullptr or not references elsewhere items are not always removed. + void update(double referenceTime, double expiryDelay) { std::vector> objectsToRemove; { - std::lock_guard lock(_objectCacheMutex); - // Remove expired entries from object cache - typename ObjectCacheMap::iterator oitr = _objectCache.begin(); - while (oitr != _objectCache.end()) - { - if (oitr->second.mLastUsage <= expiryTime) - { - if (oitr->second.mValue != nullptr) - objectsToRemove.push_back(std::move(oitr->second.mValue)); - _objectCache.erase(oitr++); - } - else - ++oitr; - } + const double expiryTime = referenceTime - expiryDelay; + std::lock_guard lock(mMutex); + std::erase_if(mItems, [&](auto& v) { + Item& item = v.second; + if ((item.mValue != nullptr && item.mValue->referenceCount() > 1) || item.mLastUsage == 0) + item.mLastUsage = referenceTime; + if (item.mLastUsage > expiryTime) + return false; + if (item.mValue != nullptr) + objectsToRemove.push_back(std::move(item.mValue)); + return true; + }); } // note, actual unref happens outside of the lock objectsToRemove.clear(); @@ -98,52 +76,57 @@ namespace Resource /** Remove all objects in the cache regardless of having external references or expiry times.*/ void clear() { - std::lock_guard lock(_objectCacheMutex); - _objectCache.clear(); + std::lock_guard lock(mMutex); + mItems.clear(); } /** Add a key,object,timestamp triple to the Registry::ObjectCache.*/ - void addEntryToObjectCache(const KeyType& key, osg::Object* object, double timestamp = 0.0) + template + void addEntryToObjectCache(K&& key, osg::Object* object, double timestamp = 0.0) { - std::lock_guard lock(_objectCacheMutex); - _objectCache[key] = Item{ object, timestamp }; + std::lock_guard lock(mMutex); + const auto it = mItems.find(key); + if (it == mItems.end()) + mItems.emplace_hint(it, std::forward(key), Item{ object, timestamp }); + else + it->second = Item{ object, timestamp }; } /** Remove Object from cache.*/ - void removeFromObjectCache(const KeyType& key) + void removeFromObjectCache(const auto& key) { - std::lock_guard lock(_objectCacheMutex); - typename ObjectCacheMap::iterator itr = _objectCache.find(key); - if (itr != _objectCache.end()) - _objectCache.erase(itr); + std::lock_guard lock(mMutex); + const auto itr = mItems.find(key); + if (itr != mItems.end()) + mItems.erase(itr); } /** Get an ref_ptr from the object cache*/ - osg::ref_ptr getRefFromObjectCache(const KeyType& key) + osg::ref_ptr getRefFromObjectCache(const auto& key) { - std::lock_guard lock(_objectCacheMutex); - typename ObjectCacheMap::iterator itr = _objectCache.find(key); - if (itr != _objectCache.end()) + std::lock_guard lock(mMutex); + const auto itr = mItems.find(key); + if (itr != mItems.end()) return itr->second.mValue; else return nullptr; } - std::optional> getRefFromObjectCacheOrNone(const KeyType& key) + std::optional> getRefFromObjectCacheOrNone(const auto& key) { - const std::lock_guard lock(_objectCacheMutex); - const auto it = _objectCache.find(key); - if (it == _objectCache.end()) + const std::lock_guard lock(mMutex); + const auto it = mItems.find(key); + if (it == mItems.end()) return std::nullopt; return it->second.mValue; } /** Check if an object is in the cache, and if it is, update its usage time stamp. */ - bool checkInObjectCache(const KeyType& key, double timeStamp) + bool checkInObjectCache(const auto& key, double timeStamp) { - std::lock_guard lock(_objectCacheMutex); - typename ObjectCacheMap::iterator itr = _objectCache.find(key); - if (itr != _objectCache.end()) + std::lock_guard lock(mMutex); + const auto itr = mItems.find(key); + if (itr != mItems.end()) { itr->second.mLastUsage = timeStamp; return true; @@ -155,51 +138,43 @@ namespace Resource /** call releaseGLObjects on all objects attached to the object cache.*/ void releaseGLObjects(osg::State* state) { - std::lock_guard lock(_objectCacheMutex); - for (typename ObjectCacheMap::iterator itr = _objectCache.begin(); itr != _objectCache.end(); ++itr) - { - osg::Object* object = itr->second.mValue.get(); - object->releaseGLObjects(state); - } + std::lock_guard lock(mMutex); + for (const auto& [k, v] : mItems) + v.mValue->releaseGLObjects(state); } /** call node->accept(nv); for all nodes in the objectCache. */ void accept(osg::NodeVisitor& nv) { - std::lock_guard lock(_objectCacheMutex); - for (typename ObjectCacheMap::iterator itr = _objectCache.begin(); itr != _objectCache.end(); ++itr) - { - if (osg::Object* object = itr->second.mValue.get()) - { - osg::Node* node = dynamic_cast(object); - if (node) + std::lock_guard lock(mMutex); + for (const auto& [k, v] : mItems) + if (osg::Object* const object = v.mValue.get()) + if (osg::Node* const node = dynamic_cast(object)) node->accept(nv); - } - } } /** call operator()(KeyType, osg::Object*) for each object in the cache. */ template - void call(Functor& f) + void call(Functor&& f) { - std::lock_guard lock(_objectCacheMutex); - for (typename ObjectCacheMap::iterator it = _objectCache.begin(); it != _objectCache.end(); ++it) - f(it->first, it->second.mValue.get()); + std::lock_guard lock(mMutex); + for (const auto& [k, v] : mItems) + f(k, v.mValue.get()); } /** Get the number of objects in the cache. */ unsigned int getCacheSize() const { - std::lock_guard lock(_objectCacheMutex); - return _objectCache.size(); + std::lock_guard lock(mMutex); + return mItems.size(); } template std::optional>> lowerBound(K&& key) { - const std::lock_guard lock(_objectCacheMutex); - const auto it = _objectCache.lower_bound(std::forward(key)); - if (it == _objectCache.end()) + const std::lock_guard lock(mMutex); + const auto it = mItems.lower_bound(std::forward(key)); + if (it == mItems.end()) return std::nullopt; return std::pair(it->first, it->second.mValue); } @@ -211,12 +186,8 @@ namespace Resource double mLastUsage; }; - virtual ~GenericObjectCache() {} - - using ObjectCacheMap = std::map>; - - ObjectCacheMap _objectCache; - mutable std::mutex _objectCacheMutex; + std::map> mItems; + mutable std::mutex mMutex; }; class ObjectCache : public GenericObjectCache diff --git a/components/resource/resourcemanager.hpp b/components/resource/resourcemanager.hpp index 55e4e142b5..b2427c308a 100644 --- a/components/resource/resourcemanager.hpp +++ b/components/resource/resourcemanager.hpp @@ -49,11 +49,7 @@ namespace Resource virtual ~GenericResourceManager() = default; /// Clear cache entries that have not been referenced for longer than expiryDelay. - void updateCache(double referenceTime) override - { - mCache->updateTimeStampOfObjectsInCacheWithExternalReferences(referenceTime); - mCache->removeExpiredObjectsInCache(referenceTime - mExpiryDelay); - } + void updateCache(double referenceTime) override { mCache->update(referenceTime, mExpiryDelay); } /// Clear all cache entries. void clearCache() override { mCache->clear(); } diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index b3c9eee5e7..25abcfd0d8 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -649,6 +649,7 @@ namespace Resource node->getOrCreateStateSet()->addUniform(new osg::Uniform("specStrength", 1.f)); node->getOrCreateStateSet()->addUniform(new osg::Uniform("envMapColor", osg::Vec4f(1, 1, 1, 1))); node->getOrCreateStateSet()->addUniform(new osg::Uniform("useFalloff", false)); + node->getOrCreateStateSet()->addUniform(new osg::Uniform("distortionStrength", 0.f)); } node->setUserValue(Misc::OsgUserValues::sFileHash, diff --git a/components/sceneutil/extradata.cpp b/components/sceneutil/extradata.cpp index 720e032a61..bd82e9abba 100644 --- a/components/sceneutil/extradata.cpp +++ b/components/sceneutil/extradata.cpp @@ -29,6 +29,19 @@ namespace SceneUtil node.setUserValue(Misc::OsgUserValues::sXSoftEffect, true); } + void setupDistortion(osg::Node& node, float distortionStrength) + { + static const osg::ref_ptr depth + = new SceneUtil::AutoDepth(osg::Depth::ALWAYS, 0, 1, false); + + osg::StateSet* stateset = node.getOrCreateStateSet(); + + stateset->setRenderBinDetails(14, "Distortion", osg::StateSet::OVERRIDE_RENDERBIN_DETAILS); + stateset->addUniform(new osg::Uniform("distortionStrength", distortionStrength)); + + stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); + } + void ProcessExtraDataVisitor::apply(osg::Node& node) { if (!mSceneMgr->getSoftParticles()) @@ -54,6 +67,12 @@ namespace SceneUtil setupSoftEffect(node, size, falloff, falloffDepth); } + else if (key == "distortion") + { + auto strength = it.second["strength"].as(0.1f); + + setupDistortion(node, strength); + } } node.setUserValue(Misc::OsgUserValues::sExtraData, std::string{}); diff --git a/components/sceneutil/extradata.hpp b/components/sceneutil/extradata.hpp index 9b1563b78a..7054ac91b3 100644 --- a/components/sceneutil/extradata.hpp +++ b/components/sceneutil/extradata.hpp @@ -16,6 +16,7 @@ namespace osg namespace SceneUtil { void setupSoftEffect(osg::Node& node, float size, bool falloff, float falloffDepth); + void setupDistortion(osg::Node& node, float distortionStrength); class ProcessExtraDataVisitor : public osg::NodeVisitor { diff --git a/components/sceneutil/riggeometry.cpp b/components/sceneutil/riggeometry.cpp index 1572fab338..92e316d412 100644 --- a/components/sceneutil/riggeometry.cpp +++ b/components/sceneutil/riggeometry.cpp @@ -301,23 +301,29 @@ namespace SceneUtil } } - void RigGeometry::setInfluenceMap(osg::ref_ptr influenceMap) + void RigGeometry::setBoneInfo(std::vector&& bones) { - mData = new InfluenceData; - mData->mBones.reserve(influenceMap->mData.size()); + if (!mData) + mData = new InfluenceData; - std::unordered_map> vertexToInfluences; + mData->mBones = std::move(bones); + } + + void RigGeometry::setInfluences(const std::vector& influences) + { + if (!mData) + mData = new InfluenceData; + + std::unordered_map vertexToInfluences; size_t index = 0; - for (const auto& [boneName, bi] : influenceMap->mData) + for (const auto& influence : influences) { - mData->mBones.push_back({ boneName, bi.mBoundSphere, bi.mInvBindMatrix }); - - for (const auto& [vertex, weight] : bi.mWeights) + for (const auto& [vertex, weight] : influence) vertexToInfluences[vertex].emplace_back(index, weight); index++; } - std::map, VertexList> influencesToVertices; + std::map influencesToVertices; for (const auto& [vertex, weights] : vertexToInfluences) influencesToVertices[weights].emplace_back(vertex); @@ -325,6 +331,19 @@ namespace SceneUtil mData->mInfluences.assign(influencesToVertices.begin(), influencesToVertices.end()); } + void RigGeometry::setInfluences(const std::vector& influences) + { + if (!mData) + mData = new InfluenceData; + + std::map influencesToVertices; + for (size_t i = 0; i < influences.size(); i++) + influencesToVertices[influences[i]].emplace_back(i); + + mData->mInfluences.reserve(influencesToVertices.size()); + mData->mInfluences.assign(influencesToVertices.begin(), influencesToVertices.end()); + } + void RigGeometry::accept(osg::NodeVisitor& nv) { if (!nv.validNodeMask(*this)) diff --git a/components/sceneutil/riggeometry.hpp b/components/sceneutil/riggeometry.hpp index d1c077288d..64ea1e2519 100644 --- a/components/sceneutil/riggeometry.hpp +++ b/components/sceneutil/riggeometry.hpp @@ -36,21 +36,23 @@ namespace SceneUtil // static parts of the model. void compileGLObjects(osg::RenderInfo& renderInfo) const override {} - // TODO: Make InfluenceMap more similar to InfluenceData - struct BoneInfluence + struct BoneInfo { - osg::Matrixf mInvBindMatrix; + std::string mName; osg::BoundingSpheref mBoundSphere; - // - std::vector> mWeights; + osg::Matrixf mInvBindMatrix; }; - struct InfluenceMap : public osg::Referenced - { - std::vector> mData; - }; + using VertexWeight = std::pair; + using VertexWeights = std::vector; + using BoneWeight = std::pair; + using BoneWeights = std::vector; - void setInfluenceMap(osg::ref_ptr influenceMap); + void setBoneInfo(std::vector&& bones); + // Convert influences in vertex and weight list per bone format + void setInfluences(const std::vector& influences); + // Convert influences in bone and weight list per vertex format + void setInfluences(const std::vector& influences); /// Initialize this geometry from the source geometry. /// @note The source geometry will not be modified. @@ -89,19 +91,11 @@ namespace SceneUtil osg::ref_ptr mGeomToSkelMatrix; - struct BoneInfo - { - std::string mName; - osg::BoundingSpheref mBoundSphere; - osg::Matrixf mInvBindMatrix; - }; - - using BoneWeight = std::pair; using VertexList = std::vector; struct InfluenceData : public osg::Referenced { std::vector mBones; - std::vector, VertexList>> mInfluences; + std::vector> mInfluences; }; osg::ref_ptr mData; std::vector mNodes; diff --git a/components/sceneutil/serialize.cpp b/components/sceneutil/serialize.cpp index 784dafafa5..8d8acacae4 100644 --- a/components/sceneutil/serialize.cpp +++ b/components/sceneutil/serialize.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -123,6 +124,19 @@ namespace SceneUtil } }; + class FogSerializer : public osgDB::ObjectWrapper + { + public: + FogSerializer() + : osgDB::ObjectWrapper( + createInstanceFunc, "NifOsg::Fog", "osg::Object osg::StateAttribute osg::Fog NifOsg::Fog") + { + addSerializer(new osgDB::PropByValSerializer( + "Depth", 1.f, &NifOsg::Fog::getDepth, &NifOsg::Fog::setDepth), + osgDB::BaseSerializer::RW_FLOAT); + } + }; + osgDB::ObjectWrapper* makeDummySerializer(const std::string& classname) { return new osgDB::ObjectWrapper(createInstanceFunc, classname, "osg::Object"); @@ -153,6 +167,7 @@ namespace SceneUtil mgr->addWrapper(new LightManagerSerializer); mgr->addWrapper(new CameraRelativeTransformSerializer); mgr->addWrapper(new MatrixTransformSerializer); + mgr->addWrapper(new FogSerializer); // Don't serialize Geometry data as we are more interested in the overall structure rather than tons of // vertex data that would make the file large and hard to read. diff --git a/components/settings/settings.hpp b/components/settings/settings.hpp index c061755bc1..bcdcbb16d1 100644 --- a/components/settings/settings.hpp +++ b/components/settings/settings.hpp @@ -77,6 +77,15 @@ namespace Settings static osg::Vec2f getVector2(std::string_view setting, std::string_view category); static osg::Vec3f getVector3(std::string_view setting, std::string_view category); + template + static T getOrDefault(std::string_view setting, std::string_view category, const T& defaultValue) + { + const auto key = std::make_pair(category, setting); + if (!mUserSettings.contains(key) && !mDefaultSettings.contains(key)) + return defaultValue; + return get(setting, category); + } + template static T get(std::string_view setting, std::string_view category) { diff --git a/components/settings/settingvalue.hpp b/components/settings/settingvalue.hpp index 2f239a4ebd..8183e8c1ac 100644 --- a/components/settings/settingvalue.hpp +++ b/components/settings/settingvalue.hpp @@ -335,6 +335,15 @@ namespace Settings { } + explicit SettingValue(Index& index, std::string_view category, std::string_view name, T&& defaultValue, + std::unique_ptr>&& sanitizer = nullptr) + : BaseSettingValue(getSettingValueType(), category, name, index) + , mSanitizer(std::move(sanitizer)) + , mDefaultValue(sanitize(defaultValue)) + , mValue(sanitize(Settings::Manager::getOrDefault(mName, mCategory, mDefaultValue))) + { + } + SettingValue(SettingValue&& other) : BaseSettingValue(std::move(other)) , mSanitizer(std::move(other.mSanitizer)) diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index 96e3d42f78..70464f571e 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -181,6 +181,7 @@ namespace Shader , mAlphaBlend(false) , mBlendFuncOverridden(false) , mAdditiveBlending(false) + , mDiffuseHeight(false) , mNormalHeight(false) , mTexStageRequiringTangents(-1) , mSoftParticles(false) @@ -350,7 +351,17 @@ namespace Shader normalMap = texture; } else if (texName == "diffuseMap") + { + int applyMode; + // Oblivion parallax + if (node.getUserValue("applyMode", applyMode) && applyMode == 4) + { + mRequirements.back().mShaderRequired = true; + mRequirements.back().mDiffuseHeight = true; + mRequirements.back().mTexStageRequiringTangents = unit; + } diffuseMap = texture; + } else if (texName == "specularMap") specularMap = texture; else if (texName == "bumpMap") @@ -615,6 +626,7 @@ namespace Shader addedState->addUniform("useDiffuseMapForShadowAlpha"); } + defineMap["diffuseParallax"] = reqs.mDiffuseHeight ? "1" : "0"; defineMap["parallax"] = reqs.mNormalHeight ? "1" : "0"; writableStateSet->addUniform(new osg::Uniform("colorMode", reqs.mColorMode)); diff --git a/components/shader/shadervisitor.hpp b/components/shader/shadervisitor.hpp index 66bd8c2a9d..a8e79ec995 100644 --- a/components/shader/shadervisitor.hpp +++ b/components/shader/shadervisitor.hpp @@ -108,6 +108,7 @@ namespace Shader bool mBlendFuncOverridden; bool mAdditiveBlending; + bool mDiffuseHeight; // true if diffuse map has height info in alpha channel bool mNormalHeight; // true if normal map has height info in alpha channel // -1 == no tangents required diff --git a/components/stereo/stereomanager.cpp b/components/stereo/stereomanager.cpp index a2eea0fda2..c3d7f1d320 100644 --- a/components/stereo/stereomanager.cpp +++ b/components/stereo/stereomanager.cpp @@ -315,8 +315,7 @@ namespace Stereo else { auto* ds = osg::DisplaySettings::instance().get(); - auto viewMatrix = mMainCamera->getViewMatrix(); - auto projectionMatrix = mMainCamera->getProjectionMatrix(); + const auto& projectionMatrix = mMainCamera->getProjectionMatrix(); auto s = ds->getEyeSeparation() * Constants::UnitsPerMeter; mViewOffsetMatrix[0] = osg::Matrixd(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, s, 0.0, 0.0, 1.0); diff --git a/components/terrain/compositemaprenderer.cpp b/components/terrain/compositemaprenderer.cpp index 5319820c83..adf58162e8 100644 --- a/components/terrain/compositemaprenderer.cpp +++ b/components/terrain/compositemaprenderer.cpp @@ -43,19 +43,18 @@ namespace Terrain mImmediateCompileSet.erase(node); mMutex.unlock(); - compile(*node, renderInfo, nullptr); + compile(*node, renderInfo); mMutex.lock(); } - double timeLeft = availableTime; - - while (!mCompileSet.empty() && timeLeft > 0) + const auto deadline = std::chrono::steady_clock::now() + std::chrono::duration(availableTime); + while (!mCompileSet.empty() && std::chrono::steady_clock::now() < deadline) { osg::ref_ptr node = *mCompileSet.begin(); mCompileSet.erase(node); mMutex.unlock(); - compile(*node, renderInfo, &timeLeft); + compile(*node, renderInfo); mMutex.lock(); if (node->mCompiled < node->mDrawables.size()) @@ -68,7 +67,7 @@ namespace Terrain mTimer.setStartTick(); } - void CompositeMapRenderer::compile(CompositeMap& compositeMap, osg::RenderInfo& renderInfo, double* timeLeft) const + void CompositeMapRenderer::compile(CompositeMap& compositeMap, osg::RenderInfo& renderInfo) const { // if there are no more external references we can assume the texture is no longer required if (compositeMap.mTexture->referenceCount() <= 1) @@ -124,15 +123,6 @@ namespace Terrain ++compositeMap.mCompiled; compositeMap.mDrawables[i] = nullptr; - - if (timeLeft) - { - *timeLeft -= timer.time_s(); - timer.setStartTick(); - - if (*timeLeft <= 0) - break; - } } if (compositeMap.mCompiled == compositeMap.mDrawables.size()) compositeMap.mDrawables = std::vector>(); diff --git a/components/terrain/compositemaprenderer.hpp b/components/terrain/compositemaprenderer.hpp index eeecec75dc..1e33c717ec 100644 --- a/components/terrain/compositemaprenderer.hpp +++ b/components/terrain/compositemaprenderer.hpp @@ -38,7 +38,7 @@ namespace Terrain void drawImplementation(osg::RenderInfo& renderInfo) const override; - void compile(CompositeMap& compositeMap, osg::RenderInfo& renderInfo, double* timeLeft) const; + void compile(CompositeMap& compositeMap, osg::RenderInfo& renderInfo) const; /// Set the available time in seconds for compiling (non-immediate) composite maps each frame void setMinimumTimeAvailableForCompile(double time); diff --git a/components/vfs/archive.hpp b/components/vfs/archive.hpp index e377e8c5b6..79c876b391 100644 --- a/components/vfs/archive.hpp +++ b/components/vfs/archive.hpp @@ -3,6 +3,7 @@ #include #include +#include #include @@ -19,16 +20,18 @@ namespace VFS virtual std::filesystem::path getPath() = 0; }; + using FileMap = std::map>; + class Archive { public: virtual ~Archive() = default; /// List all resources contained in this archive. - virtual void listResources(std::map& out) = 0; + virtual void listResources(FileMap& out) = 0; /// True if this archive contains the provided normalized file. - virtual bool contains(const std::string& file) const = 0; + virtual bool contains(std::string_view file) const = 0; virtual std::string getDescription() const = 0; }; diff --git a/components/vfs/bsaarchive.hpp b/components/vfs/bsaarchive.hpp index 517c95e273..29098db45d 100644 --- a/components/vfs/bsaarchive.hpp +++ b/components/vfs/bsaarchive.hpp @@ -48,7 +48,7 @@ namespace VFS virtual ~BsaArchive() {} - void listResources(std::map& out) override + void listResources(FileMap& out) override { for (auto& resource : mResources) { @@ -59,7 +59,7 @@ namespace VFS } } - bool contains(const std::string& file) const override + bool contains(std::string_view file) const override { for (const auto& it : mResources) { diff --git a/components/vfs/filesystemarchive.cpp b/components/vfs/filesystemarchive.cpp index 96f5e87f5e..7d88dd9cc0 100644 --- a/components/vfs/filesystemarchive.cpp +++ b/components/vfs/filesystemarchive.cpp @@ -17,7 +17,7 @@ namespace VFS { } - void FileSystemArchive::listResources(std::map& out) + void FileSystemArchive::listResources(FileMap& out) { if (!mBuiltIndex) { @@ -51,14 +51,12 @@ namespace VFS } else { - for (index::iterator it = mIndex.begin(); it != mIndex.end(); ++it) - { - out[it->first] = &it->second; - } + for (auto& [k, v] : mIndex) + out[k] = &v; } } - bool FileSystemArchive::contains(const std::string& file) const + bool FileSystemArchive::contains(std::string_view file) const { return mIndex.find(file) != mIndex.end(); } diff --git a/components/vfs/filesystemarchive.hpp b/components/vfs/filesystemarchive.hpp index 39d711b327..e31ef9bd30 100644 --- a/components/vfs/filesystemarchive.hpp +++ b/components/vfs/filesystemarchive.hpp @@ -27,16 +27,14 @@ namespace VFS public: FileSystemArchive(const std::filesystem::path& path); - void listResources(std::map& out) override; + void listResources(FileMap& out) override; - bool contains(const std::string& file) const override; + bool contains(std::string_view file) const override; std::string getDescription() const override; private: - typedef std::map index; - index mIndex; - + std::map> mIndex; bool mBuiltIndex; std::filesystem::path mPath; }; diff --git a/components/vfs/manager.cpp b/components/vfs/manager.cpp index bfc001e4f2..cc231847f5 100644 --- a/components/vfs/manager.cpp +++ b/components/vfs/manager.cpp @@ -35,11 +35,11 @@ namespace VFS return getNormalized(Path::normalizeFilename(name)); } - Files::IStreamPtr Manager::getNormalized(const std::string& normalizedName) const + Files::IStreamPtr Manager::getNormalized(std::string_view normalizedName) const { - std::map::const_iterator found = mIndex.find(normalizedName); + const auto found = mIndex.find(normalizedName); if (found == mIndex.end()) - throw std::runtime_error("Resource '" + normalizedName + "' not found"); + throw std::runtime_error("Resource '" + std::string(normalizedName) + "' not found"); return found->second->open(); } @@ -70,21 +70,13 @@ namespace VFS return found->second->getPath(); } - namespace - { - bool startsWith(std::string_view text, std::string_view start) - { - return text.rfind(start, 0) == 0; - } - } - Manager::RecursiveDirectoryRange Manager::getRecursiveDirectoryIterator(std::string_view path) const { if (path.empty()) return { mIndex.begin(), mIndex.end() }; std::string normalized = Path::normalizeFilename(path); const auto it = mIndex.lower_bound(normalized); - if (it == mIndex.end() || !startsWith(it->first, normalized)) + if (it == mIndex.end() || !it->first.starts_with(normalized)) return { it, it }; ++normalized.back(); return { it, mIndex.lower_bound(normalized) }; diff --git a/components/vfs/manager.hpp b/components/vfs/manager.hpp index db38e4b240..76405aae2c 100644 --- a/components/vfs/manager.hpp +++ b/components/vfs/manager.hpp @@ -41,7 +41,7 @@ namespace VFS class RecursiveDirectoryIterator { public: - RecursiveDirectoryIterator(std::map::const_iterator it) + RecursiveDirectoryIterator(FileMap::const_iterator it) : mIt(it) { } @@ -55,7 +55,7 @@ namespace VFS } private: - std::map::const_iterator mIt; + FileMap::const_iterator mIt; }; using RecursiveDirectoryRange = IteratorPair; @@ -83,7 +83,7 @@ namespace VFS /// Retrieve a file by name (name is already normalized). /// @note Throws an exception if the file can not be found. /// @note May be called from any thread once the index has been built. - Files::IStreamPtr getNormalized(const std::string& normalizedName) const; + Files::IStreamPtr getNormalized(std::string_view normalizedName) const; std::string getArchive(std::string_view name) const; @@ -101,7 +101,7 @@ namespace VFS private: std::vector> mArchives; - std::map mIndex; + FileMap mIndex; }; } diff --git a/docs/source/install_luadocumentor_in_docker.sh b/docs/source/install_luadocumentor_in_docker.sh index a1ec253600..fd7fcdb0e6 100755 --- a/docs/source/install_luadocumentor_in_docker.sh +++ b/docs/source/install_luadocumentor_in_docker.sh @@ -1,3 +1,5 @@ +#!/bin/bash -e + if [ ! -f /.dockerenv ] && [ ! -f /home/docs/omw_luadoc_docker ]; then echo 'This script installs lua-5.1, luarocks, and openmwluadocumentor to $HOME. Should be used only in docker.' exit 1 @@ -28,8 +30,8 @@ PATH=$PATH:~/luarocks/bin echo "Install openmwluadocumentor" git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git -git checkout 78577b255d19a1f4f4f539662e00357936b73c33 cd openmw-luadocumentor +git checkout 78577b255d19a1f4f4f539662e00357936b73c33 luarocks make luarocks/openmwluadocumentor-0.2.0-1.rockspec cd ~ rm -r openmw-luadocumentor diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst index 1ffa1820f3..10ed3ee555 100644 --- a/docs/source/reference/lua-scripting/engine_handlers.rst +++ b/docs/source/reference/lua-scripting/engine_handlers.rst @@ -109,7 +109,8 @@ Engine handler is a function defined by a script, that can be called by the engi | Usage example: | ``if id == input.CONTROLLER_BUTTON.LeftStick then ...`` * - onInputAction(id) - - | `Game control `_ is pressed. + - | (DEPRECATED, use `registerActionHandler `_) + | `Game control `_ is pressed. | Usage example: | ``if id == input.ACTION.ToggleWeapon then ...`` * - onTouchPress(touchEvent) diff --git a/docs/source/reference/lua-scripting/events.rst b/docs/source/reference/lua-scripting/events.rst index 7f0a764b86..282e3d1173 100644 --- a/docs/source/reference/lua-scripting/events.rst +++ b/docs/source/reference/lua-scripting/events.rst @@ -6,6 +6,18 @@ Built-in events Actor events ------------ +**Died** + +This event is sent to an actor's local script when that actor dies. + +.. code-block:: Lua + + eventHandlers = { + Died = function() + print('Alas, ye hardly knew me!') + end + } + **StartAIPackage, RemoveAIPackages** Any script can send to any actor (except player, for player will be ignored) events ``StartAIPackage`` and ``RemoveAIPackages``. diff --git a/docs/source/reference/lua-scripting/setting_renderers.rst b/docs/source/reference/lua-scripting/setting_renderers.rst index 966246d503..7f40eb08bd 100644 --- a/docs/source/reference/lua-scripting/setting_renderers.rst +++ b/docs/source/reference/lua-scripting/setting_renderers.rst @@ -126,3 +126,27 @@ Table with the following optional fields: * - disabled - bool (false) - Disables changing the setting from the UI + +inputBinding +----- + +Allows the user to bind inputs to an action or trigger + +**Argument** + +Table with the following fields: + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type (default) + - description + * - type + - 'keyboardPress', 'keyboardHold' + - The type of input that's allowed to be bound + * - key + - #string + - Key of the action or trigger to which the input is bound + diff --git a/docs/source/reference/modding/custom-shader-effects.rst b/docs/source/reference/modding/custom-shader-effects.rst index 5ea711953d..60a306a97a 100644 --- a/docs/source/reference/modding/custom-shader-effects.rst +++ b/docs/source/reference/modding/custom-shader-effects.rst @@ -6,10 +6,9 @@ This node must have the prefix `omw:data` and have a valid JSON object that foll .. note:: - This is a new feature to inject OpenMW-specific shader effects. Only a single - effect is currently supported. By default, the shader effects will propogate - to all a node's children. Other propogation modes and effects will come with - future releases. + This is a new feature to inject OpenMW-specific shader effects. By default, + the shader effects will propagate to all of a node's children. + Other propagation modes and effects will come with future releases. Soft Effect @@ -54,3 +53,38 @@ Example usage. } } } + +Distortion +---------- + +This effect is used to imitate effects such as refraction and heat distortion. A common use case is to assign a normal map to the +diffuse slot to a material and add uv scrolling. The red and green channels of the texture are used to offset the final scene texture. +Blue and alpha channels are ignored. + +To use this feature the :ref:`post processing` setting must be enabled. +This setting can either be activated in the OpenMW launcher, in-game, or changed in `settings.cfg`: + +:: + + [Post Processing] + enabled = false + +Variables. + ++---------+--------------------------------------------------------------------------------------------------------+---------+---------+ +| Name | Description | Type | Default | ++---------+--------------------------------------------------------------------------------------------------------+---------+---------+ +| strength| The strength of the distortion effect. Scales linearly. | float | 0.1 | ++---------+--------------------------------------------------------------------------------------------------------+---------+---------+ + +Example usage. + +:: + + omw:data { + "shader" : { + "distortion" : { + "strength": 0.12, + } + } + } diff --git a/docs/source/reference/modding/settings/models.rst b/docs/source/reference/modding/settings/models.rst index 998be9e6ea..4cf8d4c0d6 100644 --- a/docs/source/reference/modding/settings/models.rst +++ b/docs/source/reference/modding/settings/models.rst @@ -269,7 +269,6 @@ write nif debug log :Type: boolean :Range: True/False -:Default: True +:Default: False -If enabled, log the loading process of unsupported NIF files. -:ref:`load unsupported nif files` setting must be enabled for this setting to have any effect. +If enabled, log the loading process of NIF files. diff --git a/extern/CMakeLists.txt b/extern/CMakeLists.txt index 6f55e4f1c6..10d75c1057 100644 --- a/extern/CMakeLists.txt +++ b/extern/CMakeLists.txt @@ -173,11 +173,10 @@ if(NOT OPENMW_USE_SYSTEM_RECASTNAVIGATION) set(RECASTNAVIGATION_TESTS OFF CACHE BOOL "") set(RECASTNAVIGATION_EXAMPLES OFF CACHE BOOL "") - # master on 12 Oct 2022 include(FetchContent) FetchContent_Declare(recastnavigation - URL https://github.com/recastnavigation/recastnavigation/archive/405cc095ab3a2df976a298421974a2af83843baf.zip - URL_HASH SHA512=39580ca258783ab3bda237843facc918697266e729c85065cdae1f0ed3d0ed1429a7ed08b18b926ba64402d9875a18f52904a87f43fe4fe30252f23edcfa6c70 + URL https://github.com/recastnavigation/recastnavigation/archive/c393777d26d2ff6519ac23612abf8af42678c9dd.zip + URL_HASH SHA512=48f20cee7a70c2f20f4c68bb74d5af11a1434be85294e37f5fe7b7aae820fbcdff4f35d3be286eaf6f9cbce0aed4201fcc090df409a5bd04aec5fd7c29b3ad94 SOURCE_DIR fetched/recastnavigation ) FetchContent_MakeAvailableExcludeFromAll(recastnavigation) diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index dbf86cc44d..0e91a0b495 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -92,10 +92,13 @@ set(BUILTIN_DATA_FILES scripts/omw/ui.lua scripts/omw/usehandlers.lua scripts/omw/worldeventhandlers.lua + scripts/omw/input/actionbindings.lua + scripts/omw/input/smoothmovement.lua shaders/adjustments.omwfx shaders/bloomlinear.omwfx shaders/debug.omwfx + shaders/internal_distortion.omwfx mygui/core.skin mygui/core.xml diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index ec08c5299d..e4338df533 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -13,6 +13,8 @@ GLOBAL: scripts/omw/worldeventhandlers.lua PLAYER: scripts/omw/mechanics/playercontroller.lua PLAYER: scripts/omw/playercontrols.lua PLAYER: scripts/omw/camera/camera.lua +PLAYER: scripts/omw/input/actionbindings.lua +PLAYER: scripts/omw/input/smoothmovement.lua NPC,CREATURE: scripts/omw/ai.lua # User interface diff --git a/files/data/l10n/OMWControls/en.yaml b/files/data/l10n/OMWControls/en.yaml index de6edde19a..9c45c1d1e5 100644 --- a/files/data/l10n/OMWControls/en.yaml +++ b/files/data/l10n/OMWControls/en.yaml @@ -10,7 +10,76 @@ alwaysRunDescription: | toggleSneak: "Toggle sneak" toggleSneakDescription: | - This setting causes the behavior of the sneak key (bound to Ctrl by default) - to toggle sneaking on and off rather than requiring the key to be held down while sneaking. + This setting causes the sneak key (bound to Ctrl by default) to toggle sneaking on and off + rather than requiring the key to be held down while sneaking. Players that spend significant time sneaking may find the character easier to control with this option enabled. +smoothControllerMovement: "Smooth controller movement" +smoothControllerMovementDescription: | + Enables smooth movement with controller stick, with no abrupt switch from walking to running. + +TogglePOV_name: "Toggle POV" +TogglePOV_description: "Toggle between first and third person view. Hold to enter preview mode." + +Zoom3rdPerson_name: "Zoom In/Out" +Zoom3rdPerson_description: "Moves the camera closer / further away when in third person view." + +MoveForward_name: "Move Forward" +MoveForward_description: "Can cancel out with Move Backward" + +MoveBackward_name: "Move Backward" +MoveBackward_description: "Can cancel out with Move Forward" + +MoveLeft_name: "Move Left" +MoveLeft_description: "Can cancel out with Move Right" + +MoveRight_name: "Move Right" +MoveRight_description: "Can cancel out with Move Left" + +Use_name: "Use" +Use_description: "Attack with a weapon or cast a spell depending on current stance" + +Run_name: "Run" +Run_description: "Hold to run/walk, depending on the Always Run setting" + +AlwaysRun_name: "Always Run" +AlwaysRun_description: "Toggle the Always Run setting" + +Jump_name: "Jump" +Jump_description: "Jump whenever you are on the ground" + +AutoMove_name: "Auto Run" +AutoMove_description: "Toggle continous forward movement" + +Sneak_name: "Sneak" +Sneak_description: "Hold to sneak, if the Toggle Sneak setting is off" + +ToggleSneak_name: "Toggle Sneak" +ToggleSneak_description: "Toggle sneak, if the Toggle Sneak setting is on" + +ToggleWeapon_name: "Ready Weapon" +ToggleWeapon_description: "Enter or leave the weapon stance" + +ToggleSpell_name: "Ready Magic" +ToggleSpell_description: "Enter or leave the magic stance" + +Inventory_name: "Inventory" +Inventory_description: "Open the inventory" + +Journal_name: "Journal" +Journal_description: "Open the journal" + +QuickKeysMenu_name: "QuickKeysMenu" +QuickKeysMenu_description: "Open the quick keys menu" + +SmoothMoveForward_name: "Smooth Move Forward" +SmoothMoveForward_description: "Forward movement adjusted for smooth Walk-Run transitions" + +SmoothMoveBackward_name: "Smooth Move Backward" +SmoothMoveBackward_description: "Backward movement adjusted for smooth Walk-Run transitions" + +SmoothMoveLeft_name: "Smooth Move Left" +SmoothMoveLeft_description: "Left movement adjusted for smooth Walk-Run transitions" + +SkmoothMoveRight_name: "SmoothMove Right" +SkmoothMoveRight_description: "Right movement adjusted for smooth Walk-Run transitions" diff --git a/files/data/l10n/OMWControls/fr.yaml b/files/data/l10n/OMWControls/fr.yaml index 6d58ee1794..dab9ddb8fc 100644 --- a/files/data/l10n/OMWControls/fr.yaml +++ b/files/data/l10n/OMWControls/fr.yaml @@ -14,3 +14,6 @@ toggleSneakDescription: | Une simple pression de la touche associée (Ctrl par défaut) active le mode discrétion, une seconde pression désactive le mode discrétion.\n\n Il n'est plus nécessaire de maintenir une touche appuyée pour que le mode discrétion soit actif.\n\n Certains joueurs ayant une utilisation intensive du mode discrétion considèrent qu'il est plus aisé de contrôler leur personnage ainsi. + +# smoothControllerMovement +# smoothControllerMovementDescription diff --git a/files/data/l10n/OMWControls/ru.yaml b/files/data/l10n/OMWControls/ru.yaml index 30bb15ee57..0ce3609e16 100644 --- a/files/data/l10n/OMWControls/ru.yaml +++ b/files/data/l10n/OMWControls/ru.yaml @@ -14,3 +14,5 @@ toggleSneakDescription: | чтобы красться, её достаточно нажать единожды для переключения положения, а не зажимать. Игрокам, которые много времени крадутся, может быть проще управлять персонажем, когда опция включена. +# smoothControllerMovement +# smoothControllerMovementDescription diff --git a/files/data/l10n/OMWControls/sv.yaml b/files/data/l10n/OMWControls/sv.yaml index 43565053c5..73fc5e18dc 100644 --- a/files/data/l10n/OMWControls/sv.yaml +++ b/files/data/l10n/OMWControls/sv.yaml @@ -13,3 +13,6 @@ toggleSneakDescription: | Denna inställningen gör att smygknappen (förinställt till ctrl) slår smygning på eller av vid ett knapptryck istället för att att kräva att knappen hålls nedtryckt för att smyga. Spelare som spenderar mycket tid med att smyga lär ha lättare att kontrollera rollfiguren med denna funktion aktiverad. + +# smoothControllerMovement +# smoothControllerMovementDescription diff --git a/files/data/l10n/OMWEngine/de.yaml b/files/data/l10n/OMWEngine/de.yaml index 26838bd93c..0f729d0077 100644 --- a/files/data/l10n/OMWEngine/de.yaml +++ b/files/data/l10n/OMWEngine/de.yaml @@ -25,6 +25,9 @@ BuildingNavigationMesh: "Baue Navigationsgitter" #LoadingRequiresNewVersionError: |- # This save file was created using a newer version of OpenMW and is thus not supported. # Please upgrade to the newest OpenMW version to load this file. +# LoadingRequiresOldVersionError: |- +# This save file was created using an older version of OpenMW in a format that is no longer supported. +# Load and save this file using {version} to upgrade it. #NewGameConfirmation: "Do you want to start a new game and lose the current one?" #SaveGameDenied: "The game cannot be saved right now." #SavingInProgress: "Saving..." diff --git a/files/data/l10n/OMWEngine/en.yaml b/files/data/l10n/OMWEngine/en.yaml index ee2a33ee71..09db2b496d 100644 --- a/files/data/l10n/OMWEngine/en.yaml +++ b/files/data/l10n/OMWEngine/en.yaml @@ -22,6 +22,9 @@ LoadingInProgress: "Loading Save Game" LoadingRequiresNewVersionError: |- This save file was created using a newer version of OpenMW and is thus not supported. Please upgrade to the newest OpenMW version to load this file. +LoadingRequiresOldVersionError: |- + This save file was created using an older version of OpenMW in a format that is no longer supported. + Load and save this file using {version} to upgrade it. NewGameConfirmation: "Do you want to start a new game and lose the current one?" SaveGameDenied: "The game cannot be saved right now." SavingInProgress: "Saving..." diff --git a/files/data/l10n/OMWEngine/fr.yaml b/files/data/l10n/OMWEngine/fr.yaml index 689ccc59a5..f2772b017e 100644 --- a/files/data/l10n/OMWEngine/fr.yaml +++ b/files/data/l10n/OMWEngine/fr.yaml @@ -22,6 +22,9 @@ LoadingInProgress: "Chargement de la sauvegarde" LoadingRequiresNewVersionError: |- Ce fichier de sauvegarde provient d'une version plus récente d'OpenMW, il n'est par consequent pas supporté. Mettez à jour votre version d'OpenMW afin de pouvoir charger cette sauvegarde. +# LoadingRequiresOldVersionError: |- +# This save file was created using an older version of OpenMW in a format that is no longer supported. +# Load and save this file using {version} to upgrade it. NewGameConfirmation: "Voulez-vous démarrer une nouvelle partie ? Toute progression non sauvegardée sera perdue." SaveGameDenied: "Sauvegarde impossible" SavingInProgress: "Sauvegarde en cours..." diff --git a/files/data/l10n/OMWEngine/ru.yaml b/files/data/l10n/OMWEngine/ru.yaml index b645b681b1..2bcb76a442 100644 --- a/files/data/l10n/OMWEngine/ru.yaml +++ b/files/data/l10n/OMWEngine/ru.yaml @@ -22,6 +22,9 @@ LoadingInProgress: "Загрузка сохранения" LoadingRequiresNewVersionError: |- Это сохранение создано более новой версией OpenMW и поэтому не может быть загружено. Обновите OpenMW до последней версии, чтобы загрузить этот файл. +LoadingRequiresOldVersionError: |- + Это сохранение создано старой версией OpenMW и использует формат, который больше не поддерживается. + Загрузите и сохраните этот файл в {version}, чтобы обновить его. NewGameConfirmation: "Вы хотите начать новую игру? Текущая игра будет потеряна." SaveGameDenied: "В данный момент игру нельзя сохранить." SavingInProgress: "Сохранение..." diff --git a/files/data/l10n/OMWEngine/sv.yaml b/files/data/l10n/OMWEngine/sv.yaml index f4c9db031a..dc65726fdd 100644 --- a/files/data/l10n/OMWEngine/sv.yaml +++ b/files/data/l10n/OMWEngine/sv.yaml @@ -22,6 +22,9 @@ LoadingInProgress: "Laddar sparat spel" LoadingRequiresNewVersionError: |- Denna sparfil skapades i en nyare version av OpenMW och stöds därför inte. Uppgradera till den senaste versionen av OpenMW för att ladda filen. +# LoadingRequiresOldVersionError: |- +# This save file was created using an older version of OpenMW in a format that is no longer supported. +# Load and save this file using {version} to upgrade it. NewGameConfirmation: "Vill du starta ett nytt spel och förlora det pågående spelet?" SaveGameDenied: "Spelet kan inte sparas just nu." SavingInProgress: "Sparar..." diff --git a/files/data/scripts/omw/camera/camera.lua b/files/data/scripts/omw/camera/camera.lua index 38441f9e59..6c162f3a25 100644 --- a/files/data/scripts/omw/camera/camera.lua +++ b/files/data/scripts/omw/camera/camera.lua @@ -10,6 +10,24 @@ local I = require('openmw.interfaces') local Actor = require('openmw.types').Actor local Player = require('openmw.types').Player +input.registerAction { + key = 'TogglePOV', + l10n = 'OMWControls', + name = 'TogglePOV_name', + description = 'TogglePOV_description', + type = input.ACTION_TYPE.Boolean, + defaultValue = false, +} + +input.registerAction { + key = 'Zoom3rdPerson', + l10n = 'OMWControls', + name = 'Zoom3rdPerson_name', + description = 'Zoom3rdPerson_description', + type = input.ACTION_TYPE.Number, + defaultValue = 0, +} + local settings = require('scripts.omw.camera.settings').thirdPerson local head_bobbing = require('scripts.omw.camera.head_bobbing') local third_person = require('scripts.omw.camera.third_person') @@ -63,7 +81,7 @@ local previewTimer = 0 local function updatePOV(dt) local switchLimit = 0.25 - if input.isActionPressed(input.ACTION.TogglePOV) and Player.getControlSwitch(self, Player.CONTROL_SWITCH.ViewMode) then + if input.getBooleanActionValue('TogglePOV') and Player.getControlSwitch(self, Player.CONTROL_SWITCH.ViewMode) then previewTimer = previewTimer + dt if primaryMode == MODE.ThirdPerson or previewTimer >= switchLimit then third_person.standingPreview = false @@ -117,18 +135,19 @@ local maxDistance = 800 local function zoom(delta) if not Player.getControlSwitch(self, Player.CONTROL_SWITCH.ViewMode) or - not Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) or - camera.getMode() == MODE.Static or next(noZoom) then + not Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) or + camera.getMode() == MODE.Static or next(noZoom) then return end if camera.getMode() ~= MODE.FirstPerson then local obstacleDelta = third_person.preferredDistance - camera.getThirdPersonDistance() if delta > 0 and third_person.baseDistance == minDistance and - (camera.getMode() ~= MODE.Preview or third_person.standingPreview) and not next(noModeControl) then + (camera.getMode() ~= MODE.Preview or third_person.standingPreview) and not next(noModeControl) then primaryMode = MODE.FirstPerson camera.setMode(primaryMode) elseif delta > 0 or obstacleDelta < -delta then - third_person.baseDistance = util.clamp(third_person.baseDistance - delta - obstacleDelta, minDistance, maxDistance) + third_person.baseDistance = util.clamp(third_person.baseDistance - delta - obstacleDelta, minDistance, + maxDistance) end elseif delta < 0 and not next(noModeControl) then primaryMode = MODE.ThirdPerson @@ -137,21 +156,10 @@ local function zoom(delta) end end -local function applyControllerZoom(dt) - if input.isActionPressed(input.ACTION.TogglePOV) then - local triggerLeft = input.getAxisValue(input.CONTROLLER_AXIS.TriggerLeft) - local triggerRight = input.getAxisValue(input.CONTROLLER_AXIS.TriggerRight) - local controllerZoom = (triggerRight - triggerLeft) * 100 * dt - if controllerZoom ~= 0 then - zoom(controllerZoom) - end - end -end - local function updateStandingPreview() local mode = camera.getMode() if not previewIfStandStill or next(noStandingPreview) - or mode == MODE.FirstPerson or mode == MODE.Static or mode == MODE.Vanity then + or mode == MODE.FirstPerson or mode == MODE.Static or mode == MODE.Vanity then third_person.standingPreview = false return end @@ -184,7 +192,7 @@ local function updateIdleTimer(dt) if not input.isIdle() then idleTimer = 0 elseif self.controls.movement ~= 0 or self.controls.sideMovement ~= 0 or self.controls.jump or self.controls.use ~= 0 then - idleTimer = 0 -- also reset the timer in case of a scripted movement + idleTimer = 0 -- also reset the timer in case of a scripted movement else idleTimer = idleTimer + dt end @@ -205,7 +213,14 @@ local function onFrame(dt) updateStandingPreview() updateCrosshair() end - applyControllerZoom(dt) + + do + local Zoom3rdPerson = input.getNumberActionValue('Zoom3rdPerson') + if Zoom3rdPerson ~= 0 then + zoom(Zoom3rdPerson) + end + end + third_person.update(dt, smoothedSpeed) if not next(noHeadBobbing) then head_bobbing.update(dt, smoothedSpeed) end if slowViewChange then @@ -312,15 +327,6 @@ return { engineHandlers = { onUpdate = onUpdate, onFrame = onFrame, - onInputAction = function(action) - if core.isWorldPaused() or I.UI.getMode() then return end - if action == input.ACTION.ZoomIn then - zoom(10) - elseif action == input.ACTION.ZoomOut then - zoom(-10) - end - move360.onInputAction(action) - end, onTeleported = function() camera.instantTransition() end, @@ -329,7 +335,7 @@ return { if data and data.distance then third_person.baseDistance = data.distance end end, onSave = function() - return {version = 0, distance = third_person.baseDistance} + return { version = 0, distance = third_person.baseDistance } end, }, } diff --git a/files/data/scripts/omw/camera/move360.lua b/files/data/scripts/omw/camera/move360.lua index 18c30ae77b..a80019f3a0 100644 --- a/files/data/scripts/omw/camera/move360.lua +++ b/files/data/scripts/omw/camera/move360.lua @@ -30,6 +30,26 @@ local function turnOff() end end +local function processZoom3rdPerson() + if + not Player.getControlSwitch(self, Player.CONTROL_SWITCH.ViewMode) or + not Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) or + input.getBooleanActionValue('TogglePOV') or + not I.Camera.isModeControlEnabled() + then + return + end + local Zoom3rdPerson = input.getNumberActionValue('Zoom3rdPerson') + if Zoom3rdPerson > 0 and camera.getMode() == MODE.Preview + and I.Camera.getBaseThirdPersonDistance() == 30 then + self.controls.yawChange = camera.getYaw() - self.rotation:getYaw() + camera.setMode(MODE.FirstPerson) + elseif Zoom3rdPerson < 0 and camera.getMode() == MODE.FirstPerson then + camera.setMode(MODE.Preview) + I.Camera.setBaseThirdPersonDistance(30) + end +end + function M.onFrame(dt) if core.isWorldPaused() then return end local newActive = M.enabled and Actor.getStance(self) == Actor.STANCE.Nothing @@ -39,9 +59,10 @@ function M.onFrame(dt) turnOff() end if not active then return end + processZoom3rdPerson() if camera.getMode() == MODE.Static then return end if camera.getMode() == MODE.ThirdPerson then camera.setMode(MODE.Preview) end - if camera.getMode() == MODE.Preview and not input.isActionPressed(input.ACTION.TogglePOV) then + if camera.getMode() == MODE.Preview and not input.getBooleanActionValue('TogglePOV') then camera.showCrosshair(camera.getFocalPreferredOffset():length() > 5) local move = util.vector2(self.controls.sideMovement, self.controls.movement) local yawDelta = camera.getYaw() - self.rotation:getYaw() @@ -59,22 +80,4 @@ function M.onFrame(dt) end end -function M.onInputAction(action) - if not active or core.isWorldPaused() or - not Player.getControlSwitch(self, Player.CONTROL_SWITCH.ViewMode) or - not Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) or - input.isActionPressed(input.ACTION.TogglePOV) or - not I.Camera.isModeControlEnabled() then - return - end - if action == input.ACTION.ZoomIn and camera.getMode() == MODE.Preview - and I.Camera.getBaseThirdPersonDistance() == 30 then - self.controls.yawChange = camera.getYaw() - self.rotation:getYaw() - camera.setMode(MODE.FirstPerson) - elseif action == input.ACTION.ZoomOut and camera.getMode() == MODE.FirstPerson then - camera.setMode(MODE.Preview) - I.Camera.setBaseThirdPersonDistance(30) - end -end - return M diff --git a/files/data/scripts/omw/input/actionbindings.lua b/files/data/scripts/omw/input/actionbindings.lua new file mode 100644 index 0000000000..06ded80793 --- /dev/null +++ b/files/data/scripts/omw/input/actionbindings.lua @@ -0,0 +1,255 @@ +local core = require('openmw.core') +local input = require('openmw.input') +local util = require('openmw.util') +local async = require('openmw.async') +local storage = require('openmw.storage') +local ui = require('openmw.ui') + +local I = require('openmw.interfaces') + +local actionPressHandlers = {} +local function onActionPress(id, handler) + actionPressHandlers[id] = actionPressHandlers[id] or {} + table.insert(actionPressHandlers[id], handler) +end + +local function bindHold(key, actionId) + input.bindAction(key, async:callback(function() + return input.isActionPressed(actionId) + end), {}) +end + +local function bindMovement(key, actionId, axisId, direction) + input.bindAction(key, async:callback(function() + local actionActive = input.isActionPressed(actionId) + local axisActive = input.getAxisValue(axisId) * direction > 0 + return (actionActive or axisActive) and 1 or 0 + end), {}) +end + +local function bindTrigger(key, actionid) + onActionPress(actionid, function() + input.activateTrigger(key) + end) +end + +bindTrigger('AlwaysRun', input.ACTION.AlwaysRun) +bindTrigger('ToggleSneak', input.ACTION.Sneak) +bindTrigger('ToggleWeapon', input.ACTION.ToggleWeapon) +bindTrigger('ToggleSpell', input.ACTION.ToggleSpell) +bindTrigger('Jump', input.ACTION.Jump) +bindTrigger('AutoMove', input.ACTION.AutoMove) +bindTrigger('Inventory', input.ACTION.Inventory) +bindTrigger('Journal', input.ACTION.Journal) +bindTrigger('QuickKeysMenu', input.ACTION.QuickKeysMenu) + +bindHold('TogglePOV', input.ACTION.TogglePOV) +bindHold('Sneak', input.ACTION.Sneak) + +bindHold('Run', input.ACTION.Run) +input.bindAction('Run', async:callback(function(_, value) + local controllerInput = util.vector2( + input.getAxisValue(input.CONTROLLER_AXIS.MoveForwardBackward), + input.getAxisValue(input.CONTROLLER_AXIS.MoveLeftRight) + ):length2() + return value or controllerInput > 0.25 +end), {}) + +input.bindAction('Use', async:callback(function() + -- The value "0.6" shouldn't exceed the triggering threshold in BindingsManager::actionValueChanged. + -- TODO: Move more logic from BindingsManager to Lua and consider to make this threshold configurable. + return input.isActionPressed(input.ACTION.Use) or input.getAxisValue(input.CONTROLLER_AXIS.TriggerRight) >= 0.6 +end), {}) + +bindMovement('MoveBackward', input.ACTION.MoveBackward, input.CONTROLLER_AXIS.MoveForwardBackward, 1) +bindMovement('MoveForward', input.ACTION.MoveForward, input.CONTROLLER_AXIS.MoveForwardBackward, -1) +bindMovement('MoveRight', input.ACTION.MoveRight, input.CONTROLLER_AXIS.MoveLeftRight, 1) +bindMovement('MoveLeft', input.ACTION.MoveLeft, input.CONTROLLER_AXIS.MoveLeftRight, -1) + +do + local zoomInOut = 0 + onActionPress(input.ACTION.ZoomIn, function() + zoomInOut = zoomInOut + 1 + end) + onActionPress(input.ACTION.ZoomOut, function() + zoomInOut = zoomInOut - 1 + end) + input.bindAction('Zoom3rdPerson', async:callback(function(dt, _, togglePOV) + local Zoom3rdPerson = zoomInOut * 10 + if togglePOV then + local triggerLeft = input.getAxisValue(input.CONTROLLER_AXIS.TriggerLeft) + local triggerRight = input.getAxisValue(input.CONTROLLER_AXIS.TriggerRight) + local controllerZoom = (triggerRight - triggerLeft) * 100 * dt + Zoom3rdPerson = Zoom3rdPerson + controllerZoom + end + zoomInOut = 0 + return Zoom3rdPerson + end), { 'TogglePOV' }) +end + +local bindingSection = storage.playerSection('OMWInputBindings') + +local keyboardPresses = {} +local keybordHolds = {} +local boundActions = {} + +local function bindAction(action) + if boundActions[action] then return end + boundActions[action] = true + input.bindAction(action, async:callback(function() + if keybordHolds[action] then + for _, binding in pairs(keybordHolds[action]) do + if input.isKeyPressed(binding.code) then return true end + end + end + return false + end), {}) +end + +local function registerBinding(binding, id) + if not input.actions[binding.key] and not input.triggers[binding.key] then + print(string.format('Skipping binding for unknown action or trigger: "%s"', binding.key)) + return + end + if binding.type == 'keyboardPress' then + local bindings = keyboardPresses[binding.code] or {} + bindings[id] = binding + keyboardPresses[binding.code] = bindings + elseif binding.type == 'keyboardHold' then + local bindings = keybordHolds[binding.key] or {} + bindings[id] = binding + keybordHolds[binding.key] = bindings + bindAction(binding.key) + else + error('Unknown binding type "' .. binding.type .. '"') + end +end + +function clearBinding(id) + for _, boundTriggers in pairs(keyboardPresses) do + boundTriggers[id] = nil + end + for _, boundKeys in pairs(keybordHolds) do + boundKeys[id] = nil + end +end + +local function updateBinding(id, binding) + bindingSection:set(id, binding) + clearBinding(id) + if binding ~= nil then + registerBinding(binding, id) + end + return id +end + +local interfaceL10n = core.l10n('interface') + +I.Settings.registerRenderer('inputBinding', function(id, set, arg) + if type(id) ~= 'string' then error('inputBinding: must have a string default value') end + if not arg.type then error('inputBinding: type argument is required') end + if not arg.key then error('inputBinding: key argument is required') end + local info = input.actions[arg.key] or input.triggers[arg.key] + if not info then return {} end + + local l10n = core.l10n(info.key) + + local name = { + template = I.MWUI.templates.textNormal, + props = { + text = l10n(info.name), + }, + } + + local description = { + template = I.MWUI.templates.textNormal, + props = { + text = l10n(info.description), + }, + } + + local binding = bindingSection:get(id) + local label = binding and input.getKeyName(binding.code) or interfaceL10n('None') + + local recorder = { + template = I.MWUI.templates.textEditLine, + props = { + readOnly = true, + text = label, + }, + events = { + focusGain = async:callback(function() + if binding == nil then return end + updateBinding(id, nil) + set(id) + end), + keyPress = async:callback(function(key) + if binding ~= nil or key.code == input.KEY.Escape then return end + + local newBinding = { + code = key.code, + type = arg.type, + key = arg.key, + } + updateBinding(id, newBinding) + set(id) + end), + }, + } + + local row = { + type = ui.TYPE.Flex, + props = { + horizontal = true, + }, + content = ui.content { + name, + { props = { size = util.vector2(10, 0) } }, + recorder, + }, + } + local column = { + type = ui.TYPE.Flex, + content = ui.content { + row, + description, + }, + } + + return column +end) + +local initiated = false + +local function init() + for id, binding in pairs(bindingSection:asTable()) do + registerBinding(binding, id) + end +end + +return { + engineHandlers = { + onFrame = function() + if not initiated then + initiated = true + init() + end + end, + onInputAction = function(id) + if not actionPressHandlers[id] then + return + end + for _, handler in ipairs(actionPressHandlers[id]) do + handler() + end + end, + onKeyPress = function(e) + local bindings = keyboardPresses[e.code] + if bindings then + for _, binding in pairs(bindings) do + input.activateTrigger(binding.key) + end + end + end, + } +} diff --git a/files/data/scripts/omw/input/smoothmovement.lua b/files/data/scripts/omw/input/smoothmovement.lua new file mode 100644 index 0000000000..ebd322f25d --- /dev/null +++ b/files/data/scripts/omw/input/smoothmovement.lua @@ -0,0 +1,92 @@ +local input = require('openmw.input') +local util = require('openmw.util') +local async = require('openmw.async') +local storage = require('openmw.storage') +local types = require('openmw.types') +local self = require('openmw.self') + +local NPC = types.NPC + +local moveActions = { + 'MoveForward', + 'MoveBackward', + 'MoveLeft', + 'MoveRight' +} +for _, key in ipairs(moveActions) do + local smoothKey = 'Smooth' .. key + input.registerAction { + key = smoothKey, + l10n = 'OMWControls', + name = smoothKey .. '_name', + description = smoothKey .. '_description', + type = input.ACTION_TYPE.Range, + defaultValue = 0, + } +end + +local settings = storage.playerSection('SettingsOMWControls') + +local function shouldAlwaysRun(actor) + return actor.controls.sneak or not NPC.isOnGround(actor) or NPC.isSwimming(actor) +end + +local function remapToWalkRun(actor, inputMovement) + if shouldAlwaysRun(actor) then + return true, inputMovement + end + local normalizedInput, inputSpeed = inputMovement:normalize() + local switchPoint = 0.5 + if inputSpeed < switchPoint then + return false, inputMovement * 2 + else + local matchWalkingSpeed = NPC.getWalkSpeed(actor) / NPC.getRunSpeed(actor) + local runSpeedRatio = 2 * (inputSpeed - switchPoint) * (1 - matchWalkingSpeed) + matchWalkingSpeed + return true, normalizedInput * math.min(1, runSpeedRatio) + end +end + +local function computeSmoothMovement() + local controllerInput = util.vector2( + input.getAxisValue(input.CONTROLLER_AXIS.MoveForwardBackward), + input.getAxisValue(input.CONTROLLER_AXIS.MoveLeftRight) + ) + return remapToWalkRun(self, controllerInput) +end + +local function bindSmoothMove(key, axis, direction) + local smoothKey = 'Smooth' .. key + input.bindAction(smoothKey, async:callback(function() + local _, movement = computeSmoothMovement() + return math.max(direction * movement[axis], 0) + end), {}) + input.bindAction(key, async:callback(function(_, standardMovement, smoothMovement) + if not settings:get('smoothControllerMovement') then + return standardMovement + end + + if smoothMovement > 0 then + return smoothMovement + else + return standardMovement + end + end), { smoothKey }) +end + +bindSmoothMove('MoveForward', 'x', -1) +bindSmoothMove('MoveBackward', 'x', 1) +bindSmoothMove('MoveRight', 'y', 1) +bindSmoothMove('MoveLeft', 'y', -1) + +input.bindAction('Run', async:callback(function(_, run) + if not settings:get('smoothControllerMovement') then + return run + end + local smoothRun, movement = computeSmoothMovement() + if movement:length2() > 0 then + -- ignore always run + return smoothRun ~= settings:get('alwaysRun') + else + return run + end +end), {}) diff --git a/files/data/scripts/omw/mwui/init.lua b/files/data/scripts/omw/mwui/init.lua index 560d8af704..5ab2d3cb23 100644 --- a/files/data/scripts/omw/mwui/init.lua +++ b/files/data/scripts/omw/mwui/init.lua @@ -77,6 +77,7 @@ require('scripts.omw.mwui.space')(templates) --- -- Same as box, but with a semi-transparent background -- @field [parent=#Templates] openmw.ui#Template boxTransparent + --- -- Same as box, but with a solid background -- @field [parent=#Templates] openmw.ui#Template boxSolid @@ -100,6 +101,7 @@ require('scripts.omw.mwui.space')(templates) --- -- Same as box, but with a semi-transparent background -- @field [parent=#Templates] openmw.ui#Template boxTransparentThick + --- -- Same as box, but with a solid background -- @field [parent=#Templates] openmw.ui#Template boxSolidThick diff --git a/files/data/scripts/omw/playercontrols.lua b/files/data/scripts/omw/playercontrols.lua index 7b405180e8..ec7d0d238e 100644 --- a/files/data/scripts/omw/playercontrols.lua +++ b/files/data/scripts/omw/playercontrols.lua @@ -1,12 +1,12 @@ local core = require('openmw.core') local input = require('openmw.input') local self = require('openmw.self') -local util = require('openmw.util') +local storage = require('openmw.storage') local ui = require('openmw.ui') +local async = require('openmw.async') local Actor = require('openmw.types').Actor local Player = require('openmw.types').Player -local storage = require('openmw.storage') local I = require('openmw.interfaces') local settingsGroup = 'SettingsOMWControls' @@ -16,16 +16,16 @@ local function boolSetting(key, default) key = key, renderer = 'checkbox', name = key, - description = key..'Description', + description = key .. 'Description', default = default, } end I.Settings.registerPage({ - key = 'OMWControls', - l10n = 'OMWControls', - name = 'ControlsPage', - description = 'ControlsPageDescription', + key = 'OMWControls', + l10n = 'OMWControls', + name = 'ControlsPage', + description = 'ControlsPageDescription', }) I.Settings.registerGroup({ @@ -36,95 +36,68 @@ I.Settings.registerGroup({ permanentStorage = true, settings = { boolSetting('alwaysRun', false), - boolSetting('toggleSneak', false), + boolSetting('toggleSneak', false), -- TODO: consider removing this setting when we have the advanced binding UI + boolSetting('smoothControllerMovement', true), }, }) -local settings = storage.playerSection(settingsGroup) +local settings = storage.playerSection('SettingsOMWControls') -local attemptJump = false -local startAttack = false -local autoMove = false -local movementControlsOverridden = false -local combatControlsOverridden = false -local uiControlsOverridden = false +do + local rangeActions = { + 'MoveForward', + 'MoveBackward', + 'MoveLeft', + 'MoveRight' + } + for _, key in ipairs(rangeActions) do + input.registerAction { + key = key, + l10n = 'OMWControls', + name = key .. '_name', + description = key .. '_description', + type = input.ACTION_TYPE.Range, + defaultValue = 0, + } + end -local function processMovement() - local controllerMovement = -input.getAxisValue(input.CONTROLLER_AXIS.MoveForwardBackward) - local controllerSideMovement = input.getAxisValue(input.CONTROLLER_AXIS.MoveLeftRight) - if controllerMovement ~= 0 or controllerSideMovement ~= 0 then - -- controller movement - if util.vector2(controllerMovement, controllerSideMovement):length2() < 0.25 - and not self.controls.sneak and Actor.isOnGround(self) and not Actor.isSwimming(self) then - self.controls.run = false - self.controls.movement = controllerMovement * 2 - self.controls.sideMovement = controllerSideMovement * 2 - else - self.controls.run = true - self.controls.movement = controllerMovement - self.controls.sideMovement = controllerSideMovement - end - else - -- keyboard movement - self.controls.movement = 0 - self.controls.sideMovement = 0 - if input.isActionPressed(input.ACTION.MoveLeft) then - self.controls.sideMovement = self.controls.sideMovement - 1 - end - if input.isActionPressed(input.ACTION.MoveRight) then - self.controls.sideMovement = self.controls.sideMovement + 1 - end - if input.isActionPressed(input.ACTION.MoveBackward) then - self.controls.movement = self.controls.movement - 1 - end - if input.isActionPressed(input.ACTION.MoveForward) then - self.controls.movement = self.controls.movement + 1 - end - self.controls.run = input.isActionPressed(input.ACTION.Run) ~= settings:get('alwaysRun') + local booleanActions = { + 'Use', + 'Run', + 'Sneak', + } + for _, key in ipairs(booleanActions) do + input.registerAction { + key = key, + l10n = 'OMWControls', + name = key .. '_name', + description = key .. '_description', + type = input.ACTION_TYPE.Boolean, + defaultValue = false, + } end - if self.controls.movement ~= 0 or not Actor.canMove(self) then - autoMove = false - elseif autoMove then - self.controls.movement = 1 - end - self.controls.jump = attemptJump and Player.getControlSwitch(self, Player.CONTROL_SWITCH.Jumping) - if not settings:get('toggleSneak') then - self.controls.sneak = input.isActionPressed(input.ACTION.Sneak) - end -end -local function processAttacking() - if startAttack then - self.controls.use = 1 - elseif Actor.stance(self) == Actor.STANCE.Spell then - self.controls.use = 0 - elseif input.getAxisValue(input.CONTROLLER_AXIS.TriggerRight) < 0.6 - and not input.isActionPressed(input.ACTION.Use) then - -- The value "0.6" shouldn't exceed the triggering threshold in BindingsManager::actionValueChanged. - -- TODO: Move more logic from BindingsManager to Lua and consider to make this threshold configurable. - self.controls.use = 0 + local triggers = { + 'Jump', + 'AutoMove', + 'ToggleWeapon', + 'ToggleSpell', + 'AlwaysRun', + 'ToggleSneak', + 'Inventory', + 'Journal', + 'QuickKeysMenu', + } + for _, key in ipairs(triggers) do + input.registerTrigger { + key = key, + l10n = 'OMWControls', + name = key .. '_name', + description = key .. '_description', + } end end -local function onFrame(dt) - local controlsAllowed = Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) - and not core.isWorldPaused() and not I.UI.getMode() - if not movementControlsOverridden then - if controlsAllowed then - processMovement() - else - self.controls.movement = 0 - self.controls.sideMovement = 0 - self.controls.jump = false - end - end - if controlsAllowed and not combatControlsOverridden then - processAttacking() - end - attemptJump = false - startAttack = false -end - local function checkNotWerewolf() if Player.isWerewolf(self) then ui.showMessage(core.getGMST('sWerewolfRefusal')) @@ -139,68 +112,155 @@ local function isJournalAllowed() return I.UI.getWindowsForMode(I.UI.MODE.Interface)[I.UI.WINDOW.Magic] end -local function onInputAction(action) - if not Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) then - return +local movementControlsOverridden = false + +local autoMove = false +local function processMovement() + local movement = input.getRangeActionValue('MoveForward') - input.getRangeActionValue('MoveBackward') + 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 + autoMove = false + elseif autoMove then + movement = 1 end - if not uiControlsOverridden then - if action == input.ACTION.Inventory then - if I.UI.getMode() == nil then - I.UI.setMode(I.UI.MODE.Interface) - elseif I.UI.getMode() == I.UI.MODE.Interface or I.UI.getMode() == I.UI.MODE.Container then - I.UI.removeMode(I.UI.getMode()) - end - elseif action == input.ACTION.Journal then - if I.UI.getMode() == I.UI.MODE.Journal then - I.UI.removeMode(I.UI.MODE.Journal) - elseif isJournalAllowed() then - I.UI.addMode(I.UI.MODE.Journal) - end - elseif action == input.ACTION.QuickKeysMenu then - if I.UI.getMode() == I.UI.MODE.QuickKeysMenu then - I.UI.removeMode(I.UI.MODE.QuickKeysMenu) - elseif checkNotWerewolf() and Player.isCharGenFinished(self) then - I.UI.addMode(I.UI.MODE.QuickKeysMenu) - end + self.controls.movement = movement + self.controls.sideMovement = sideMovement + self.controls.run = run + + if not settings:get('toggleSneak') then + self.controls.sneak = input.getBooleanActionValue('Sneak') + end +end + +local function controlsAllowed() + return not core.isWorldPaused() + and Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) + and not I.UI.getMode() +end + +local function movementAllowed() + return controlsAllowed() and not movementControlsOverridden +end + +input.registerTriggerHandler('Jump', async:callback(function() + if not movementAllowed() then return end + self.controls.jump = Player.getControlSwitch(self, Player.CONTROL_SWITCH.Jumping) +end)) + +input.registerTriggerHandler('ToggleSneak', async:callback(function() + if not movementAllowed() then return end + if settings:get('toggleSneak') then + self.controls.sneak = not self.controls.sneak + end +end)) + +input.registerTriggerHandler('AlwaysRun', async:callback(function() + if not movementAllowed() then return end + settings:set('alwaysRun', not settings:get('alwaysRun')) +end)) + +input.registerTriggerHandler('AutoMove', async:callback(function() + if not movementAllowed() then return end + autoMove = not autoMove +end)) + +local combatControlsOverridden = false + +local function combatAllowed() + return controlsAllowed() and not combatControlsOverridden +end + +input.registerTriggerHandler('ToggleSpell', async:callback(function() + if not combatAllowed() then return end + if Actor.stance(self) == Actor.STANCE.Spell then + Actor.setStance(self, Actor.STANCE.Nothing) + elseif Player.getControlSwitch(self, Player.CONTROL_SWITCH.Magic) then + if checkNotWerewolf() then + Actor.setStance(self, Actor.STANCE.Spell) end end +end)) - if core.isWorldPaused() or I.UI.getMode() then - return +input.registerTriggerHandler('ToggleWeapon', async:callback(function() + if not combatAllowed() then return end + if Actor.stance(self) == Actor.STANCE.Weapon then + Actor.setStance(self, Actor.STANCE.Nothing) + elseif Player.getControlSwitch(self, Player.CONTROL_SWITCH.Fighting) then + Actor.setStance(self, Actor.STANCE.Weapon) end +end)) - if action == input.ACTION.Jump then - attemptJump = true - elseif action == input.ACTION.Use then - startAttack = Actor.stance(self) ~= Actor.STANCE.Nothing - elseif action == input.ACTION.AutoMove and not movementControlsOverridden then - autoMove = not autoMove - elseif action == input.ACTION.AlwaysRun and not movementControlsOverridden then - settings:set('alwaysRun', not settings:get('alwaysRun')) - elseif action == input.ACTION.Sneak and not movementControlsOverridden then - if settings:get('toggleSneak') then - self.controls.sneak = not self.controls.sneak - end - elseif action == input.ACTION.ToggleSpell and not combatControlsOverridden then - if Actor.stance(self) == Actor.STANCE.Spell then - Actor.setStance(self, Actor.STANCE.Nothing) - elseif Player.getControlSwitch(self, Player.CONTROL_SWITCH.Magic) then - if checkNotWerewolf() then - Actor.setStance(self, Actor.STANCE.Spell) - end - end - elseif action == input.ACTION.ToggleWeapon and not combatControlsOverridden then - if Actor.stance(self) == Actor.STANCE.Weapon then - Actor.setStance(self, Actor.STANCE.Nothing) - elseif Player.getControlSwitch(self, Player.CONTROL_SWITCH.Fighting) then - Actor.setStance(self, Actor.STANCE.Weapon) - end +local startUse = false +input.registerActionHandler('Use', async:callback(function(value) + if value and combatAllowed() then startUse = true end +end)) +local function processAttacking() + -- for spell-casting, set controls.use to true for exactly one frame + -- otherwise spell casting is attempted every frame while Use is true + if Actor.stance(self) == Actor.STANCE.Spell then + self.controls.use = startUse and 1 or 0 + else + self.controls.use = input.getBooleanActionValue('Use') and 1 or 0 + end + startUse = false +end + +local uiControlsOverridden = false + +local function uiAllowed() + return Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) and not uiControlsOverridden +end + +input.registerTriggerHandler('Inventory', async:callback(function() + if not uiAllowed() then return end + + if I.UI.getMode() == nil then + I.UI.setMode(I.UI.MODE.Interface) + elseif I.UI.getMode() == I.UI.MODE.Interface or I.UI.getMode() == I.UI.MODE.Container then + I.UI.removeMode(I.UI.getMode()) + end +end)) + +input.registerTriggerHandler('Journal', async:callback(function() + if not uiAllowed() then return end + + if I.UI.getMode() == I.UI.MODE.Journal then + I.UI.removeMode(I.UI.MODE.Journal) + elseif isJournalAllowed() then + I.UI.addMode(I.UI.MODE.Journal) + end +end)) + +input.registerTriggerHandler('QuickKeysMenu', async:callback(function() + if not uiAllowed() then return end + + if I.UI.getMode() == I.UI.MODE.QuickKeysMenu then + I.UI.removeMode(I.UI.MODE.QuickKeysMenu) + elseif checkNotWerewolf() and Player.isCharGenFinished(self) then + I.UI.addMode(I.UI.MODE.QuickKeysMenu) + end +end)) + +local function onFrame(_) + if movementAllowed() then + processMovement() + elseif not movementControlsOverridden then + self.controls.movement = 0 + self.controls.sideMovement = 0 + self.controls.jump = false + end + if combatAllowed() then + processAttacking() end end local function onSave() - return {sneaking = self.controls.sneak} + return { + sneaking = self.controls.sneak + } end local function onLoad(data) @@ -211,7 +271,6 @@ end return { engineHandlers = { onFrame = onFrame, - onInputAction = onInputAction, onSave = onSave, onLoad = onLoad, }, @@ -242,4 +301,3 @@ return { overrideUiControls = function(v) uiControlsOverridden = v end, } } - diff --git a/files/data/shaders/internal_distortion.omwfx b/files/data/shaders/internal_distortion.omwfx new file mode 100644 index 0000000000..b641bb6711 --- /dev/null +++ b/files/data/shaders/internal_distortion.omwfx @@ -0,0 +1,25 @@ +fragment main { + + omw_In vec2 omw_TexCoord; + + void main() + { + const float multiplier = 0.14; + + vec2 offset = omw_Texture2D(omw_SamplerDistortion, omw_TexCoord).rg; + offset *= multiplier; + offset = clamp(offset, vec2(-1.0), vec2(1.0)); + + float occlusionFactor = omw_Texture2D(omw_SamplerDistortion, omw_TexCoord+offset).b; + + omw_FragColor = mix(omw_GetLastShader(omw_TexCoord + offset), omw_GetLastShader(omw_TexCoord), occlusionFactor); + } +} + +technique { + description = "Internal refraction shader for OpenMW"; + version = "1.0"; + author = "OpenMW"; + passes = main; + flags = hidden; +} diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 18898b5002..7ea3c75f1c 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -55,6 +55,11 @@ -- @function [parent=#core] getRealTime -- @return #number +--- +-- Frame duration in seconds +-- @function [parent=#core] getRealFrameDuration +-- @return #number + --- -- Get a GMST setting from content files. -- @function [parent=#core] getGMST @@ -677,7 +682,6 @@ --- -- @type ActiveEffect -- Magic effect that is currently active on an actor. --- Note that when this effect expires or is removed, it will remain temporarily. Magnitude will be set to 0 for effects that expire. -- @field #string affectedSkill Optional skill ID -- @field #string affectedAttribute Optional attribute ID -- @field #string id Effect id string @@ -866,6 +870,7 @@ -- @field #MagicSchoolData school Optional magic school -- @field #string attribute The id of the skill's governing attribute +--- -- @type MagicSchoolData -- @field #string name Human-readable name -- @field #string areaSound VFS path to the area sound diff --git a/files/lua_api/openmw/input.lua b/files/lua_api/openmw/input.lua index fbb790ce49..12bd51b47a 100644 --- a/files/lua_api/openmw/input.lua +++ b/files/lua_api/openmw/input.lua @@ -11,8 +11,7 @@ -- @return #boolean --- --- Is a specific control currently pressed. --- Input bindings can be changed ingame using Options/Controls menu. +-- (DEPRECATED, use getBooleanActionValue) Input bindings can be changed ingame using Options/Controls menu. -- @function [parent=#input] isActionPressed -- @param #number actionId One of @{openmw.input#ACTION} -- @return #boolean @@ -118,6 +117,7 @@ -- @field [parent=#input] #CONTROL_SWITCH CONTROL_SWITCH --- +-- (DEPRECATED, use actions with matching keys) -- @type ACTION -- @field [parent=#ACTION] #number GameMenu -- @field [parent=#ACTION] #number Screenshot @@ -163,7 +163,7 @@ -- @field [parent=#ACTION] #number TogglePostProcessorHUD --- --- Values that can be used with isActionPressed. +-- (DEPRECATED, use getBooleanActionValue) Values that can be used with isActionPressed. -- @field [parent=#input] #ACTION ACTION --- @@ -197,10 +197,10 @@ -- @field [parent=#CONTROLLER_AXIS] #number RightY Right stick vertical axis (from -1 to 1) -- @field [parent=#CONTROLLER_AXIS] #number TriggerLeft Left trigger (from 0 to 1) -- @field [parent=#CONTROLLER_AXIS] #number TriggerRight Right trigger (from 0 to 1) --- @field [parent=#CONTROLLER_AXIS] #number LookUpDown View direction vertical axis (RightY by default, can be mapped to another axis in Options/Controls menu) --- @field [parent=#CONTROLLER_AXIS] #number LookLeftRight View direction horizontal axis (RightX by default, can be mapped to another axis in Options/Controls menu) --- @field [parent=#CONTROLLER_AXIS] #number MoveForwardBackward Movement forward/backward (LeftY by default, can be mapped to another axis in Options/Controls menu) --- @field [parent=#CONTROLLER_AXIS] #number MoveLeftRight Side movement (LeftX by default, can be mapped to another axis in Options/Controls menu) +-- @field [parent=#CONTROLLER_AXIS] #number LookUpDown (DEPRECATED, use the LookUpDown action) View direction vertical axis (RightY by default, can be mapped to another axis in Options/Controls menu) +-- @field [parent=#CONTROLLER_AXIS] #number LookLeftRight (DEPRECATED, use the LookLeftRight action) View direction horizontal axis (RightX by default, can be mapped to another axis in Options/Controls menu) +-- @field [parent=#CONTROLLER_AXIS] #number MoveForwardBackward (DEPRECATED, use the MoveForwardBackward action) Movement forward/backward (LeftY by default, can be mapped to another axis in Options/Controls menu) +-- @field [parent=#CONTROLLER_AXIS] #number MoveLeftRight (DEPRECATED, use the MoveLeftRight action) Side movement (LeftX by default, can be mapped to another axis in Options/Controls menu) --- -- Values that can be used with getAxisValue. @@ -337,4 +337,105 @@ -- @field [parent=#TouchEvent] openmw.util#Vector2 position Relative position on the touch device (0 to 1 from top left corner), -- @field [parent=#TouchEvent] #number pressure Pressure of the finger. +--- +-- @type ActionType + +--- +-- @type ACTION_TYPE +-- @field #ActionType Boolean Input action with value of true or false +-- @field #ActionType Number Input action with a numeric value +-- @field #ActionType Range Input action with a numeric value between 0 and 1 (inclusive) + +--- +-- Values that can be used in registerAction +-- @field [parent=#input] #ACTION_TYPE ACTION_TYPE + +--- +-- @type ActionInfo +-- @field [parent=#Actioninfo] #string key +-- @field [parent=#Actioninfo] #ActionType type +-- @field [parent=#Actioninfo] #string l10n Localization context containing the name and description keys +-- @field [parent=#Actioninfo] #string name Localization key of the action's name +-- @field [parent=#Actioninfo] #string description Localization key of the action's description +-- @field [parent=#Actioninfo] defaultValue initial value of the action + +--- +-- Map of all currently registered actions +-- @field [parent=#input] #map<#string,#ActionInfo> actions + +--- +-- Registers a new input action. The key must be unique +-- @function [parent=#input] registerAction +-- @param #ActionInfo info + +--- +-- Provides a function computing the value of given input action. +-- The callback is called once a frame, after the values of dependency actions are resolved. +-- Throws an error if a cyclic action dependency is detected. +-- @function [parent=#input] bindAction +-- @param #string key +-- @param openmw.async#Callback callback returning the new value of the action, and taking as arguments: +-- frame time in seconds, +-- value of the function, +-- value of the first dependency action, +-- ... +-- @param #list<#string> dependencies +-- @usage +-- input.bindAction('Activate', async:callback(function(dt, use, sneak, run) +-- -- while sneaking, only activate things while holding the run binding +-- return use and (run or not sneak) +-- end), { 'Sneak', 'Run' }) + +--- +-- Registers a function to be called whenever the action's value changes +-- @function [parent=#input] registerActionHandler +-- @param #string key +-- @param openmw.async#Callback callback takes the new action value as the only argument + +--- +-- Returns the value of a Boolean action +-- @function [parent=#input] getBooleanActionValue +-- @param #string key +-- @return #boolean + +--- +-- Returns the value of a Number action +-- @function [parent=#input] getNumberActionValue +-- @param #string key +-- @return #number + +--- +-- Returns the value of a Range action +-- @function [parent=#input] getRangeActionValue +-- @param #string key +-- @return #number + +--- +-- @type TriggerInfo +-- @field [parent=#TriggerInfo] #string key +-- @field [parent=#TriggerInfo] #string l10n Localization context containing the name and description keys +-- @field [parent=#TriggerInfo] #string name Localization key of the trigger's name +-- @field [parent=#TriggerInfo] #string description Localization key of the trigger's description + +--- +-- Map of all currently registered triggers +-- @field [parent=#input] #map<#string,#TriggerInfo> triggers + +--- +-- Registers a new input trigger. The key must be unique +-- @function [parent=#input] registerTrigger +-- @param #TriggerInfo info + +--- +-- Registers a function to be called whenever the trigger activates +-- @function [parent=#input] registerTriggerHandler +-- @param #string key +-- @param openmw.async#Callback callback takes the new action value as the only argument + +--- +-- Activates the trigger with the given key +-- @function [parent=#input] activateTrigger +-- @param #string key + + return nil diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index 0c4f19d19f..ad30994fe8 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -210,14 +210,14 @@ -- end -- @usage -- Check for a specific effect -- local effect = Actor.activeEffects(self):getEffect(core.magic.EFFECT_TYPE.Telekinesis) --- if effect then +-- if effect.magnitude ~= 0 then -- print(effect.id..', attribute='..tostring(effect.affectedAttribute)..', skill='..tostring(effect.affectedSkill)..', magnitude='..tostring(effect.magnitude)) -- else -- print('No Telekinesis effect') -- end -- @usage -- Check for a specific effect targeting a specific attribute. -- local effect = Actor.activeEffects(self):getEffect(core.magic.EFFECT_TYPE.FortifyAttribute, core.ATTRIBUTE.Luck) --- if effect then +-- if effect.magnitude ~= 0 then -- print(effect.id..', attribute='..tostring(effect.affectedAttribute)..', skill='..tostring(effect.affectedSkill)..', magnitude='..tostring(effect.magnitude)) -- else -- print('No Fortify Luck effect') @@ -229,7 +229,7 @@ -- @param self -- @param #string effectId effect ID -- @param #string extraParam Optional skill or attribute ID --- @return openmw.core#ActiveEffect if such an effect is active, nil otherwise +-- @return openmw.core#ActiveEffect --- -- Completely removes the active effect from the actor. @@ -846,14 +846,14 @@ -- NPC.modifyFactionReputation(player, "mages guild", 5); --- --- Expell NPC from given faction. +-- Expel NPC from given faction. -- Throws an exception if there is no such faction. -- Note: expelled NPC still keeps his rank and reputation in faction, he just get an additonal flag for given faction. --- @function [parent=#NPC] expell +-- @function [parent=#NPC] expel -- @param openmw.core#GameObject actor NPC object -- @param #string faction Faction ID -- @usage local NPC = require('openmw.types').NPC; --- NPC.expell(player, "mages guild"); +-- NPC.expel(player, "mages guild"); --- -- Clear expelling of NPC from given faction. diff --git a/files/lua_api/openmw/world.lua b/files/lua_api/openmw/world.lua index 5baa624c5d..404b744eb8 100644 --- a/files/lua_api/openmw/world.lua +++ b/files/lua_api/openmw/world.lua @@ -111,6 +111,11 @@ -- @function [parent=#world] setGameTimeScale -- @param #number ratio +--- +-- Frame duration in seconds +-- @function [parent=#world] getRealFrameDuration +-- @return #number + --- -- Whether the world is paused (onUpdate doesn't work when the world is paused). -- @function [parent=#world] isWorldPaused diff --git a/files/settings-default.cfg b/files/settings-default.cfg index da1c97519a..4a90a46cc5 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -1136,8 +1136,8 @@ weathersnow = meshes/snow.nif # Blizzard weather effect weatherblizzard = meshes/blizzard.nif -# Enable to write logs when loading unsupported nif file -write nif debug log = true +# Enable to write logs when loading NIF files +write nif debug log = false [Groundcover] diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt index 1b73acf758..ca0c264ade 100644 --- a/files/shaders/CMakeLists.txt +++ b/files/shaders/CMakeLists.txt @@ -16,6 +16,7 @@ set(SHADER_FILES lib/particle/occlusion.glsl lib/util/quickstep.glsl lib/util/coordinates.glsl + lib/util/distortion.glsl lib/core/fragment.glsl lib/core/fragment.h.glsl lib/core/fragment_multiview.glsl @@ -45,6 +46,7 @@ set(SHADER_FILES compatibility/shadowcasting.vert compatibility/shadowcasting.frag compatibility/vertexcolors.glsl + compatibility/normals.glsl compatibility/multiview_resolve.vert compatibility/multiview_resolve.frag compatibility/depthclipped.vert diff --git a/files/shaders/compatibility/bs/default.frag b/files/shaders/compatibility/bs/default.frag index f1be8da80c..77131c6a52 100644 --- a/files/shaders/compatibility/bs/default.frag +++ b/files/shaders/compatibility/bs/default.frag @@ -1,5 +1,5 @@ #version 120 -#pragma import_defines(FORCE_OPAQUE) +#pragma import_defines(FORCE_OPAQUE, DISTORTION) #if @useUBO #extension GL_ARB_uniform_buffer_object : require @@ -24,9 +24,10 @@ varying vec2 emissiveMapUV; #if @normalMap uniform sampler2D normalMap; varying vec2 normalMapUV; -varying vec4 passTangent; #endif +uniform sampler2D opaqueDepthTex; + varying float euclideanDepth; varying float linearDepth; @@ -39,20 +40,30 @@ uniform float alphaRef; uniform float emissiveMult; uniform float specStrength; uniform bool useTreeAnim; +uniform float distortionStrength; #include "lib/light/lighting.glsl" #include "lib/material/alpha.glsl" +#include "lib/util/distortion.glsl" #include "compatibility/vertexcolors.glsl" #include "compatibility/shadows_fragment.glsl" #include "compatibility/fog.glsl" +#include "compatibility/normals.glsl" void main() { - vec3 normal = normalize(passNormal); - #if @diffuseMap gl_FragData[0] = texture2D(diffuseMap, diffuseMapUV); + +#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); + + return; +#endif + gl_FragData[0].a *= coveragePreservingAlphaScale(diffuseMap, diffuseMapUV); #else gl_FragData[0] = vec4(1.0); @@ -63,39 +74,30 @@ void main() gl_FragData[0].a *= diffuseColor.a; gl_FragData[0].a = alphaTest(gl_FragData[0].a, alphaRef); + vec3 specularColor = getSpecularColor().xyz; #if @normalMap vec4 normalTex = texture2D(normalMap, normalMapUV); - - vec3 normalizedNormal = normal; - vec3 normalizedTangent = normalize(passTangent.xyz); - vec3 binormal = cross(normalizedTangent, normalizedNormal) * passTangent.w; - mat3 tbnTranspose = mat3(normalizedTangent, binormal, normalizedNormal); - - normal = normalize(tbnTranspose * (normalTex.xyz * 2.0 - 1.0)); + vec3 viewNormal = normalToView(normalTex.xyz * 2.0 - 1.0); + specularColor *= normalTex.a; +#else + vec3 viewNormal = normalize(gl_NormalMatrix * passNormal); #endif - vec3 viewNormal = normalize(gl_NormalMatrix * normal); float shadowing = unshadowedLightRatio(linearDepth); - vec3 diffuseLight, ambientLight; - doLighting(passViewPos, viewNormal, shadowing, diffuseLight, ambientLight); + vec3 diffuseLight, ambientLight, specularLight; + doLighting(passViewPos, viewNormal, gl_FrontMaterial.shininess, shadowing, diffuseLight, ambientLight, specularLight); + vec3 diffuse = diffuseColor.xyz * diffuseLight; + vec3 ambient = getAmbientColor().xyz * ambientLight; vec3 emission = getEmissionColor().xyz * emissiveMult; #if @emissiveMap emission *= texture2D(emissiveMap, emissiveMapUV).xyz; #endif - vec3 lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + emission; + vec3 lighting = diffuse + ambient + emission; + vec3 specular = specularColor * specularLight * specStrength; clampLightingResult(lighting); - gl_FragData[0].xyz *= lighting; - - float shininess = gl_FrontMaterial.shininess; - vec3 matSpec = getSpecularColor().xyz * specStrength; -#if @normalMap - matSpec *= normalTex.a; -#endif - - if (matSpec != vec3(0.0)) - gl_FragData[0].xyz += matSpec * getSpecular(viewNormal, passViewPos, shininess, shadowing); + gl_FragData[0].xyz = gl_FragData[0].xyz * lighting + specular; gl_FragData[0] = applyFogAtDist(gl_FragData[0], euclideanDepth, linearDepth, far); diff --git a/files/shaders/compatibility/bs/default.vert b/files/shaders/compatibility/bs/default.vert index 712a3f3d0c..21942ec91e 100644 --- a/files/shaders/compatibility/bs/default.vert +++ b/files/shaders/compatibility/bs/default.vert @@ -36,6 +36,7 @@ varying vec3 passNormal; #include "compatibility/vertexcolors.glsl" #include "compatibility/shadows_vertex.glsl" +#include "compatibility/normals.glsl" void main(void) { @@ -45,6 +46,14 @@ void main(void) gl_ClipVertex = viewPos; euclideanDepth = length(viewPos.xyz); linearDepth = getLinearDepth(gl_Position.z, viewPos.z); + passColor = gl_Color; + passViewPos = viewPos.xyz; + passNormal = gl_Normal.xyz; + normalToViewMatrix = gl_NormalMatrix; + +#if @normalMap + normalToViewMatrix *= generateTangentSpace(gl_MultiTexCoord7.xyzw, passNormal); +#endif #if @diffuseMap diffuseMapUV = (gl_TextureMatrix[@diffuseMapUV] * gl_MultiTexCoord@diffuseMapUV).xy; @@ -56,15 +65,11 @@ void main(void) #if @normalMap normalMapUV = (gl_TextureMatrix[@normalMapUV] * gl_MultiTexCoord@normalMapUV).xy; - passTangent = gl_MultiTexCoord7.xyzw; #endif - passColor = gl_Color; - passViewPos = viewPos.xyz; - passNormal = gl_Normal.xyz; #if @shadows_enabled - vec3 viewNormal = normalize((gl_NormalMatrix * gl_Normal).xyz); + vec3 viewNormal = normalize(gl_NormalMatrix * passNormal); setupShadowCoords(viewPos, viewNormal); #endif } diff --git a/files/shaders/compatibility/bs/nolighting.vert b/files/shaders/compatibility/bs/nolighting.vert index 3b0fa7b626..57cedc6e94 100644 --- a/files/shaders/compatibility/bs/nolighting.vert +++ b/files/shaders/compatibility/bs/nolighting.vert @@ -63,7 +63,7 @@ void main(void) } #if @shadows_enabled - vec3 viewNormal = normalize((gl_NormalMatrix * gl_Normal).xyz); + vec3 viewNormal = normalize(gl_NormalMatrix * passNormal); setupShadowCoords(viewPos, viewNormal); #endif } diff --git a/files/shaders/compatibility/groundcover.frag b/files/shaders/compatibility/groundcover.frag index 9dfd71b32e..dfdd6518c3 100644 --- a/files/shaders/compatibility/groundcover.frag +++ b/files/shaders/compatibility/groundcover.frag @@ -18,7 +18,6 @@ varying vec2 diffuseMapUV; #if @normalMap uniform sampler2D normalMap; varying vec2 normalMapUV; -varying vec4 passTangent; #endif // Other shaders respect forcePPL, but legacy groundcover mods were designed to work with vertex lighting. @@ -44,23 +43,10 @@ varying vec3 passNormal; #include "lib/light/lighting.glsl" #include "lib/material/alpha.glsl" #include "fog.glsl" +#include "compatibility/normals.glsl" void main() { - vec3 normal = normalize(passNormal); - -#if @normalMap - vec4 normalTex = texture2D(normalMap, normalMapUV); - - vec3 normalizedNormal = normal; - vec3 normalizedTangent = normalize(passTangent.xyz); - vec3 binormal = cross(normalizedTangent, normalizedNormal) * passTangent.w; - mat3 tbnTranspose = mat3(normalizedTangent, binormal, normalizedNormal); - - normal = normalize(tbnTranspose * (normalTex.xyz * 2.0 - 1.0)); -#endif - vec3 viewNormal = normalize(gl_NormalMatrix * normal); - #if @diffuseMap gl_FragData[0] = texture2D(diffuseMap, diffuseMapUV); #else @@ -72,14 +58,20 @@ void main() gl_FragData[0].a = alphaTest(gl_FragData[0].a, alphaRef); +#if @normalMap + vec3 viewNormal = normalToView(texture2D(normalMap, normalMapUV).xyz * 2.0 - 1.0); +#else + vec3 viewNormal = normalToView(normalize(passNormal)); +#endif + float shadowing = unshadowedLightRatio(linearDepth); vec3 lighting; #if !PER_PIXEL_LIGHTING lighting = passLighting + shadowDiffuseLighting * shadowing; #else - vec3 diffuseLight, ambientLight; - doLighting(passViewPos, viewNormal, shadowing, diffuseLight, ambientLight); + vec3 diffuseLight, ambientLight, specularLight; + doLighting(passViewPos, viewNormal, gl_FrontMaterial.shininess, shadowing, diffuseLight, ambientLight, specularLight); lighting = diffuseLight + ambientLight; #endif diff --git a/files/shaders/compatibility/groundcover.vert b/files/shaders/compatibility/groundcover.vert index dd8d083a47..8cf53a19e0 100644 --- a/files/shaders/compatibility/groundcover.vert +++ b/files/shaders/compatibility/groundcover.vert @@ -21,7 +21,6 @@ varying vec2 diffuseMapUV; #if @normalMap varying vec2 normalMapUV; -varying vec4 passTangent; #endif // Other shaders respect forcePPL, but legacy groundcover mods were designed to work with vertex lighting. @@ -41,6 +40,7 @@ centroid varying vec3 shadowDiffuseLighting; varying vec3 passNormal; #include "shadows_vertex.glsl" +#include "compatibility/normals.glsl" #include "lib/light/lighting.glsl" #include "lib/view/depth.glsl" @@ -149,8 +149,14 @@ void main(void) linearDepth = getLinearDepth(gl_Position.z, viewPos.z); + passNormal = rotation3(rotation) * gl_Normal.xyz; + normalToViewMatrix = gl_NormalMatrix; +#if @normalMap + normalToViewMatrix *= generateTangentSpace(gl_MultiTexCoord7.xyzw * rotation, passNormal); +#endif + #if (!PER_PIXEL_LIGHTING || @shadows_enabled) - vec3 viewNormal = normalize((gl_NormalMatrix * rotation3(rotation) * gl_Normal).xyz); + vec3 viewNormal = normalize(gl_NormalMatrix * passNormal); #endif #if @diffuseMap @@ -159,15 +165,14 @@ void main(void) #if @normalMap normalMapUV = (gl_TextureMatrix[@normalMapUV] * gl_MultiTexCoord@normalMapUV).xy; - passTangent = gl_MultiTexCoord7.xyzw * rotation; #endif - passNormal = rotation3(rotation) * gl_Normal.xyz; #if PER_PIXEL_LIGHTING passViewPos = viewPos.xyz; #else - vec3 diffuseLight, ambientLight; - doLighting(viewPos.xyz, viewNormal, diffuseLight, ambientLight, shadowDiffuseLighting); + vec3 diffuseLight, ambientLight, specularLight; + vec3 unusedShadowSpecular; + doLighting(viewPos.xyz, viewNormal, gl_FrontMaterial.shininess, diffuseLight, ambientLight, specularLight, shadowDiffuseLighting, unusedShadowSpecular); passLighting = diffuseLight + ambientLight; clampLightingResult(passLighting); #endif diff --git a/files/shaders/compatibility/normals.glsl b/files/shaders/compatibility/normals.glsl new file mode 100644 index 0000000000..8df16a4b12 --- /dev/null +++ b/files/shaders/compatibility/normals.glsl @@ -0,0 +1,14 @@ +varying mat3 normalToViewMatrix; + +mat3 generateTangentSpace(vec4 tangent, vec3 normal) +{ + vec3 normalizedNormal = normalize(normal); + vec3 normalizedTangent = normalize(tangent.xyz); + vec3 binormal = cross(normalizedTangent, normalizedNormal) * tangent.w; + return mat3(normalizedTangent, binormal, normalizedNormal); +} + +vec3 normalToView(vec3 normal) +{ + return normalize(normalToViewMatrix * normal); +} diff --git a/files/shaders/compatibility/objects.frag b/files/shaders/compatibility/objects.frag index b86678af87..56c7abf27c 100644 --- a/files/shaders/compatibility/objects.frag +++ b/files/shaders/compatibility/objects.frag @@ -1,5 +1,5 @@ #version 120 -#pragma import_defines(FORCE_OPAQUE) +#pragma import_defines(FORCE_OPAQUE, DISTORTION) #if @useUBO #extension GL_ARB_uniform_buffer_object : require @@ -37,7 +37,6 @@ varying vec2 emissiveMapUV; #if @normalMap uniform sampler2D normalMap; varying vec2 normalMapUV; -varying vec4 passTangent; #endif #if @envMap @@ -67,18 +66,24 @@ uniform vec2 screenRes; uniform float near; uniform float far; uniform float alphaRef; +uniform float distortionStrength; -#define PER_PIXEL_LIGHTING (@normalMap || @forcePPL) +#define PER_PIXEL_LIGHTING (@normalMap || @specularMap || @forcePPL) #if !PER_PIXEL_LIGHTING centroid varying vec3 passLighting; +centroid varying vec3 passSpecular; centroid varying vec3 shadowDiffuseLighting; +centroid varying vec3 shadowSpecularLighting; #else uniform float emissiveMult; -#endif uniform float specStrength; +#endif varying vec3 passViewPos; varying vec3 passNormal; +#if @normalMap || @diffuseParallax +varying vec4 passTangent; +#endif #if @additiveBlending #define ADDITIVE_BLENDING @@ -87,15 +92,16 @@ varying vec3 passNormal; #include "lib/light/lighting.glsl" #include "lib/material/parallax.glsl" #include "lib/material/alpha.glsl" +#include "lib/util/distortion.glsl" #include "fog.glsl" #include "vertexcolors.glsl" #include "shadows_fragment.glsl" +#include "compatibility/normals.glsl" #if @softParticles #include "lib/particle/soft.glsl" -uniform sampler2D opaqueDepthTex; uniform float particleSize; uniform bool particleFade; uniform float softFalloffDepth; @@ -107,53 +113,45 @@ uniform sampler2D orthoDepthMap; varying vec3 orthoDepthMapCoord; #endif +uniform sampler2D opaqueDepthTex; + void main() { #if @particleOcclusion applyOcclusionDiscard(orthoDepthMapCoord, texture2D(orthoDepthMap, orthoDepthMapCoord.xy * 0.5 + 0.5).r); #endif -#if @diffuseMap - vec2 adjustedDiffuseUV = diffuseMapUV; -#endif - - vec3 normal = normalize(passNormal); - vec3 viewVec = normalize(passViewPos.xyz); - -#if @normalMap - vec4 normalTex = texture2D(normalMap, normalMapUV); - - vec3 normalizedNormal = normal; - vec3 normalizedTangent = normalize(passTangent.xyz); - vec3 binormal = cross(normalizedTangent, normalizedNormal) * passTangent.w; - mat3 tbnTranspose = mat3(normalizedTangent, binormal, normalizedNormal); - - normal = normalize(tbnTranspose * (normalTex.xyz * 2.0 - 1.0)); -#endif + // only offset diffuse and normal maps for now, other textures are more likely to be using a completely different UV set + vec2 offset = vec2(0.0); +#if @parallax || @diffuseParallax #if @parallax - vec3 cameraPos = (gl_ModelViewMatrixInverse * vec4(0,0,0,1)).xyz; - vec3 objectPos = (gl_ModelViewMatrixInverse * vec4(passViewPos, 1)).xyz; - vec3 eyeDir = normalize(cameraPos - objectPos); - vec2 offset = getParallaxOffset(eyeDir, tbnTranspose, normalTex.a, (passTangent.w > 0.0) ? -1.f : 1.f); - adjustedDiffuseUV += offset; // only offset diffuse for now, other textures are more likely to be using a completely different UV set - - // TODO: check not working as the same UV buffer is being bound to different targets - // if diffuseMapUV == normalMapUV -#if 1 - // fetch a new normal using updated coordinates - normalTex = texture2D(normalMap, adjustedDiffuseUV); - - normal = normalize(tbnTranspose * (normalTex.xyz * 2.0 - 1.0)); + float height = texture2D(normalMap, normalMapUV).a; + float flipY = (passTangent.w > 0.0) ? -1.f : 1.f; +#else + float height = texture2D(diffuseMap, diffuseMapUV).a; + // FIXME: shouldn't be necessary, but in this path false-positives are common + float flipY = -1.f; +#endif + offset = getParallaxOffset(transpose(normalToViewMatrix) * normalize(-passViewPos), height, flipY); #endif -#endif - -vec3 viewNormal = normalize(gl_NormalMatrix * normal); +vec2 screenCoords = gl_FragCoord.xy / screenRes; #if @diffuseMap - gl_FragData[0] = texture2D(diffuseMap, adjustedDiffuseUV); - gl_FragData[0].a *= coveragePreservingAlphaScale(diffuseMap, adjustedDiffuseUV); + gl_FragData[0] = texture2D(diffuseMap, diffuseMapUV + offset); + +#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); + return; +#endif + +#if @diffuseParallax + gl_FragData[0].a = 1.0; +#else + gl_FragData[0].a *= coveragePreservingAlphaScale(diffuseMap, diffuseMapUV + offset); +#endif #else gl_FragData[0] = vec4(1.0); #endif @@ -168,6 +166,14 @@ vec3 viewNormal = normalize(gl_NormalMatrix * normal); gl_FragData[0].a = alphaTest(gl_FragData[0].a, alphaRef); +#if @normalMap + vec3 viewNormal = normalToView(texture2D(normalMap, normalMapUV + offset).xyz * 2.0 - 1.0); +#else + vec3 viewNormal = normalize(gl_NormalMatrix * passNormal); +#endif + + vec3 viewVec = normalize(passViewPos); + #if @detailMap gl_FragData[0].xyz *= texture2D(detailMap, detailMapUV).xyz * 2.0; #endif @@ -208,19 +214,27 @@ vec3 viewNormal = normalize(gl_NormalMatrix * normal); #endif float shadowing = unshadowedLightRatio(-passViewPos.z); - vec3 lighting; + vec3 lighting, specular; #if !PER_PIXEL_LIGHTING lighting = passLighting + shadowDiffuseLighting * shadowing; + specular = passSpecular + shadowSpecularLighting * shadowing; #else - vec3 diffuseLight, ambientLight; - doLighting(passViewPos, viewNormal, shadowing, diffuseLight, ambientLight); - vec3 emission = getEmissionColor().xyz * emissiveMult; - lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + emission; +#if @specularMap + vec4 specTex = texture2D(specularMap, specularMapUV); + float shininess = specTex.a * 255.0; + vec3 specularColor = specTex.xyz; +#else + float shininess = gl_FrontMaterial.shininess; + vec3 specularColor = getSpecularColor().xyz; +#endif + vec3 diffuseLight, ambientLight, specularLight; + doLighting(passViewPos, viewNormal, shininess, shadowing, diffuseLight, ambientLight, specularLight); + lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + getEmissionColor().xyz * emissiveMult; + specular = specularColor * specularLight * specStrength; #endif clampLightingResult(lighting); - - gl_FragData[0].xyz *= lighting; + gl_FragData[0].xyz = gl_FragData[0].xyz * lighting + specular; #if @envMap && !@preLightEnv gl_FragData[0].xyz += envEffect; @@ -230,24 +244,8 @@ vec3 viewNormal = normalize(gl_NormalMatrix * normal); gl_FragData[0].xyz += texture2D(emissiveMap, emissiveMapUV).xyz; #endif -#if @specularMap - vec4 specTex = texture2D(specularMap, specularMapUV); - float shininess = specTex.a * 255.0; - vec3 matSpec = specTex.xyz; -#else - float shininess = gl_FrontMaterial.shininess; - vec3 matSpec = getSpecularColor().xyz; -#endif - - matSpec *= specStrength; - if (matSpec != vec3(0.0)) - { - gl_FragData[0].xyz += matSpec * getSpecular(viewNormal, passViewPos, shininess, shadowing); - } - gl_FragData[0] = applyFogAtPos(gl_FragData[0], passViewPos, far); - vec2 screenCoords = gl_FragCoord.xy / screenRes; #if !defined(FORCE_OPAQUE) && @softParticles gl_FragData[0].a *= calcSoftParticleFade( viewVec, diff --git a/files/shaders/compatibility/objects.vert b/files/shaders/compatibility/objects.vert index 1ea4a1553f..081ff909cf 100644 --- a/files/shaders/compatibility/objects.vert +++ b/files/shaders/compatibility/objects.vert @@ -31,7 +31,6 @@ varying vec2 emissiveMapUV; #if @normalMap varying vec2 normalMapUV; -varying vec4 passTangent; #endif #if @envMap @@ -50,18 +49,25 @@ varying vec2 specularMapUV; varying vec2 glossMapUV; #endif -#define PER_PIXEL_LIGHTING (@normalMap || @forcePPL) +#define PER_PIXEL_LIGHTING (@normalMap || @specularMap || @forcePPL) #if !PER_PIXEL_LIGHTING centroid varying vec3 passLighting; +centroid varying vec3 passSpecular; centroid varying vec3 shadowDiffuseLighting; +centroid varying vec3 shadowSpecularLighting; uniform float emissiveMult; +uniform float specStrength; #endif varying vec3 passViewPos; varying vec3 passNormal; +#if @normalMap || @diffuseParallax +varying vec4 passTangent; +#endif #include "vertexcolors.glsl" #include "shadows_vertex.glsl" +#include "compatibility/normals.glsl" #include "lib/light/lighting.glsl" #include "lib/view/depth.glsl" @@ -84,9 +90,18 @@ void main(void) vec4 viewPos = modelToView(gl_Vertex); gl_ClipVertex = viewPos; + passColor = gl_Color; + passViewPos = viewPos.xyz; + passNormal = gl_Normal.xyz; + normalToViewMatrix = gl_NormalMatrix; -#if (@envMap || !PER_PIXEL_LIGHTING || @shadows_enabled) - vec3 viewNormal = normalize((gl_NormalMatrix * gl_Normal).xyz); +#if @normalMap || @diffuseParallax + passTangent = gl_MultiTexCoord7.xyzw; + normalToViewMatrix *= generateTangentSpace(passTangent, passNormal); +#endif + +#if @envMap || !PER_PIXEL_LIGHTING || @shadows_enabled + vec3 viewNormal = normalize(gl_NormalMatrix * passNormal); #endif #if @envMap @@ -118,7 +133,6 @@ void main(void) #if @normalMap normalMapUV = (gl_TextureMatrix[@normalMapUV] * gl_MultiTexCoord@normalMapUV).xy; - passTangent = gl_MultiTexCoord7.xyzw; #endif #if @bumpMap @@ -133,17 +147,14 @@ void main(void) glossMapUV = (gl_TextureMatrix[@glossMapUV] * gl_MultiTexCoord@glossMapUV).xy; #endif - passColor = gl_Color; - passViewPos = viewPos.xyz; - passNormal = gl_Normal.xyz; - #if !PER_PIXEL_LIGHTING - vec3 diffuseLight, ambientLight; - doLighting(viewPos.xyz, viewNormal, diffuseLight, ambientLight, shadowDiffuseLighting); - vec3 emission = getEmissionColor().xyz * emissiveMult; - passLighting = getDiffuseColor().xyz * diffuseLight + getAmbientColor().xyz * ambientLight + emission; + vec3 diffuseLight, ambientLight, specularLight; + doLighting(viewPos.xyz, viewNormal, gl_FrontMaterial.shininess, diffuseLight, ambientLight, specularLight, shadowDiffuseLighting, shadowSpecularLighting); + passLighting = getDiffuseColor().xyz * diffuseLight + getAmbientColor().xyz * ambientLight + getEmissionColor().xyz * emissiveMult; + passSpecular = getSpecularColor().xyz * specularLight * specStrength; clampLightingResult(passLighting); shadowDiffuseLighting *= getDiffuseColor().xyz; + shadowSpecularLighting *= getSpecularColor().xyz * specStrength; #endif #if (@shadows_enabled) diff --git a/files/shaders/compatibility/terrain.frag b/files/shaders/compatibility/terrain.frag index 744a56d18b..abc7425eb0 100644 --- a/files/shaders/compatibility/terrain.frag +++ b/files/shaders/compatibility/terrain.frag @@ -23,11 +23,13 @@ uniform sampler2D blendMap; varying float euclideanDepth; varying float linearDepth; -#define PER_PIXEL_LIGHTING (@normalMap || @forcePPL) +#define PER_PIXEL_LIGHTING (@normalMap || @specularMap || @forcePPL) #if !PER_PIXEL_LIGHTING centroid varying vec3 passLighting; +centroid varying vec3 passSpecular; centroid varying vec3 shadowDiffuseLighting; +centroid varying vec3 shadowSpecularLighting; #endif varying vec3 passViewPos; varying vec3 passNormal; @@ -40,76 +42,53 @@ uniform float far; #include "lib/light/lighting.glsl" #include "lib/material/parallax.glsl" #include "fog.glsl" +#include "compatibility/normals.glsl" void main() { vec2 adjustedUV = (gl_TextureMatrix[0] * vec4(uv, 0.0, 1.0)).xy; - vec3 normal = normalize(passNormal); - -#if @normalMap - vec4 normalTex = texture2D(normalMap, adjustedUV); - - vec3 normalizedNormal = normal; - vec3 tangent = vec3(1.0, 0.0, 0.0); - vec3 binormal = normalize(cross(tangent, normalizedNormal)); - tangent = normalize(cross(normalizedNormal, binormal)); // note, now we need to re-cross to derive tangent again because it wasn't orthonormal - mat3 tbnTranspose = mat3(tangent, binormal, normalizedNormal); - - normal = tbnTranspose * (normalTex.xyz * 2.0 - 1.0); -#endif - #if @parallax - vec3 cameraPos = (gl_ModelViewMatrixInverse * vec4(0,0,0,1)).xyz; - vec3 objectPos = (gl_ModelViewMatrixInverse * vec4(passViewPos, 1)).xyz; - vec3 eyeDir = normalize(cameraPos - objectPos); - adjustedUV += getParallaxOffset(eyeDir, tbnTranspose, normalTex.a, 1.f); - - // update normal using new coordinates - normalTex = texture2D(normalMap, adjustedUV); - - normal = tbnTranspose * (normalTex.xyz * 2.0 - 1.0); + adjustedUV += getParallaxOffset(transpose(normalToViewMatrix) * normalize(-passViewPos), texture2D(normalMap, adjustedUV).a, 1.f); #endif - - vec3 viewNormal = normalize(gl_NormalMatrix * normal); - vec4 diffuseTex = texture2D(diffuseMap, adjustedUV); gl_FragData[0] = vec4(diffuseTex.xyz, 1.0); + vec4 diffuseColor = getDiffuseColor(); + gl_FragData[0].a *= diffuseColor.a; + #if @blendMap vec2 blendMapUV = (gl_TextureMatrix[1] * vec4(uv, 0.0, 1.0)).xy; gl_FragData[0].a *= texture2D(blendMap, blendMapUV).a; #endif - vec4 diffuseColor = getDiffuseColor(); - gl_FragData[0].a *= diffuseColor.a; +#if @normalMap + vec3 viewNormal = normalToView(texture2D(normalMap, adjustedUV).xyz * 2.0 - 1.0); +#else + vec3 viewNormal = normalize(gl_NormalMatrix * passNormal); +#endif float shadowing = unshadowedLightRatio(linearDepth); - vec3 lighting; + vec3 lighting, specular; #if !PER_PIXEL_LIGHTING lighting = passLighting + shadowDiffuseLighting * shadowing; + specular = passSpecular + shadowSpecularLighting * shadowing; #else - vec3 diffuseLight, ambientLight; - doLighting(passViewPos, viewNormal, shadowing, diffuseLight, ambientLight); +#if @specularMap + float shininess = 128.0; // TODO: make configurable + vec3 specularColor = vec3(diffuseTex.a); +#else + float shininess = gl_FrontMaterial.shininess; + vec3 specularColor = getSpecularColor().xyz; +#endif + vec3 diffuseLight, ambientLight, specularLight; + doLighting(passViewPos, viewNormal, shininess, shadowing, diffuseLight, ambientLight, specularLight); lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + getEmissionColor().xyz; + specular = specularColor * specularLight; #endif clampLightingResult(lighting); - - gl_FragData[0].xyz *= lighting; - -#if @specularMap - float shininess = 128.0; // TODO: make configurable - vec3 matSpec = vec3(diffuseTex.a); -#else - float shininess = gl_FrontMaterial.shininess; - vec3 matSpec = getSpecularColor().xyz; -#endif - - if (matSpec != vec3(0.0)) - { - gl_FragData[0].xyz += matSpec * getSpecular(viewNormal, passViewPos, shininess, shadowing); - } + gl_FragData[0].xyz = gl_FragData[0].xyz * lighting + specular; gl_FragData[0] = applyFogAtDist(gl_FragData[0], euclideanDepth, linearDepth, far); diff --git a/files/shaders/compatibility/terrain.vert b/files/shaders/compatibility/terrain.vert index 5e154d912a..cbfb7769ba 100644 --- a/files/shaders/compatibility/terrain.vert +++ b/files/shaders/compatibility/terrain.vert @@ -13,17 +13,20 @@ varying vec2 uv; varying float euclideanDepth; varying float linearDepth; -#define PER_PIXEL_LIGHTING (@normalMap || @forcePPL) +#define PER_PIXEL_LIGHTING (@normalMap || @specularMap || @forcePPL) #if !PER_PIXEL_LIGHTING centroid varying vec3 passLighting; +centroid varying vec3 passSpecular; centroid varying vec3 shadowDiffuseLighting; +centroid varying vec3 shadowSpecularLighting; #endif varying vec3 passViewPos; varying vec3 passNormal; #include "vertexcolors.glsl" #include "shadows_vertex.glsl" +#include "compatibility/normals.glsl" #include "lib/light/lighting.glsl" #include "lib/view/depth.glsl" @@ -37,20 +40,29 @@ void main(void) euclideanDepth = length(viewPos.xyz); linearDepth = getLinearDepth(gl_Position.z, viewPos.z); -#if (!PER_PIXEL_LIGHTING || @shadows_enabled) - vec3 viewNormal = normalize((gl_NormalMatrix * gl_Normal).xyz); -#endif - passColor = gl_Color; passNormal = gl_Normal.xyz; passViewPos = viewPos.xyz; + normalToViewMatrix = gl_NormalMatrix; + +#if @normalMap + mat3 tbnMatrix = generateTangentSpace(vec4(1.0, 0.0, 0.0, 1.0), passNormal); + tbnMatrix[0] = normalize(cross(tbnMatrix[2], tbnMatrix[1])); // note, now we need to re-cross to derive tangent again because it wasn't orthonormal + normalToViewMatrix *= tbnMatrix; +#endif + +#if !PER_PIXEL_LIGHTING || @shadows_enabled + vec3 viewNormal = normalize(gl_NormalMatrix * passNormal); +#endif #if !PER_PIXEL_LIGHTING - vec3 diffuseLight, ambientLight; - doLighting(viewPos.xyz, viewNormal, diffuseLight, ambientLight, shadowDiffuseLighting); + vec3 diffuseLight, ambientLight, specularLight; + doLighting(viewPos.xyz, viewNormal, gl_FrontMaterial.shininess, diffuseLight, ambientLight, specularLight, shadowDiffuseLighting, shadowSpecularLighting); passLighting = getDiffuseColor().xyz * diffuseLight + getAmbientColor().xyz * ambientLight + getEmissionColor().xyz; + passSpecular = getSpecularColor().xyz * specularLight; clampLightingResult(passLighting); shadowDiffuseLighting *= getDiffuseColor().xyz; + shadowSpecularLighting *= getSpecularColor().xyz; #endif uv = gl_MultiTexCoord0.xy; diff --git a/files/shaders/compatibility/water.frag b/files/shaders/compatibility/water.frag index 987dab10a6..5817b0c5ae 100644 --- a/files/shaders/compatibility/water.frag +++ b/files/shaders/compatibility/water.frag @@ -81,6 +81,7 @@ uniform float near; uniform float far; uniform float rainIntensity; +uniform bool enableRainRipples; uniform vec2 screenRes; @@ -113,7 +114,7 @@ void main(void) vec4 rainRipple; - if (rainIntensity > 0.01) + if (rainIntensity > 0.01 && enableRainRipples) rainRipple = rainCombined(position.xy/1000.0, waterTimer) * clamp(rainIntensity, 0.0, 1.0); else rainRipple = vec4(0.0); @@ -173,7 +174,7 @@ void main(void) vec3 waterColor = WATER_COLOR * sunFade; vec4 sunSpec = lcalcSpecular(0); - // alpha component is sun visibility; we want to start fading specularity when visibility is low + // alpha component is sun visibility; we want to start fading lighting effects when visibility is low sunSpec.a = min(1.0, sunSpec.a / SUN_SPEC_FADING_THRESHOLD); // artificial specularity to make rain ripples more noticeable @@ -202,7 +203,7 @@ void main(void) float sunHeight = lVec.z; vec3 scatterColour = mix(SCATTER_COLOUR*vec3(1.0,0.4,0.0), SCATTER_COLOUR, clamp(1.0-exp(-sunHeight*SUN_EXT), 0.0, 1.0)); vec3 lR = reflect(lVec, lNormal); - float lightScatter = clamp(dot(lVec,lNormal)*0.7+0.3, 0.0, 1.0) * clamp(dot(lR, vVec)*2.0-1.2, 0.0, 1.0) * SCATTER_AMOUNT * sunFade * clamp(1.0-exp(-sunHeight), 0.0, 1.0); + float lightScatter = clamp(dot(lVec,lNormal)*0.7+0.3, 0.0, 1.0) * clamp(dot(lR, vVec)*2.0-1.2, 0.0, 1.0) * SCATTER_AMOUNT * sunFade * sunSpec.a * clamp(1.0-exp(-sunHeight), 0.0, 1.0); gl_FragData[0].xyz = mix(mix(refraction, scatterColour, lightScatter), reflection, fresnel) + specular * sunSpec.rgb * sunSpec.a + rainSpecular; gl_FragData[0].w = 1.0; diff --git a/files/shaders/lib/light/lighting.glsl b/files/shaders/lib/light/lighting.glsl index 8351fce8a0..8bb6ba148f 100644 --- a/files/shaders/lib/light/lighting.glsl +++ b/files/shaders/lib/light/lighting.glsl @@ -3,15 +3,13 @@ #include "lighting_util.glsl" -void perLightSun(out vec3 diffuseOut, vec3 viewPos, vec3 viewNormal) +float calcLambert(vec3 viewNormal, vec3 lightDir, vec3 viewDir) { - vec3 lightDir = normalize(lcalcPosition(0)); - float lambert = dot(viewNormal.xyz, lightDir); - + float lambert = dot(viewNormal, lightDir); #ifndef GROUNDCOVER lambert = max(lambert, 0.0); #else - float eyeCosine = dot(normalize(viewPos), viewNormal.xyz); + float eyeCosine = dot(viewNormal, viewDir); if (lambert < 0.0) { lambert = -lambert; @@ -19,75 +17,7 @@ void perLightSun(out vec3 diffuseOut, vec3 viewPos, vec3 viewNormal) } lambert *= clamp(-8.0 * (1.0 - 0.3) * eyeCosine + 1.0, 0.3, 1.0); #endif - - diffuseOut = lcalcDiffuse(0).xyz * lambert; -} - -void perLightPoint(out vec3 ambientOut, out vec3 diffuseOut, int lightIndex, vec3 viewPos, vec3 viewNormal) -{ - vec3 lightPos = lcalcPosition(lightIndex) - viewPos; - float lightDistance = length(lightPos); - -// cull non-FFP point lighting by radius, light is guaranteed to not fall outside this bound with our cutoff -#if !@lightingMethodFFP - float radius = lcalcRadius(lightIndex); - - if (lightDistance > radius * 2.0) - { - ambientOut = vec3(0.0); - diffuseOut = vec3(0.0); - return; - } -#endif - - lightPos = normalize(lightPos); - - float illumination = lcalcIllumination(lightIndex, lightDistance); - ambientOut = lcalcAmbient(lightIndex) * illumination; - float lambert = dot(viewNormal.xyz, lightPos) * illumination; - -#ifndef GROUNDCOVER - lambert = max(lambert, 0.0); -#else - float eyeCosine = dot(normalize(viewPos), viewNormal.xyz); - if (lambert < 0.0) - { - lambert = -lambert; - eyeCosine = -eyeCosine; - } - lambert *= clamp(-8.0 * (1.0 - 0.3) * eyeCosine + 1.0, 0.3, 1.0); -#endif - - diffuseOut = lcalcDiffuse(lightIndex) * lambert; -} - -#if PER_PIXEL_LIGHTING -void doLighting(vec3 viewPos, vec3 viewNormal, float shadowing, out vec3 diffuseLight, out vec3 ambientLight) -#else -void doLighting(vec3 viewPos, vec3 viewNormal, out vec3 diffuseLight, out vec3 ambientLight, out vec3 shadowDiffuse) -#endif -{ - vec3 ambientOut, diffuseOut; - - perLightSun(diffuseOut, viewPos, viewNormal); - ambientLight = gl_LightModel.ambient.xyz; -#if PER_PIXEL_LIGHTING - diffuseLight = diffuseOut * shadowing; -#else - shadowDiffuse = diffuseOut; - diffuseLight = vec3(0.0); -#endif - - for (int i = @startLight; i < @endLight; ++i) - { -#if @lightingMethodUBO - perLightPoint(ambientOut, diffuseOut, PointLightIndex[i], viewPos, viewNormal); -#else - perLightPoint(ambientOut, diffuseOut, i, viewPos, viewNormal); -#endif - ambientLight += ambientOut; - diffuseLight += diffuseOut; - } + return lambert; } float calcSpecIntensity(vec3 viewNormal, vec3 viewDir, float shininess, vec3 lightDir) @@ -102,12 +32,28 @@ float calcSpecIntensity(vec3 viewNormal, vec3 viewDir, float shininess, vec3 lig return 0.0; } -vec3 getSpecular(vec3 viewNormal, vec3 viewPos, float shininess, float shadowing) +#if PER_PIXEL_LIGHTING +void doLighting(vec3 viewPos, vec3 viewNormal, float shininess, float shadowing, out vec3 diffuseLight, out vec3 ambientLight, out vec3 specularLight) +#else +void doLighting(vec3 viewPos, vec3 viewNormal, float shininess, out vec3 diffuseLight, out vec3 ambientLight, out vec3 specularLight, out vec3 shadowDiffuse, out vec3 shadowSpecular) +#endif { - shininess = max(shininess, 1e-4); vec3 viewDir = normalize(viewPos); - vec3 specularLight = lcalcSpecular(0).xyz * calcSpecIntensity(viewNormal, viewDir, shininess, normalize(lcalcPosition(0))); + shininess = max(shininess, 1e-4); + + vec3 sunDir = normalize(lcalcPosition(0)); + diffuseLight = lcalcDiffuse(0) * calcLambert(viewNormal, sunDir, viewDir); + ambientLight = gl_LightModel.ambient.xyz; + specularLight = lcalcSpecular(0).xyz * calcSpecIntensity(viewNormal, viewDir, shininess, sunDir); +#if PER_PIXEL_LIGHTING + diffuseLight *= shadowing; specularLight *= shadowing; +#else + shadowDiffuse = diffuseLight; + shadowSpecular = specularLight; + diffuseLight = vec3(0.0); + specularLight = vec3(0.0); +#endif for (int i = @startLight; i < @endLight; ++i) { @@ -116,21 +62,22 @@ vec3 getSpecular(vec3 viewNormal, vec3 viewPos, float shininess, float shadowing #else int lightIndex = i; #endif - vec3 lightPos = lcalcPosition(lightIndex) - viewPos; float lightDistance = length(lightPos); + // cull non-FFP point lighting by radius, light is guaranteed to not fall outside this bound with our cutoff #if !@lightingMethodFFP if (lightDistance > lcalcRadius(lightIndex) * 2.0) continue; #endif - float illumination = lcalcIllumination(lightIndex, lightDistance); - float intensity = calcSpecIntensity(viewNormal, viewDir, shininess, normalize(lightPos)); - specularLight += lcalcSpecular(lightIndex).xyz * intensity * illumination; - } + vec3 lightDir = lightPos / lightDistance; - return specularLight; + float illumination = lcalcIllumination(lightIndex, lightDistance); + diffuseLight += lcalcDiffuse(lightIndex) * calcLambert(viewNormal, lightDir, viewDir) * illumination; + ambientLight += lcalcAmbient(lightIndex) * illumination; + specularLight += lcalcSpecular(lightIndex).xyz * calcSpecIntensity(viewNormal, viewDir, shininess, lightDir) * illumination; + } } #endif diff --git a/files/shaders/lib/material/parallax.glsl b/files/shaders/lib/material/parallax.glsl index 7f4ce2d1dc..5525281f75 100644 --- a/files/shaders/lib/material/parallax.glsl +++ b/files/shaders/lib/material/parallax.glsl @@ -4,10 +4,9 @@ #define PARALLAX_SCALE 0.04 #define PARALLAX_BIAS -0.02 -vec2 getParallaxOffset(vec3 eyeDir, mat3 tbnTranspose, float height, float flipY) +vec2 getParallaxOffset(vec3 eyeDir, float height, float flipY) { - vec3 TSeyeDir = normalize(eyeDir * tbnTranspose); - return vec2(TSeyeDir.x, TSeyeDir.y * flipY) * ( height * PARALLAX_SCALE + PARALLAX_BIAS ); + return vec2(eyeDir.x, eyeDir.y * flipY) * ( height * PARALLAX_SCALE + PARALLAX_BIAS ); } -#endif \ No newline at end of file +#endif diff --git a/files/shaders/lib/util/distortion.glsl b/files/shaders/lib/util/distortion.glsl new file mode 100644 index 0000000000..e0ccf6f2ec --- /dev/null +++ b/files/shaders/lib/util/distortion.glsl @@ -0,0 +1,32 @@ +#ifndef LIB_UTIL_DISTORTION +#define LIB_UTIL_DISTORTION + +vec4 applyDistortion(in vec4 color, in float strength, in float pixelDepth, in float sceneDepth) +{ + vec4 distortion = color; + float invOcclusion = 1.0; + + // TODO: Investigate me. Alpha-clipping is enabled for refraction for what seems an arbitrary threshold, even when + // there are no associated NIF properties. + if (distortion.a < 0.1) + discard; + + distortion.b = 0.0; + +#if @reverseZ + if (pixelDepth < sceneDepth) +#else + if (pixelDepth > sceneDepth) +#endif + { + invOcclusion = 0.0; + distortion.b = 1.0; + } + distortion.rg = color.rg * 2.0 - 1.0; + + distortion.rg *= invOcclusion * strength; + + return distortion; +} + +#endif diff --git a/files/shaders/lib/util/quickstep.glsl b/files/shaders/lib/util/quickstep.glsl index 2baa0a7430..e505886337 100644 --- a/files/shaders/lib/util/quickstep.glsl +++ b/files/shaders/lib/util/quickstep.glsl @@ -4,8 +4,8 @@ float quickstep(float x) { x = clamp(x, 0.0, 1.0); - x = 1.0 - x*x; - x = 1.0 - x*x; + x = 1.0 - x * x; + x = 1.0 - x * x; return x; } diff --git a/scripts/data/integration_tests/test_lua_api/player.lua b/scripts/data/integration_tests/test_lua_api/player.lua index 41022828f9..544bf5adc0 100644 --- a/scripts/data/integration_tests/test_lua_api/player.lua +++ b/scripts/data/integration_tests/test_lua_api/player.lua @@ -40,8 +40,7 @@ testing.registerLocalTest('playerForwardRunning', coroutine.yield() end local direction, distance = (self.position - startPos):normalize() - local normalizedDistance = distance / types.Actor.runSpeed(self) - testing.expectEqualWithDelta(normalizedDistance, 1, 0.2, 'Normalized forward runned distance') + testing.expectGreaterThan(distance, 0, 'Run forward, distance') testing.expectEqualWithDelta(direction.x, 0, 0.1, 'Run forward, X coord') testing.expectEqualWithDelta(direction.y, 1, 0.1, 'Run forward, Y coord') end) @@ -59,8 +58,7 @@ testing.registerLocalTest('playerDiagonalWalking', coroutine.yield() end local direction, distance = (self.position - startPos):normalize() - local normalizedDistance = distance / types.Actor.walkSpeed(self) - testing.expectEqualWithDelta(normalizedDistance, 1, 0.2, 'Normalized diagonally walked distance') + testing.expectGreaterThan(distance, 0, 'Walk diagonally, distance') testing.expectEqualWithDelta(direction.x, -0.707, 0.1, 'Walk diagonally, X coord') testing.expectEqualWithDelta(direction.y, -0.707, 0.1, 'Walk diagonally, Y coord') end) diff --git a/scripts/integration_tests.py b/scripts/integration_tests.py index da2b9aa7fe..41c6c3a5a2 100755 --- a/scripts/integration_tests.py +++ b/scripts/integration_tests.py @@ -13,6 +13,7 @@ parser.add_argument("--omw", type=str, default="openmw", help="path to openmw bi parser.add_argument( "--workdir", type=str, default="integration_tests_output", help="directory for temporary files and logs" ) +parser.add_argument("--verbose", action='store_true', help="print all openmw output") args = parser.parse_args() example_suite_dir = Path(args.example_suite).resolve() @@ -78,7 +79,10 @@ def runTest(name): ) as process: quit_requested = False for line in process.stdout: - stdout_lines.append(line) + 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="") @@ -102,7 +106,7 @@ def runTest(name): exit_ok = False 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: + if not exit_ok and not args.verbose: sys.stdout.writelines(stdout_lines) if test_success and exit_ok: print(f"{name} succeeded") diff --git a/scripts/osg_stats.py b/scripts/osg_stats.py index d8dab2ad1a..3cdd0febae 100755 --- a/scripts/osg_stats.py +++ b/scripts/osg_stats.py @@ -27,6 +27,8 @@ import termtables help='Show a graph for given metric over time.') @click.option('--commulative_timeseries', type=str, multiple=True, help='Show a graph for commulative sum of a given metric over time.') +@click.option('--timeseries_delta', type=str, multiple=True, + help='Show a graph for delta between neighbouring frames of a given metric over time.') @click.option('--hist', type=str, multiple=True, help='Show a histogram for all values of given metric.') @click.option('--hist_ratio', nargs=2, type=str, multiple=True, @@ -47,6 +49,8 @@ import termtables help='Add a graph to timeseries for a sum per frame of all given timeseries metrics.') @click.option('--commulative_timeseries_sum', is_flag=True, help='Add a graph to timeseries for a sum per frame of all given commulative timeseries.') +@click.option('--timeseries_delta_sum', is_flag=True, + help='Add a graph to timeseries for a sum per frame of all given timeseries delta.') @click.option('--stats_sum', is_flag=True, help='Add a row to stats table for a sum per frame of all given stats metrics.') @click.option('--begin_frame', type=int, default=0, @@ -69,7 +73,8 @@ import termtables def main(print_keys, regexp_match, timeseries, hist, hist_ratio, stdev_hist, plot, stats, precision, timeseries_sum, stats_sum, begin_frame, end_frame, path, commulative_timeseries, commulative_timeseries_sum, frame_number_name, - hist_threshold, threshold_name, threshold_value, show_common_path_prefix, stats_sort_by): + hist_threshold, threshold_name, threshold_value, show_common_path_prefix, stats_sort_by, + timeseries_delta, timeseries_delta_sum): sources = {v: list(read_data(v)) for v in path} if path else {'stdin': list(read_data(None))} if not show_common_path_prefix and len(sources) > 1: longest_common_prefix = os.path.commonprefix(list(sources.keys())) @@ -92,6 +97,9 @@ def main(print_keys, regexp_match, timeseries, hist, hist_ratio, stdev_hist, plo if commulative_timeseries: draw_commulative_timeseries(sources=frames, keys=matching_keys(commulative_timeseries), add_sum=commulative_timeseries_sum, begin_frame=begin_frame, end_frame=end_frame) + if timeseries_delta: + draw_timeseries_delta(sources=frames, keys=matching_keys(timeseries_delta), add_sum=timeseries_delta_sum, + begin_frame=begin_frame, end_frame=end_frame) if hist: draw_hists(sources=frames, keys=matching_keys(hist)) if hist_ratio: @@ -135,7 +143,7 @@ def collect_per_frame(sources, keys, begin_frame, end_frame, frame_number_name): end_frame = min(end_frame, max(v[-1][frame_number_name] for v in sources.values()) + 1) for name in sources.keys(): for key in keys: - result[name][key] = [0] * (end_frame - begin_frame) + result[name][key] = [None] * (end_frame - begin_frame) for name, frames in sources.items(): for frame in frames: number = frame[frame_number_name] @@ -146,7 +154,14 @@ def collect_per_frame(sources, keys, begin_frame, end_frame, frame_number_name): result[name][key][index] = frame[key] for name in result.keys(): for key in keys: - result[name][key] = numpy.array(result[name][key]) + prev = 0.0 + values = result[name][key] + for i in range(len(values)): + if values[i] is not None: + prev = values[i] + else: + values[i] = prev + result[name][key] = numpy.array(values) return result, begin_frame, end_frame @@ -186,6 +201,20 @@ def draw_commulative_timeseries(sources, keys, add_sum, begin_frame, end_frame): fig.canvas.manager.set_window_title('commulative_timeseries') +def draw_timeseries_delta(sources, keys, add_sum, begin_frame, end_frame): + fig, ax = matplotlib.pyplot.subplots() + x = numpy.array(range(begin_frame + 1, end_frame)) + for name, frames in sources.items(): + for key in keys: + ax.plot(x, numpy.diff(frames[key]), label=f'{key}:{name}') + if add_sum: + ax.plot(x, numpy.diff(numpy.sum(list(frames[k] for k in keys), axis=0)), label=f'sum:{name}', + linestyle='--') + ax.grid(True) + ax.legend() + fig.canvas.manager.set_window_title('timeseries_delta') + + def draw_hists(sources, keys): fig, ax = matplotlib.pyplot.subplots() bins = numpy.linspace( @@ -328,7 +357,7 @@ def make_stats(source, key, values, precision): sum=fixed_float(sum(values), precision), mean=fixed_float(statistics.mean(values), precision), median=fixed_float(statistics.median(values), precision), - stdev=fixed_float(statistics.stdev(values), precision), + stdev=fixed_float(statistics.stdev(float(v) for v in values), precision), q95=fixed_float(numpy.quantile(values, 0.95), precision), )