diff --git a/.gitignore b/.gitignore index f25adf58e6..39033bd725 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ Doxygen .idea cmake-build-* files/windows/*.aps +.cache/clangd ## qt-creator CMakeLists.txt.user* .vs 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/CHANGELOG.md b/CHANGELOG.md index 39da12eac3..8f986e8dea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,17 @@ Bug #4204: Dead slaughterfish doesn't float to water surface after loading saved game Bug #4207: RestoreHealth/Fatigue spells have a huge priority even if a success chance is near 0 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 @@ -22,6 +29,7 @@ Bug #5977: Fatigueless NPCs' corpse underwater changes animation on game load Bug #6025: Subrecords cannot overlap records Bug #6027: Collisionshape becomes spiderweb-like when the mesh is too complex + Bug #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 #6427: Enemy health bar disappears before damaging effect ends @@ -30,6 +38,8 @@ 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 @@ -63,32 +73,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 #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 #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 + 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 + Feature #6188: Specular lighting from point light sources Feature #6447: Add LOD support to Object Paging Feature #6491: Add support for Qt6 Feature #6556: Lua API for sounds @@ -113,12 +154,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..e340d501ed 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -1,22 +1,18 @@ #!/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 diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index 338ca1ee9e..e11ceb499d 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -902,8 +902,6 @@ printf "Qt ${QT_VER}... " fi cd $QT_SDK - add_cmake_opts -DQT_QMAKE_EXECUTABLE="${QT_SDK}/bin/qmake.exe" \ - -DCMAKE_PREFIX_PATH="$QT_SDK" for CONFIGURATION in ${CONFIGURATIONS[@]}; do if [ $CONFIGURATION == "Debug" ]; then DLLSUFFIX="d" @@ -930,7 +928,7 @@ printf "SDL 2.24.0... " rm -rf SDL2-2.24.0 eval 7z x -y SDL2-devel-2.24.0-VC.zip $STRIP fi - export SDL2DIR="$(real_pwd)/SDL2-2.24.0" + SDL2DIR="$(real_pwd)/SDL2-2.24.0" for config in ${CONFIGURATIONS[@]}; do add_runtime_dlls $config "$(pwd)/SDL2-2.24.0/lib/x${ARCHSUFFIX}/SDL2.dll" done @@ -1025,6 +1023,8 @@ printf "zlib 1.2.11... " echo Done. } +add_cmake_opts -DCMAKE_PREFIX_PATH="\"${QT_SDK};${SDL2DIR}\"" + echo cd $DEPS_INSTALL/.. echo 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/install_debian_deps.sh b/CI/install_debian_deps.sh index bd767bb173..8d7c0a493f 100755 --- a/CI/install_debian_deps.sh +++ b/CI/install_debian_deps.sh @@ -86,7 +86,7 @@ declare -rA GROUPED_DEPS=( libswresample3 libswscale5 libtinyxml2.6.2v5 - libyaml-cpp0.7 + libyaml-cpp0.8 python3-pip xvfb " @@ -125,3 +125,4 @@ add-apt-repository -y ppa:openmw/openmw add-apt-repository -y ppa:openmw/openmw-daily add-apt-repository -y ppa:openmw/staging apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends "${deps[@]}" >/dev/null +apt list --installed 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 cad6290274..34df0216da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,7 +71,8 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 49) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 50) +set(OPENMW_LUA_API_REVISION 51) +set(OPENMW_POSTPROCESSING_API_REVISION 1) set(OPENMW_VERSION_COMMITHASH "") set(OPENMW_VERSION_TAGHASH "") @@ -113,7 +114,6 @@ include(WholeArchive) configure_file ("${OpenMW_SOURCE_DIR}/docs/mainpage.hpp.cmake" "${OpenMW_BINARY_DIR}/docs/mainpage.hpp") option(BOOST_STATIC "Link static build of Boost into the binaries" FALSE) -option(SDL2_STATIC "Link static build of SDL into the binaries" FALSE) option(QT_STATIC "Link static build of Qt into the binaries" FALSE) option(OPENMW_USE_SYSTEM_BULLET "Use system provided bullet physics library" ON) @@ -179,6 +179,8 @@ if (MSVC) # there should be no relevant downsides to having it on: # https://docs.microsoft.com/en-us/cpp/build/reference/bigobj-increase-number-of-sections-in-dot-obj-file add_compile_options(/bigobj) + + add_compile_options(/Zc:__cplusplus) endif() # Set up common paths @@ -480,7 +482,6 @@ set(SOL_CONFIG_DIR ${OpenMW_SOURCE_DIR}/extern/sol_config) include_directories( BEFORE SYSTEM "." - ${SDL2_INCLUDE_DIR} ${Boost_INCLUDE_DIR} ${MyGUI_INCLUDE_DIRS} ${OPENAL_INCLUDE_DIR} @@ -492,7 +493,7 @@ include_directories( ${ICU_INCLUDE_DIRS} ) -link_directories(${SDL2_LIBRARY_DIRS} ${Boost_LIBRARY_DIRS} ${COLLADA_DOM_LIBRARY_DIRS}) +link_directories(${Boost_LIBRARY_DIRS} ${COLLADA_DOM_LIBRARY_DIRS}) if(MYGUI_STATIC) add_definitions(-DMYGUI_STATIC) diff --git a/apps/benchmarks/detournavigator/navmeshtilescache.cpp b/apps/benchmarks/detournavigator/navmeshtilescache.cpp index 3be1c8762a..746739c856 100644 --- a/apps/benchmarks/detournavigator/navmeshtilescache.cpp +++ b/apps/benchmarks/detournavigator/navmeshtilescache.cpp @@ -5,6 +5,7 @@ #include #include +#include #include namespace @@ -24,29 +25,25 @@ namespace PreparedNavMeshData mValue; }; - template - osg::Vec2i generateVec2i(int max, Random& random) + osg::Vec2i generateVec2i(int max, auto& random) { std::uniform_int_distribution distribution(0, max); return osg::Vec2i(distribution(random), distribution(random)); } - template - osg::Vec3f generateAgentHalfExtents(float min, float max, Random& random) + osg::Vec3f generateAgentHalfExtents(float min, float max, auto& random) { std::uniform_int_distribution distribution(min, max); return osg::Vec3f(distribution(random), distribution(random), distribution(random)); } - template - void generateVertices(OutputIterator out, std::size_t number, Random& random) + void generateVertices(std::output_iterator auto out, std::size_t number, auto& random) { std::uniform_real_distribution distribution(0.0, 1.0); std::generate_n(out, 3 * (number - number % 3), [&] { return distribution(random); }); } - template - void generateIndices(OutputIterator out, int max, std::size_t number, Random& random) + void generateIndices(std::output_iterator auto out, int max, std::size_t number, auto& random) { std::uniform_int_distribution distribution(0, max); std::generate_n(out, number - number % 3, [&] { return distribution(random); }); @@ -70,21 +67,18 @@ namespace return AreaType_null; } - template - AreaType generateAreaType(Random& random) + AreaType generateAreaType(auto& random) { std::uniform_int_distribution distribution(0, 4); return toAreaType(distribution(random)); } - template - void generateAreaTypes(OutputIterator out, std::size_t triangles, Random& random) + void generateAreaTypes(std::output_iterator auto out, std::size_t triangles, auto& random) { std::generate_n(out, triangles, [&] { return generateAreaType(random); }); } - template - void generateWater(OutputIterator out, std::size_t count, Random& random) + void generateWater(std::output_iterator auto out, std::size_t count, auto& random) { std::uniform_real_distribution distribution(0.0, 1.0); std::generate_n(out, count, [&] { @@ -92,8 +86,7 @@ namespace }); } - template - Mesh generateMesh(std::size_t triangles, Random& random) + Mesh generateMesh(std::size_t triangles, auto& random) { std::uniform_real_distribution distribution(0.0, 1.0); std::vector vertices; @@ -109,8 +102,7 @@ namespace return Mesh(std::move(indices), std::move(vertices), std::move(areaTypes)); } - template - Heightfield generateHeightfield(Random& random) + Heightfield generateHeightfield(auto& random) { std::uniform_real_distribution distribution(0.0, 1.0); Heightfield result; @@ -127,8 +119,7 @@ namespace return result; } - template - FlatHeightfield generateFlatHeightfield(Random& random) + FlatHeightfield generateFlatHeightfield(auto& random) { std::uniform_real_distribution distribution(0.0, 1.0); FlatHeightfield result; @@ -138,8 +129,7 @@ namespace return result; } - template - Key generateKey(std::size_t triangles, Random& random) + Key generateKey(std::size_t triangles, auto& random) { const CollisionShapeType agentShapeType = CollisionShapeType::Aabb; const osg::Vec3f agentHalfExtents = generateAgentHalfExtents(0.5, 1.5, random); @@ -158,14 +148,12 @@ namespace constexpr std::size_t trianglesPerTile = 239; - template - void generateKeys(OutputIterator out, std::size_t count, Random& random) + void generateKeys(std::output_iterator auto out, std::size_t count, auto& random) { std::generate_n(out, count, [&] { return generateKey(trianglesPerTile, random); }); } - template - void fillCache(OutputIterator out, Random& random, NavMeshTilesCache& cache) + void fillCache(std::output_iterator auto out, auto& random, NavMeshTilesCache& cache) { std::size_t size = cache.getStats().mNavMeshCacheSize; 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 e293055919..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. @@ -1204,7 +1202,8 @@ namespace EsmTool std::array weathers = { "Clear", "Cloudy", "Fog", "Overcast", "Rain", "Thunder", "Ash", "Blight", "Snow", "Blizzard" }; for (size_t i = 0; i < weathers.size(); ++i) - std::cout << " " << weathers[i] << ": " << mData.mData.mProbabilities[i] << std::endl; + std::cout << " " << weathers[i] << ": " << static_cast(mData.mData.mProbabilities[i]) + << std::endl; std::cout << " Map Color: " << mData.mMapColor << std::endl; if (!mData.mSleepList.empty()) std::cout << " Sleep List: " << mData.mSleepList << std::endl; 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 2f03cfaf41..7025b0ae43 100644 --- a/apps/essimporter/convertinventory.cpp +++ b/apps/essimporter/convertinventory.cpp @@ -9,15 +9,14 @@ namespace ESSImport void convertInventory(const Inventory& inventory, ESM::InventoryState& state) { - int index = 0; + uint32_t index = 0; for (const auto& item : inventory.mItems) { ESM::ObjectState objstate; objstate.blank(); objstate.mRef = item; objstate.mRef.mRefID = ESM::RefId::stringRefId(item.mId); - objstate.mCount = std::abs(item.mCount); // restocking items have negative count in the savefile - // openmw handles them differently, so no need to set any flags + 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 87cee06e5d..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}) @@ -77,7 +76,7 @@ if (WIN32) endif (WIN32) target_link_libraries(openmw-launcher - ${SDL2_LIBRARY_ONLY} + SDL2::SDL2 ${OPENAL_LIBRARY} components_qt ) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index b6192d3c02..dc2c07d9bd 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -26,7 +26,7 @@ #include #include #include -#include +#include #include #include "utils/profilescombobox.hpp" @@ -123,7 +123,7 @@ namespace Launcher int getMaxNavMeshDbFileSizeMiB() { - return Settings::Manager::getUInt64("max navmeshdb file size", "Navigator") / (1024 * 1024); + return Settings::navigator().mMaxNavmeshdbFileSize / (1024 * 1024); } std::optional findFirstPath(const QStringList& directories, const QString& fileName) @@ -164,11 +164,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 +257,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 +312,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 @@ -359,9 +372,8 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) void Launcher::DataFilesPage::saveSettings(const QString& profile) { - if (const int value = ui.navMeshMaxSizeSpinBox->value(); value != getMaxNavMeshDbFileSizeMiB()) - Settings::Manager::setUInt64( - "max navmeshdb file size", "Navigator", static_cast(std::max(0, value)) * 1024 * 1024); + Settings::navigator().mMaxNavmeshdbFileSize.set( + static_cast(std::max(0, ui.navMeshMaxSizeSpinBox->value())) * 1024 * 1024); QString profileName = profile; @@ -385,7 +397,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); @@ -738,8 +750,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 9a10bf6d9c..fa9d5eb479 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -2,6 +2,7 @@ #include "sdlinit.hpp" +#include #include #include @@ -16,22 +17,6 @@ #include #include -#include - -QString getAspect(int x, int y) -{ - int gcd = std::gcd(x, y); - if (gcd == 0) - return QString(); - - int xaspect = x / gcd; - int yaspect = y / gcd; - // special case: 8 : 5 is usually referred to as 16:10 - if (xaspect == 8 && yaspect == 5) - return QString("16:10"); - - return QString(QString::number(xaspect) + ":" + QString::number(yaspect)); -} Launcher::GraphicsPage::GraphicsPage(QWidget* parent) : QWidget(parent) @@ -96,32 +81,29 @@ bool Launcher::GraphicsPage::loadSettings() // Visuals - int vsync = Settings::Manager::getInt("vsync mode", "Video"); - if (vsync < 0 || vsync > 2) - vsync = 0; + const int vsync = Settings::video().mVsyncMode; vSyncComboBox->setCurrentIndex(vsync); - size_t windowMode = static_cast(Settings::Manager::getInt("window mode", "Video")); - if (windowMode > static_cast(Settings::WindowMode::Windowed)) - windowMode = 0; - windowModeComboBox->setCurrentIndex(windowMode); - slotFullScreenChanged(windowMode); + const Settings::WindowMode windowMode = Settings::video().mWindowMode; - if (Settings::Manager::getBool("window border", "Video")) + windowModeComboBox->setCurrentIndex(static_cast(windowMode)); + handleWindowModeChange(windowMode); + + if (Settings::video().mWindowBorder) windowBorderCheckBox->setCheckState(Qt::Checked); // aaValue is the actual value (0, 1, 2, 4, 8, 16) - int aaValue = Settings::Manager::getInt("antialiasing", "Video"); + const int aaValue = Settings::video().mAntialiasing; // aaIndex is the index into the allowed values in the pull down. - int aaIndex = antiAliasingComboBox->findText(QString::number(aaValue)); + const int aaIndex = antiAliasingComboBox->findText(QString::number(aaValue)); if (aaIndex != -1) antiAliasingComboBox->setCurrentIndex(aaIndex); - int width = Settings::Manager::getInt("resolution x", "Video"); - int height = Settings::Manager::getInt("resolution y", "Video"); - QString resolution = QString::number(width) + QString(" x ") + QString::number(height); - screenComboBox->setCurrentIndex(Settings::Manager::getInt("screen", "Video")); + const int width = Settings::video().mResolutionX; + const int height = Settings::video().mResolutionY; + QString resolution = QString::number(width) + QString(" × ") + QString::number(height); + screenComboBox->setCurrentIndex(Settings::video().mScreen); int resIndex = resolutionComboBox->findText(resolution, Qt::MatchStartsWith); @@ -137,7 +119,7 @@ bool Launcher::GraphicsPage::loadSettings() customHeightSpinBox->setValue(height); } - float fpsLimit = Settings::Manager::getFloat("framerate limit", "Video"); + const float fpsLimit = Settings::video().mFramerateLimit; if (fpsLimit != 0) { framerateLimitCheckBox->setCheckState(Qt::Checked); @@ -161,32 +143,37 @@ bool Launcher::GraphicsPage::loadSettings() lightingMethodComboBox->setCurrentIndex(lightingMethod); // Shadows - if (Settings::Manager::getBool("actor shadows", "Shadows")) + if (Settings::shadows().mActorShadows) actorShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::Manager::getBool("player shadows", "Shadows")) + if (Settings::shadows().mPlayerShadows) playerShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::Manager::getBool("terrain shadows", "Shadows")) + if (Settings::shadows().mTerrainShadows) terrainShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::Manager::getBool("object shadows", "Shadows")) + if (Settings::shadows().mObjectShadows) objectShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::Manager::getBool("enable indoor shadows", "Shadows")) + if (Settings::shadows().mEnableIndoorShadows) indoorShadowsCheckBox->setCheckState(Qt::Checked); - shadowComputeSceneBoundsComboBox->setCurrentIndex(shadowComputeSceneBoundsComboBox->findText( - QString(tr(Settings::Manager::getString("compute scene bounds", "Shadows").c_str())))); + auto boundMethod = Settings::shadows().mComputeSceneBounds.get(); + if (boundMethod == "bounds") + shadowComputeSceneBoundsComboBox->setCurrentIndex(0); + else if (boundMethod == "primitives") + shadowComputeSceneBoundsComboBox->setCurrentIndex(1); + else + shadowComputeSceneBoundsComboBox->setCurrentIndex(2); - int shadowDistLimit = Settings::Manager::getInt("maximum shadow map distance", "Shadows"); + const int shadowDistLimit = Settings::shadows().mMaximumShadowMapDistance; if (shadowDistLimit > 0) { shadowDistanceCheckBox->setCheckState(Qt::Checked); shadowDistanceSpinBox->setValue(shadowDistLimit); } - float shadowFadeStart = Settings::Manager::getFloat("shadow fade start", "Shadows"); + const float shadowFadeStart = Settings::shadows().mShadowFadeStart; if (shadowFadeStart != 0) fadeStartSpinBox->setValue(shadowFadeStart); - int shadowRes = Settings::Manager::getInt("shadow map resolution", "Shadows"); + const int shadowRes = Settings::shadows().mShadowMapResolution; int shadowResIndex = shadowResolutionComboBox->findText(QString::number(shadowRes)); if (shadowResIndex != -1) shadowResolutionComboBox->setCurrentIndex(shadowResIndex); @@ -198,29 +185,16 @@ void Launcher::GraphicsPage::saveSettings() { // Visuals - // Ensure we only set the new settings if they changed. This is to avoid cluttering the - // user settings file (which by definition should only contain settings the user has touched) - int cVSync = vSyncComboBox->currentIndex(); - if (cVSync != Settings::Manager::getInt("vsync mode", "Video")) - Settings::Manager::setInt("vsync mode", "Video", cVSync); - - int cWindowMode = windowModeComboBox->currentIndex(); - if (cWindowMode != Settings::Manager::getInt("window mode", "Video")) - Settings::Manager::setInt("window mode", "Video", cWindowMode); - - bool cWindowBorder = windowBorderCheckBox->checkState(); - if (cWindowBorder != Settings::Manager::getBool("window border", "Video")) - Settings::Manager::setBool("window border", "Video", cWindowBorder); - - int cAAValue = antiAliasingComboBox->currentText().toInt(); - if (cAAValue != Settings::Manager::getInt("antialiasing", "Video")) - Settings::Manager::setInt("antialiasing", "Video", cAAValue); + Settings::video().mVsyncMode.set(static_cast(vSyncComboBox->currentIndex())); + Settings::video().mWindowMode.set(static_cast(windowModeComboBox->currentIndex())); + Settings::video().mWindowBorder.set(windowBorderCheckBox->checkState() == Qt::Checked); + Settings::video().mAntialiasing.set(antiAliasingComboBox->currentText().toInt()); int cWidth = 0; int cHeight = 0; if (standardRadioButton->isChecked()) { - QRegularExpression resolutionRe("^(\\d+) x (\\d+)"); + QRegularExpression resolutionRe("^(\\d+) × (\\d+)"); QRegularExpressionMatch match = resolutionRe.match(resolutionComboBox->currentText().simplified()); if (match.hasMatch()) { @@ -234,25 +208,17 @@ void Launcher::GraphicsPage::saveSettings() cHeight = customHeightSpinBox->value(); } - if (cWidth != Settings::Manager::getInt("resolution x", "Video")) - Settings::Manager::setInt("resolution x", "Video", cWidth); - - if (cHeight != Settings::Manager::getInt("resolution y", "Video")) - Settings::Manager::setInt("resolution y", "Video", cHeight); - - int cScreen = screenComboBox->currentIndex(); - if (cScreen != Settings::Manager::getInt("screen", "Video")) - Settings::Manager::setInt("screen", "Video", cScreen); + Settings::video().mResolutionX.set(cWidth); + Settings::video().mResolutionY.set(cHeight); + Settings::video().mScreen.set(screenComboBox->currentIndex()); if (framerateLimitCheckBox->checkState() != Qt::Unchecked) { - float cFpsLimit = framerateLimitSpinBox->value(); - if (cFpsLimit != Settings::Manager::getFloat("framerate limit", "Video")) - Settings::Manager::setFloat("framerate limit", "Video", cFpsLimit); + Settings::video().mFramerateLimit.set(framerateLimitSpinBox->value()); } - else if (Settings::Manager::getFloat("framerate limit", "Video") != 0) + else if (Settings::video().mFramerateLimit != 0) { - Settings::Manager::setFloat("framerate limit", "Video", 0); + Settings::video().mFramerateLimit.set(0); } // Lighting @@ -264,55 +230,43 @@ void Launcher::GraphicsPage::saveSettings() Settings::shaders().mLightingMethod.set(lightingMethodMap[lightingMethodComboBox->currentIndex()]); // Shadows - int cShadowDist = shadowDistanceCheckBox->checkState() != Qt::Unchecked ? shadowDistanceSpinBox->value() : 0; - if (Settings::Manager::getInt("maximum shadow map distance", "Shadows") != cShadowDist) - Settings::Manager::setInt("maximum shadow map distance", "Shadows", cShadowDist); - float cFadeStart = fadeStartSpinBox->value(); - if (cShadowDist > 0 && Settings::Manager::getFloat("shadow fade start", "Shadows") != cFadeStart) - Settings::Manager::setFloat("shadow fade start", "Shadows", cFadeStart); + const int cShadowDist = shadowDistanceCheckBox->checkState() != Qt::Unchecked ? shadowDistanceSpinBox->value() : 0; + Settings::shadows().mMaximumShadowMapDistance.set(cShadowDist); + const float cFadeStart = fadeStartSpinBox->value(); + if (cShadowDist > 0) + Settings::shadows().mShadowFadeStart.set(cFadeStart); - bool cActorShadows = actorShadowsCheckBox->checkState(); - bool cObjectShadows = objectShadowsCheckBox->checkState(); - bool cTerrainShadows = terrainShadowsCheckBox->checkState(); - bool cPlayerShadows = playerShadowsCheckBox->checkState(); + const bool cActorShadows = actorShadowsCheckBox->checkState() != Qt::Unchecked; + const bool cObjectShadows = objectShadowsCheckBox->checkState() != Qt::Unchecked; + const bool cTerrainShadows = terrainShadowsCheckBox->checkState() != Qt::Unchecked; + const bool cPlayerShadows = playerShadowsCheckBox->checkState() != Qt::Unchecked; if (cActorShadows || cObjectShadows || cTerrainShadows || cPlayerShadows) { - if (!Settings::Manager::getBool("enable shadows", "Shadows")) - Settings::Manager::setBool("enable shadows", "Shadows", true); - if (Settings::Manager::getBool("actor shadows", "Shadows") != cActorShadows) - Settings::Manager::setBool("actor shadows", "Shadows", cActorShadows); - if (Settings::Manager::getBool("player shadows", "Shadows") != cPlayerShadows) - Settings::Manager::setBool("player shadows", "Shadows", cPlayerShadows); - if (Settings::Manager::getBool("object shadows", "Shadows") != cObjectShadows) - Settings::Manager::setBool("object shadows", "Shadows", cObjectShadows); - if (Settings::Manager::getBool("terrain shadows", "Shadows") != cTerrainShadows) - Settings::Manager::setBool("terrain shadows", "Shadows", cTerrainShadows); + Settings::shadows().mEnableShadows.set(true); + Settings::shadows().mActorShadows.set(cActorShadows); + Settings::shadows().mPlayerShadows.set(cPlayerShadows); + Settings::shadows().mObjectShadows.set(cObjectShadows); + Settings::shadows().mTerrainShadows.set(cTerrainShadows); } else { - if (Settings::Manager::getBool("enable shadows", "Shadows")) - Settings::Manager::setBool("enable shadows", "Shadows", false); - if (Settings::Manager::getBool("actor shadows", "Shadows")) - Settings::Manager::setBool("actor shadows", "Shadows", false); - if (Settings::Manager::getBool("player shadows", "Shadows")) - Settings::Manager::setBool("player shadows", "Shadows", false); - if (Settings::Manager::getBool("object shadows", "Shadows")) - Settings::Manager::setBool("object shadows", "Shadows", false); - if (Settings::Manager::getBool("terrain shadows", "Shadows")) - Settings::Manager::setBool("terrain shadows", "Shadows", false); + Settings::shadows().mEnableShadows.set(false); + Settings::shadows().mActorShadows.set(false); + Settings::shadows().mPlayerShadows.set(false); + Settings::shadows().mObjectShadows.set(false); + Settings::shadows().mTerrainShadows.set(false); } - bool cIndoorShadows = indoorShadowsCheckBox->checkState(); - if (Settings::Manager::getBool("enable indoor shadows", "Shadows") != cIndoorShadows) - Settings::Manager::setBool("enable indoor shadows", "Shadows", cIndoorShadows); + Settings::shadows().mEnableIndoorShadows.set(indoorShadowsCheckBox->checkState() != Qt::Unchecked); + Settings::shadows().mShadowMapResolution.set(shadowResolutionComboBox->currentText().toInt()); - int cShadowRes = shadowResolutionComboBox->currentText().toInt(); - if (cShadowRes != Settings::Manager::getInt("shadow map resolution", "Shadows")) - Settings::Manager::setInt("shadow map resolution", "Shadows", cShadowRes); - - auto cComputeSceneBounds = shadowComputeSceneBoundsComboBox->currentText().toStdString(); - if (cComputeSceneBounds != Settings::Manager::getString("compute scene bounds", "Shadows")) - Settings::Manager::setString("compute scene bounds", "Shadows", cComputeSceneBounds); + 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) @@ -347,19 +301,8 @@ QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen) return result; } - QString resolution = QString::number(mode.w) + QString(" x ") + QString::number(mode.h); - - QString aspect = getAspect(mode.w, mode.h); - if (aspect == QLatin1String("16:9") || aspect == QLatin1String("16:10")) - { - resolution.append(tr("\t(Wide ") + aspect + ")"); - } - else if (aspect == QLatin1String("4:3")) - { - resolution.append(tr("\t(Standard 4:3)")); - } - - result.append(resolution); + auto str = Misc::getResolutionText(mode.w, mode.h, "%i × %i (%i:%i)"); + result.append(QString(str.c_str())); } result.removeDuplicates(); @@ -392,8 +335,12 @@ void Launcher::GraphicsPage::screenChanged(int screen) void Launcher::GraphicsPage::slotFullScreenChanged(int mode) { - if (mode == static_cast(Settings::WindowMode::Fullscreen) - || mode == static_cast(Settings::WindowMode::WindowedFullscreen)) + handleWindowModeChange(static_cast(mode)); +} + +void Launcher::GraphicsPage::handleWindowModeChange(Settings::WindowMode mode) +{ + if (mode == Settings::WindowMode::Fullscreen || mode == Settings::WindowMode::WindowedFullscreen) { standardRadioButton->toggle(); customRadioButton->setEnabled(false); diff --git a/apps/launcher/graphicspage.hpp b/apps/launcher/graphicspage.hpp index 92bdf35ac4..85f91d1ff1 100644 --- a/apps/launcher/graphicspage.hpp +++ b/apps/launcher/graphicspage.hpp @@ -3,7 +3,7 @@ #include "ui_graphicspage.h" -#include +#include namespace Files { @@ -40,6 +40,7 @@ namespace Launcher static QRect getMaximumResolution(); bool setupSDL(); + void handleWindowModeChange(Settings::WindowMode state); }; } #endif 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/maindialog.cpp b/apps/launcher/maindialog.cpp index 023d6b729a..5d558ef38f 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -1,13 +1,5 @@ #include "maindialog.hpp" -#include -#include -#include -#include -#include -#include -#include - #include #include #include @@ -15,10 +7,15 @@ #include #include +#include +#include #include #include #include +#include #include +#include +#include #include "datafilespage.hpp" #include "graphicspage.hpp" @@ -121,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; } } @@ -139,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?! @@ -300,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; @@ -330,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 {}; } @@ -391,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); @@ -422,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; } @@ -460,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; } } @@ -482,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; } @@ -513,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; } @@ -565,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/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 5869cc3a73..b8539671b5 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -191,6 +191,7 @@ bool Launcher::SettingsPage::loadSettings() } loadSettingBool(Settings::game().mTurnToMovementDirection, *turnToMovementDirectionCheckBox); loadSettingBool(Settings::game().mSmoothMovement, *smoothMovementCheckBox); + loadSettingBool(Settings::game().mPlayerMovementIgnoresAnimation, *playerMovementIgnoresAnimationCheckBox); distantLandCheckBox->setCheckState( Settings::terrain().mDistantTerrain && Settings::terrain().mObjectPaging ? Qt::Checked : Qt::Unchecked); @@ -338,6 +339,7 @@ void Launcher::SettingsPage::saveSettings() saveSettingBool(*shieldSheathingCheckBox, Settings::game().mShieldSheathing); saveSettingBool(*turnToMovementDirectionCheckBox, Settings::game().mTurnToMovementDirection); saveSettingBool(*smoothMovementCheckBox, Settings::game().mSmoothMovement); + saveSettingBool(*playerMovementIgnoresAnimationCheckBox, Settings::game().mPlayerMovementIgnoresAnimation); const bool wantDistantLand = distantLandCheckBox->checkState() == Qt::Checked; if (wantDistantLand != (Settings::terrain().mDistantTerrain && Settings::terrain().mObjectPaging)) 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 bb900ccb2d..c0e2b0be06 100644 --- a/files/ui/graphicspage.ui +++ b/apps/launcher/ui/graphicspage.ui @@ -22,7 +22,7 @@ - + @@ -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 97% rename from files/ui/settingspage.ui rename to apps/launcher/ui/settingspage.ui index c61b4f4229..0340509205 100644 --- a/files/ui/settingspage.ui +++ b/apps/launcher/ui/settingspage.ui @@ -14,7 +14,7 @@ - 0 + 1 @@ -116,10 +116,10 @@ - <html><head/><body><p>Enable navigator. When enabled background threads are started to build nav mesh for world geometry. Pathfinding system uses nav mesh to build paths. When disabled only pathgrid is used to build paths. Single-core CPU systems may have big performance impact on exiting interior location and moving across exterior world. May slightly affect performance on multi-core CPU systems. Multi-core CPU systems may have different latency for nav mesh update depending on other settings and system performance. Moving across external world, entering/exiting location produce nav mesh update. NPC and creatures may not be able to find path before nav mesh is built around them. Try to disable this if you want to have old fashioned AI which doesn’t know where to go when you stand behind that stone and casting a firebolt.</p></body></html> + <html><head/><body><p><a name="docs-internal-guid-f375b85a-7fff-02ff-a5af-c5cff63923c0"/>When enabled, a navigation mesh is built in the background for world geometry to be used for pathfinding. When disabled only the path grid is used to build paths. Single-core CPU systems may have a big performance impact on existing interior location and moving across the exterior world. May slightly affect performance on multi-core CPU systems. Multi-core CPU systems may have different latency for nav mesh update depending on other settings and system performance. Moving across external world, entering/exiting location produce nav mesh update. NPC and creatures may not be able to find path before nav mesh is built around them. Try to disable this if you want to have old fashioned AI which doesn't know where to go when you stand behind that stone and cast a firebolt.</p></body></html> - Build nav mesh for world geometry + Use navigation mesh for pathfinding @@ -293,8 +293,8 @@ 0 0 - 680 - 882 + 671 + 774 @@ -377,6 +377,16 @@ + + + + <html><head/><body><p>In third person, the camera will sway along with the movement animations of the player. Enabling this option disables this swaying by having the player character move independently of its animation. This was the default behavior of OpenMW 0.48 and earlier.</p></body></html> + + + Player movement ignores animation + + + 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..70efd06090 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -116,7 +116,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,8 +139,7 @@ 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}) 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/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..9e237dd7a8 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, const std::string& 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..f5af627491 100644 --- a/apps/opencs/model/prefs/coloursetting.hpp +++ b/apps/opencs/model/prefs/coloursetting.hpp @@ -20,22 +20,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, const std::string& 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..3089281c91 100644 --- a/apps/opencs/model/prefs/shortcutmanager.cpp +++ b/apps/opencs/model/prefs/shortcutmanager.cpp @@ -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..87d2f2256c 100644 --- a/apps/opencs/model/prefs/shortcutmanager.hpp +++ b/apps/opencs/model/prefs/shortcutmanager.hpp @@ -32,7 +32,7 @@ namespace CSMPrefs void setSequence(const std::string& 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::multimap> ShortcutMap; typedef std::map SequenceMap; - typedef std::map ModifierMap; + 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..33bb521290 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, const std::string& 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..84857a9bc7 100644 --- a/apps/opencs/model/prefs/shortcutsetting.hpp +++ b/apps/opencs/model/prefs/shortcutsetting.hpp @@ -17,14 +17,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, const std::string& 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..b05a4b7473 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,93 +69,64 @@ 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); + declareInt(mValues->mScripts.mErrorHeight, "Initial height of the error panel").setRange(100, 10000); + declareBool(mValues->mScripts.mHighlightOccurrences, "Highlight other occurrences of selected names"); 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")); @@ -171,51 +136,55 @@ void CSMPrefs::State::declare() declareColour("colour-id", "Highlight Colour: IDs", QColor("blue")); 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); + declareDouble(mValues->mRendering.mObjectMarkerAlpha, "Object Marker Transparency").setPrecision(2).setRange(0, 1); + declareBool(mValues->mRendering.mSceneUseGradient, "Use Gradient Background"); 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)) .setTooltip( @@ -231,82 +200,52 @@ void CSMPrefs::State::declare() .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"); @@ -401,7 +340,7 @@ void CSMPrefs::State::declare() "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); + declareModifier(mValues->mKeyBindings.mSceneSpeedModifier, "Speed Modifier"); 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)); @@ -415,6 +354,30 @@ void CSMPrefs::State::declare() 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("scene-duplicate", "Duplicate Instance", QKeySequence(Qt::ShiftModifier | Qt::Key_C)); + declareShortcut("scene-clear-selection", "Clear Selection", QKeySequence(Qt::Key_Space)); + declareShortcut("scene-unhide-all", "Unhide All Objects", QKeySequence(Qt::AltModifier | Qt::Key_H)); + declareShortcut("scene-toggle-visibility", "Toggle Selection Visibility", QKeySequence(Qt::Key_H)); + declareShortcut("scene-group-1", "Select Group 1", QKeySequence(Qt::Key_1)); + declareShortcut("scene-save-1", "Save Group 1", QKeySequence(Qt::ControlModifier | Qt::Key_1)); + declareShortcut("scene-group-2", "Select Group 2", QKeySequence(Qt::Key_2)); + declareShortcut("scene-save-2", "Save Group 2", QKeySequence(Qt::ControlModifier | Qt::Key_2)); + declareShortcut("scene-group-3", "Select Group 3", QKeySequence(Qt::Key_3)); + declareShortcut("scene-save-3", "Save Group 3", QKeySequence(Qt::ControlModifier | Qt::Key_3)); + declareShortcut("scene-group-4", "Select Group 4", QKeySequence(Qt::Key_4)); + declareShortcut("scene-save-4", "Save Group 4", QKeySequence(Qt::ControlModifier | Qt::Key_4)); + declareShortcut("scene-group-5", "Selection Group 5", QKeySequence(Qt::Key_5)); + declareShortcut("scene-save-5", "Save Group 5", QKeySequence(Qt::ControlModifier | Qt::Key_5)); + declareShortcut("scene-group-6", "Selection Group 6", QKeySequence(Qt::Key_6)); + declareShortcut("scene-save-6", "Save Group 6", QKeySequence(Qt::ControlModifier | Qt::Key_6)); + declareShortcut("scene-group-7", "Selection Group 7", QKeySequence(Qt::Key_7)); + declareShortcut("scene-save-7", "Save Group 7", QKeySequence(Qt::ControlModifier | Qt::Key_7)); + declareShortcut("scene-group-8", "Selection Group 8", QKeySequence(Qt::Key_8)); + declareShortcut("scene-save-8", "Save Group 8", QKeySequence(Qt::ControlModifier | Qt::Key_8)); + declareShortcut("scene-group-9", "Selection Group 9", QKeySequence(Qt::Key_9)); + declareShortcut("scene-save-9", "Save Group 9", QKeySequence(Qt::ControlModifier | Qt::Key_9)); + declareShortcut("scene-group-0", "Selection Group 10", QKeySequence(Qt::Key_0)); + declareShortcut("scene-save-0", "Save Group 10", QKeySequence(Qt::ControlModifier | Qt::Key_0)); declareSubcategory("1st/Free Camera"); declareShortcut("free-forward", "Forward", QKeySequence(Qt::Key_W)); @@ -440,13 +403,12 @@ void CSMPrefs::State::declare() declareShortcut("script-editor-uncomment", "Uncomment Selection", QKeySequence()); 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,90 +425,65 @@ 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); return *setting; } -CSMPrefs::ColourSetting& CSMPrefs::State::declareColour( - const std::string& key, const std::string& label, QColor default_) +CSMPrefs::ColourSetting& CSMPrefs::State::declareColour(const std::string& key, const QString& label, QColor default_) { 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, key, label, *mIndex); mCurrentCategory->second.addSetting(setting); @@ -554,39 +491,32 @@ CSMPrefs::ColourSetting& CSMPrefs::State::declareColour( } CSMPrefs::ShortcutSetting& CSMPrefs::State::declareShortcut( - const std::string& key, const std::string& label, const QKeySequence& default_) + const std::string& key, const QString& label, const QKeySequence& default_) { 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().convertFromString(mIndex->get(mCurrentCategory->second.getKey(), key), sequence); getShortcutManager().setSequence(key, sequence); - CSMPrefs::ShortcutSetting* setting = new CSMPrefs::ShortcutSetting(&mCurrentCategory->second, &mMutex, key, label); + CSMPrefs::ShortcutSetting* setting + = new CSMPrefs::ShortcutSetting(&mCurrentCategory->second, &mMutex, key, 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 +524,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 +556,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 +617,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..f3e580ea5a 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(const std::string& key, const QString& label, QColor default_); - StringSetting& declareString(const std::string& key, const std::string& label, std::string default_); + ShortcutSetting& declareShortcut(const std::string& key, const QString& label, const QKeySequence& default_); - 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/mergestages.cpp b/apps/opencs/model/tools/mergestages.cpp index 5a7fa6c1b9..c2ab74bcca 100644 --- a/apps/opencs/model/tools/mergestages.cpp +++ b/apps/opencs/model/tools/mergestages.cpp @@ -189,9 +189,9 @@ void CSMTools::FixLandsAndLandTexturesMergeStage::perform(int stage, CSMDoc::Mes CSMWorld::IdTable& ltexTable = dynamic_cast( *mState.mTarget->getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); - const std::string& id = mState.mTarget->getData().getLand().getId(stage).getRefIdString(); + const auto& id = mState.mTarget->getData().getLand().getId(stage); - CSMWorld::TouchLandCommand cmd(landTable, ltexTable, id); + CSMWorld::TouchLandCommand cmd(landTable, ltexTable, id.getRefIdString()); cmd.redo(); // Get rid of base data 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 b987b1d124..645e1ba8f3 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 @@ -64,6 +64,7 @@ add_openmw_dir (mwlua context menuscripts globalscripts localscripts playerscripts luabindings objectbindings cellbindings mwscriptbindings camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings postprocessingbindings stats debugbindings corebindings worldbindings worker magicbindings factionbindings + classbindings itemdata types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus types/potion types/ingredient types/misc types/repair types/armor types/light types/static @@ -153,7 +154,7 @@ target_link_libraries(openmw ${OPENAL_LIBRARY} ${FFmpeg_LIBRARIES} ${MyGUI_LIBRARIES} - ${SDL2_LIBRARY} + SDL2::SDL2 ${RecastNavigation_LIBRARIES} "osg-ffmpeg-videoplayer" "oics" diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 7393562cfb..92483bd8c3 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -313,12 +313,19 @@ bool OMW::Engine::frame(float frametime) mLuaManager->reportStats(frameNumber, *stats); } + mStereoManager->updateSettings(Settings::camera().mNearClip, Settings::camera().mViewingDistance); + mViewer->eventTraversal(); mViewer->updateTraversal(); + // update GUI by world data { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - mWorld->updateWindowManager(); + + if (mStateManager->getState() != MWBase::StateManager::State_NoGame) + { + mWorld->updateWindowManager(); + } } mLuaWorker->allowUpdate(); // if there is a separate Lua thread, it starts the update now @@ -452,14 +459,13 @@ void OMW::Engine::setSkipMenu(bool skipMenu, bool newGame) void OMW::Engine::createWindow() { - int screen = Settings::Manager::getInt("screen", "Video"); - int width = Settings::Manager::getInt("resolution x", "Video"); - int height = Settings::Manager::getInt("resolution y", "Video"); - Settings::WindowMode windowMode - = static_cast(Settings::Manager::getInt("window mode", "Video")); - bool windowBorder = Settings::Manager::getBool("window border", "Video"); - int vsync = Settings::Manager::getInt("vsync mode", "Video"); - unsigned int antialiasing = std::max(0, Settings::Manager::getInt("antialiasing", "Video")); + const int screen = Settings::video().mScreen; + const int width = Settings::video().mResolutionX; + const int height = Settings::video().mResolutionY; + const Settings::WindowMode windowMode = Settings::video().mWindowMode; + const bool windowBorder = Settings::video().mWindowBorder; + const SDLUtil::VSyncMode vsync = Settings::video().mVsyncMode; + unsigned antialiasing = static_cast(Settings::video().mAntialiasing); int pos_x = SDL_WINDOWPOS_CENTERED_DISPLAY(screen), pos_y = SDL_WINDOWPOS_CENTERED_DISPLAY(screen); @@ -482,8 +488,7 @@ void OMW::Engine::createWindow() if (!windowBorder) flags |= SDL_WINDOW_BORDERLESS; - SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, - Settings::Manager::getBool("minimize on focus loss", "Video") ? "1" : "0"); + SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, Settings::video().mMinimizeOnFocusLoss ? "1" : "0"); checkSDLError(SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8)); @@ -513,7 +518,7 @@ void OMW::Engine::createWindow() Log(Debug::Warning) << "Warning: " << antialiasing << "x antialiasing not supported, trying " << antialiasing / 2; antialiasing /= 2; - Settings::Manager::setInt("antialiasing", "Video", antialiasing); + Settings::video().mAntialiasing.set(antialiasing); checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); continue; } @@ -560,7 +565,7 @@ void OMW::Engine::createWindow() SDL_DestroyWindow(mWindow); mWindow = nullptr; antialiasing /= 2; - Settings::Manager::setInt("antialiasing", "Video", antialiasing); + Settings::video().mAntialiasing.set(antialiasing); checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); continue; } @@ -593,7 +598,63 @@ void OMW::Engine::createWindow() realizeOperations->add(mSelectColorFormatOperation); if (Stereo::getStereo()) - realizeOperations->add(new Stereo::InitializeStereoOperation()); + { + Stereo::Settings settings; + + settings.mMultiview = Settings::stereo().mMultiview; + settings.mAllowDisplayListsForMultiview = Settings::stereo().mAllowDisplayListsForMultiview; + settings.mSharedShadowMaps = Settings::stereo().mSharedShadowMaps; + + if (Settings::stereo().mUseCustomView) + { + const osg::Vec3 leftEyeOffset(Settings::stereoView().mLeftEyeOffsetX, + Settings::stereoView().mLeftEyeOffsetY, Settings::stereoView().mLeftEyeOffsetZ); + + const osg::Quat leftEyeOrientation(Settings::stereoView().mLeftEyeOrientationX, + Settings::stereoView().mLeftEyeOrientationY, Settings::stereoView().mLeftEyeOrientationZ, + Settings::stereoView().mLeftEyeOrientationW); + + const osg::Vec3 rightEyeOffset(Settings::stereoView().mRightEyeOffsetX, + Settings::stereoView().mRightEyeOffsetY, Settings::stereoView().mRightEyeOffsetZ); + + const osg::Quat rightEyeOrientation(Settings::stereoView().mRightEyeOrientationX, + Settings::stereoView().mRightEyeOrientationY, Settings::stereoView().mRightEyeOrientationZ, + Settings::stereoView().mRightEyeOrientationW); + + settings.mCustomView = Stereo::CustomView{ + .mLeft = Stereo::View{ + .pose = Stereo::Pose{ + .position = leftEyeOffset, + .orientation = leftEyeOrientation, + }, + .fov = Stereo::FieldOfView{ + .angleLeft = Settings::stereoView().mLeftEyeFovLeft, + .angleRight = Settings::stereoView().mLeftEyeFovRight, + .angleUp = Settings::stereoView().mLeftEyeFovUp, + .angleDown = Settings::stereoView().mLeftEyeFovDown, + }, + }, + .mRight = Stereo::View{ + .pose = Stereo::Pose{ + .position = rightEyeOffset, + .orientation = rightEyeOrientation, + }, + .fov = Stereo::FieldOfView{ + .angleLeft = Settings::stereoView().mRightEyeFovLeft, + .angleRight = Settings::stereoView().mRightEyeFovRight, + .angleUp = Settings::stereoView().mRightEyeFovUp, + .angleDown = Settings::stereoView().mRightEyeFovDown, + }, + }, + }; + } + + if (Settings::stereo().mUseCustomEyeResolution) + settings.mEyeResolution + = osg::Vec2i(Settings::stereoView().mEyeResolutionX, Settings::stereoView().mEyeResolutionY); + + realizeOperations->add(new Stereo::InitializeStereoOperation(settings)); + } mViewer->realize(); mGlMaxTextureImageUnits = identifyOp->getMaxTextureImageUnits(); @@ -632,9 +693,9 @@ void OMW::Engine::prepareEngine() mStateManager = std::make_unique(mCfgMgr.getUserDataPath() / "saves", mContentFiles); mEnvironment.setStateManager(*mStateManager); - bool stereoEnabled - = Settings::Manager::getBool("stereo enabled", "Stereo") || osg::DisplaySettings::instance().get()->getStereo(); - mStereoManager = std::make_unique(mViewer, stereoEnabled); + const bool stereoEnabled = Settings::stereo().mStereoEnabled || osg::DisplaySettings::instance().get()->getStereo(); + mStereoManager = std::make_unique( + mViewer, stereoEnabled, Settings::camera().mNearClip, Settings::camera().mViewingDistance); osg::ref_ptr rootNode(new osg::Group); mViewer->setSceneData(rootNode); @@ -866,7 +927,7 @@ void OMW::Engine::go() // Do not try to outsmart the OS thread scheduler (see bug #4785). mViewer->setUseConfigureAffinity(false); - mEnvironment.setFrameRateLimit(Settings::Manager::getFloat("framerate limit", "Video")); + mEnvironment.setFrameRateLimit(Settings::video().mFramerateLimit); prepareEngine(); 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 92ad28647b..a7859ad9e6 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; @@ -255,8 +257,8 @@ namespace MWBase = 0; virtual void staticMessageBox(std::string_view message) = 0; virtual void removeStaticMessageBox() = 0; - virtual void interactiveMessageBox( - std::string_view message, const std::vector& buttons = {}, bool block = false) + virtual void interactiveMessageBox(std::string_view message, const std::vector& buttons = {}, + bool block = false, int defaultFocus = -1) = 0; /// returns the index of the pressed button or -1 if no button was pressed 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 9e99b4cacb..01437b2abd 100644 --- a/apps/openmw/mwclass/activator.cpp +++ b/apps/openmw/mwclass/activator.cpp @@ -1,6 +1,7 @@ #include "activator.hpp" #include +#include #include #include @@ -27,7 +28,6 @@ #include "../mwrender/vismask.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwmechanics/npcstats.hpp" @@ -102,8 +102,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); std::string text; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) @@ -111,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; } @@ -146,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/actor.cpp b/apps/openmw/mwclass/actor.cpp index 7f60ae5c2c..9c197a70d2 100644 --- a/apps/openmw/mwclass/actor.cpp +++ b/apps/openmw/mwclass/actor.cpp @@ -95,12 +95,16 @@ namespace MWClass bool Actor::consume(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) const { - MWBase::Environment::get().getWorld()->breakInvisibility(actor); MWMechanics::CastSpell cast(actor, actor); const ESM::RefId& recordId = consumable.getCellRef().getRefId(); MWBase::Environment::get().getWorldModel()->registerPtr(consumable); MWBase::Environment::get().getLuaManager()->itemConsumed(consumable, actor); actor.getClass().getContainerStore(actor).remove(consumable, 1); - return cast.cast(recordId); + if (cast.cast(recordId)) + { + MWBase::Environment::get().getWorld()->breakInvisibility(actor); + return true; + } + return false; } } diff --git a/apps/openmw/mwclass/apparatus.cpp b/apps/openmw/mwclass/apparatus.cpp index 2fbe2f9f87..1ff7ef5bd6 100644 --- a/apps/openmw/mwclass/apparatus.cpp +++ b/apps/openmw/mwclass/apparatus.cpp @@ -1,6 +1,7 @@ #include "apparatus.hpp" #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -15,7 +16,6 @@ #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "classmodel.hpp" @@ -92,8 +92,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -106,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 54561e3b0f..28bb1ff35c 100644 --- a/apps/openmw/mwclass/armor.cpp +++ b/apps/openmw/mwclass/armor.cpp @@ -1,6 +1,7 @@ #include "armor.hpp" #include +#include #include #include @@ -24,7 +25,6 @@ #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "classmodel.hpp" @@ -217,8 +217,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -266,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 b2b65e01b2..d731f56394 100644 --- a/apps/openmw/mwclass/book.cpp +++ b/apps/openmw/mwclass/book.cpp @@ -1,6 +1,7 @@ #include "book.hpp" #include +#include #include #include @@ -19,7 +20,6 @@ #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwmechanics/npcstats.hpp" @@ -111,8 +111,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -128,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/classes.cpp b/apps/openmw/mwclass/classes.cpp index 5017b1b272..f96c73e529 100644 --- a/apps/openmw/mwclass/classes.cpp +++ b/apps/openmw/mwclass/classes.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -84,6 +85,7 @@ namespace MWClass ESM4Named::registerSelf(); ESM4Named::registerSelf(); ESM4Named::registerSelf(); + ESM4Named::registerSelf(); ESM4Light::registerSelf(); ESM4Named::registerSelf(); ESM4Named::registerSelf(); 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 cbc5cecb70..32a0b62729 100644 --- a/apps/openmw/mwclass/clothing.cpp +++ b/apps/openmw/mwclass/clothing.cpp @@ -1,6 +1,7 @@ #include "clothing.hpp" #include +#include #include #include @@ -16,7 +17,6 @@ #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" @@ -154,8 +154,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -173,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 db9e2fc08d..28779f971f 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -1,6 +1,7 @@ #include "container.hpp" #include +#include #include #include @@ -24,7 +25,6 @@ #include "../mwworld/worldmodel.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/animation.hpp" #include "../mwrender/objects.hpp" @@ -249,7 +249,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); std::string text; int lockLevel = ptr.getCellRef().getLockLevel(); @@ -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 eebcb99512..bb9c1bc277 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -1,6 +1,7 @@ #include "creature.hpp" #include +#include #include #include @@ -45,7 +46,6 @@ #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "classmodel.hpp" @@ -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()) @@ -582,12 +594,12 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); 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 ecd6cb59aa..99acfcf4df 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -1,6 +1,7 @@ #include "door.hpp" #include +#include #include #include @@ -25,7 +26,6 @@ #include "../mwworld/worldmodel.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/animation.hpp" #include "../mwrender/objects.hpp" @@ -267,7 +267,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); std::string text; @@ -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.cpp b/apps/openmw/mwclass/esm4base.cpp index 956fc210ee..77a5ad94a6 100644 --- a/apps/openmw/mwclass/esm4base.cpp +++ b/apps/openmw/mwclass/esm4base.cpp @@ -1,11 +1,11 @@ #include "esm4base.hpp" #include +#include #include #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" @@ -35,8 +35,7 @@ namespace MWClass MWGui::ToolTipInfo ESM4Impl::getToolTipInfo(std::string_view name, int count) { MWGui::ToolTipInfo info; - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); 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 78cbd89b50..2da7342de4 100644 --- a/apps/openmw/mwclass/esm4npc.cpp +++ b/apps/openmw/mwclass/esm4npc.cpp @@ -34,11 +34,26 @@ namespace MWClass static const ESM4::Npc* chooseTemplate(const std::vector& recs, uint16_t flag) { - // In case of FO3 the function may return nullptr that will lead to "ESM4 NPC traits not found" - // exception and the NPC will not be added to the scene. But in any way it shouldn't cause a crash. for (const auto* rec : recs) - if (rec->mIsTES4 || rec->mIsFONV || !(rec->mBaseConfig.tes5.templateFlags & flag)) + { + if (rec->mIsTES4) return rec; + else if (rec->mIsFONV) + { + // TODO: FO3 should use this branch as well. But it is not clear how to distinguish FO3 from + // TES5. Currently FO3 uses wrong template flags that can lead to "ESM4 NPC traits not found" + // exception the NPC will not be added to the scene. But in any way it shouldn't cause a crash. + if (!(rec->mBaseConfig.fo3.templateFlags & flag)) + return rec; + } + else if (rec->mIsFO4) + { + if (!(rec->mBaseConfig.fo4.templateFlags & flag)) + return rec; + } + else if (!(rec->mBaseConfig.tes5.templateFlags & flag)) + return rec; + } return nullptr; } @@ -73,32 +88,43 @@ 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::TES5_UseTraits); - data->mBaseData = chooseTemplate(npcRecs, ESM4::Npc::TES5_UseBaseData); + data->mTraits = chooseTemplate(npcRecs, ESM4::Npc::Template_UseTraits); - 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->mTraits == nullptr) + Log(Debug::Warning) << "Traits are 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 - data->mIsFemale = data->mTraits->mBaseConfig.tes5.flags & ESM4::Npc::TES5_Female; + data->mBaseData = chooseTemplate(npcRecs, ESM4::Npc::Template_UseBaseData); - if (auto inv = chooseTemplate(npcRecs, ESM4::Npc::TES5_UseInventory)) + if (data->mBaseData == nullptr) + Log(Debug::Warning) << "Base data is not found for ESM4 NPC base record: \"" << base->mEditorId << "\" (" + << ESM::RefId(base->mId) << ")"; + + 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)) { for (const ESM4::InventoryItem& item : inv->mInventory) { 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)); @@ -152,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 e87f74218b..5225170be7 100644 --- a/apps/openmw/mwclass/ingredient.cpp +++ b/apps/openmw/mwclass/ingredient.cpp @@ -1,6 +1,7 @@ #include "ingredient.hpp" #include +#include #include #include @@ -16,7 +17,6 @@ #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" @@ -107,8 +107,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -146,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 931ed73dfe..6e34e3c2bd 100644 --- a/apps/openmw/mwclass/light.cpp +++ b/apps/openmw/mwclass/light.cpp @@ -1,6 +1,7 @@ #include "light.hpp" #include +#include #include #include @@ -21,7 +22,6 @@ #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" @@ -159,8 +159,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -178,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 1fc65c8f79..d3c3d479e4 100644 --- a/apps/openmw/mwclass/lockpick.cpp +++ b/apps/openmw/mwclass/lockpick.cpp @@ -1,6 +1,7 @@ #include "lockpick.hpp" #include +#include #include #include @@ -15,7 +16,6 @@ #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" @@ -104,8 +104,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -123,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 6c517e3dde..0f26dfd2df 100644 --- a/apps/openmw/mwclass/misc.cpp +++ b/apps/openmw/mwclass/misc.cpp @@ -1,6 +1,7 @@ #include "misc.hpp" #include +#include #include #include @@ -19,7 +20,6 @@ #include "../mwworld/worldmodel.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" @@ -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()) { @@ -151,8 +151,8 @@ namespace MWClass countString = " (" + std::to_string(count) + ")"; std::string_view name = getName(ptr); - info.caption = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) - + MWGui::ToolTips::getCountString(count) + MWGui::ToolTips::getSoulString(ptr.getCellRef()); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count) + + MWGui::ToolTips::getSoulString(ptr.getCellRef()); info.icon = ref->mBase->mIcon; std::string text; @@ -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 dab6dc99ae..330a48daeb 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -1,6 +1,7 @@ #include "npc.hpp" #include +#include #include #include @@ -57,7 +58,6 @@ #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" namespace { @@ -92,13 +92,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 +313,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 +431,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 +485,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 +499,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 +597,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 +671,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 @@ -1089,7 +1081,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); if (fullHelp && !ref->mBase->mName.empty() && ptr.getRefData().getCustomData() && ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats.isWerewolf()) { @@ -1199,24 +1191,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; } } @@ -1333,25 +1325,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 +1358,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 +1395,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 +1407,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 9bab0345cb..7cf0c54f5c 100644 --- a/apps/openmw/mwclass/potion.cpp +++ b/apps/openmw/mwclass/potion.cpp @@ -1,6 +1,7 @@ #include "potion.hpp" #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -13,7 +14,6 @@ #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" @@ -95,8 +95,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -119,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 e020c89443..96c94339bb 100644 --- a/apps/openmw/mwclass/probe.cpp +++ b/apps/openmw/mwclass/probe.cpp @@ -1,6 +1,7 @@ #include "probe.hpp" #include +#include #include #include @@ -15,7 +16,6 @@ #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" @@ -103,8 +103,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -122,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 68fc2f60da..cf4a42be70 100644 --- a/apps/openmw/mwclass/repair.cpp +++ b/apps/openmw/mwclass/repair.cpp @@ -1,6 +1,7 @@ #include "repair.hpp" #include +#include #include #include @@ -13,7 +14,6 @@ #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" @@ -105,8 +105,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -124,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 68a66b69d9..2c9a9b5c7a 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -1,6 +1,7 @@ #include "weapon.hpp" #include +#include #include #include @@ -20,7 +21,6 @@ #include "../mwmechanics/weapontype.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" @@ -150,8 +150,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); @@ -244,7 +243,7 @@ namespace MWClass text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } @@ -277,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 1eb41a2d86..4215782e2f 100644 --- a/apps/openmw/mwgui/alchemywindow.cpp +++ b/apps/openmw/mwgui/alchemywindow.cpp @@ -6,7 +6,9 @@ #include #include #include +#include +#include #include #include @@ -27,7 +29,6 @@ #include "itemview.hpp" #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" -#include "ustring.hpp" #include "widgets.hpp" namespace MWGui @@ -77,6 +78,11 @@ namespace MWGui mIngredients[2]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onIngredientSelected); mIngredients[3]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onIngredientSelected); + mApparatus[0]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onApparatusSelected); + mApparatus[1]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onApparatusSelected); + mApparatus[2]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onApparatusSelected); + mApparatus[3]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onApparatusSelected); + mCreateButton->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onCreateButtonClicked); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onCancelButtonClicked); @@ -141,12 +147,12 @@ namespace MWGui } // remove ingredient slots that have been fully used up - for (int i = 0; i < 4; ++i) + for (size_t i = 0; i < mIngredients.size(); ++i) if (mIngredients[i]->isUserString("ToolTipType")) { MWWorld::Ptr ingred = *mIngredients[i]->getUserData(); - if (ingred.getRefData().getCount() == 0) - removeIngredient(mIngredients[i]); + if (ingred.getCellRef().getCount() == 0) + mAlchemy->removeIngredient(i); } updateFilters(); @@ -158,7 +164,7 @@ namespace MWGui auto const& wm = MWBase::Environment::get().getWindowManager(); std::string_view ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); - if (mFilterType->getCaption() == toUString(ingredient)) + if (mFilterType->getCaption() == ingredient) mCurrentFilter = FilterType::ByName; else mCurrentFilter = FilterType::ByEffect; @@ -170,17 +176,17 @@ namespace MWGui void AlchemyWindow::switchFilterType(MyGUI::Widget* _sender) { auto const& wm = MWBase::Environment::get().getWindowManager(); - MyGUI::UString ingredient = toUString(wm->getGameSettingString("sIngredients", "Ingredients")); + std::string_view ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); auto* button = _sender->castType(); if (button->getCaption() == ingredient) { - button->setCaption(toUString(wm->getGameSettingString("sMagicEffects", "Magic Effects"))); + button->setCaption(MyGUI::UString(wm->getGameSettingString("sMagicEffects", "Magic Effects"))); mCurrentFilter = FilterType::ByEffect; } else { - button->setCaption(ingredient); + button->setCaption(MyGUI::UString(ingredient)); mCurrentFilter = FilterType::ByName; } mSortModel->setNameFilter({}); @@ -289,7 +295,85 @@ namespace MWGui void AlchemyWindow::onIngredientSelected(MyGUI::Widget* _sender) { - removeIngredient(_sender); + size_t i = std::distance(mIngredients.begin(), std::find(mIngredients.begin(), mIngredients.end(), _sender)); + mAlchemy->removeIngredient(i); + update(); + } + + void AlchemyWindow::onItemSelected(MWWorld::Ptr item) + { + mItemSelectionDialog->setVisible(false); + + int32_t index = item.get()->mBase->mData.mType; + const auto& widget = mApparatus[index]; + + widget->setItem(item); + + if (item.isEmpty()) + { + widget->clearUserStrings(); + return; + } + + mAlchemy->addApparatus(item); + + widget->setUserString("ToolTipType", "ItemPtr"); + widget->setUserData(MWWorld::Ptr(item)); + + MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); + update(); + } + + void AlchemyWindow::onItemCancel() + { + mItemSelectionDialog->setVisible(false); + } + + void AlchemyWindow::onApparatusSelected(MyGUI::Widget* _sender) + { + size_t i = std::distance(mApparatus.begin(), std::find(mApparatus.begin(), mApparatus.end(), _sender)); + if (_sender->getUserData()->isEmpty()) // if this apparatus slot is empty + { + std::string title; + switch (i) + { + case ESM::Apparatus::AppaType::MortarPestle: + title = "#{sMortar}"; + break; + case ESM::Apparatus::AppaType::Alembic: + title = "#{sAlembic}"; + break; + case ESM::Apparatus::AppaType::Calcinator: + title = "#{sCalcinator}"; + break; + case ESM::Apparatus::AppaType::Retort: + title = "#{sRetort}"; + break; + default: + title = "#{sApparatus}"; + } + + mItemSelectionDialog = std::make_unique(title); + mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &AlchemyWindow::onItemSelected); + mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &AlchemyWindow::onItemCancel); + mItemSelectionDialog->setVisible(true); + mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); + mItemSelectionDialog->getSortModel()->setApparatusTypeFilter(i); + mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyAlchemyTools); + } + else + { + const auto& widget = mApparatus[i]; + mAlchemy->removeApparatus(i); + + if (widget->getChildCount()) + MyGUI::Gui::getInstance().destroyWidget(widget->getChildAt(0)); + + widget->clearUserStrings(); + widget->setItem(MWWorld::Ptr()); + widget->setUserData(MWWorld::Ptr()); + } + update(); } @@ -329,7 +413,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)); @@ -344,12 +428,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) @@ -386,15 +470,6 @@ namespace MWGui effectsWidget->setCoord(coord); } - void AlchemyWindow::removeIngredient(MyGUI::Widget* ingredient) - { - for (int i = 0; i < 4; ++i) - if (mIngredients[i] == ingredient) - mAlchemy->removeIngredient(i); - - update(); - } - void AlchemyWindow::addRepeatController(MyGUI::Widget* widget) { MyGUI::ControllerItem* item diff --git a/apps/openmw/mwgui/alchemywindow.hpp b/apps/openmw/mwgui/alchemywindow.hpp index 39ea5ec9b3..82e5c3f583 100644 --- a/apps/openmw/mwgui/alchemywindow.hpp +++ b/apps/openmw/mwgui/alchemywindow.hpp @@ -10,6 +10,7 @@ #include #include +#include "itemselection.hpp" #include "windowbase.hpp" #include "../mwmechanics/alchemy.hpp" @@ -44,6 +45,8 @@ namespace MWGui }; FilterType mCurrentFilter; + std::unique_ptr mItemSelectionDialog; + ItemView* mItemView; InventoryItemModel* mModel; SortFilterItemModel* mSortModel; @@ -63,6 +66,7 @@ namespace MWGui void onCancelButtonClicked(MyGUI::Widget* _sender); void onCreateButtonClicked(MyGUI::Widget* _sender); void onIngredientSelected(MyGUI::Widget* _sender); + void onApparatusSelected(MyGUI::Widget* _sender); void onAccept(MyGUI::EditBox*); void onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); @@ -84,7 +88,8 @@ namespace MWGui void onSelectedItem(int index); - void removeIngredient(MyGUI::Widget* ingredient); + void onItemSelected(MWWorld::Ptr item); + void onItemCancel(); void createPotions(int count); diff --git a/apps/openmw/mwgui/birth.cpp b/apps/openmw/mwgui/birth.cpp index 617c373b0b..3dfdd17627 100644 --- a/apps/openmw/mwgui/birth.cpp +++ b/apps/openmw/mwgui/birth.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -18,7 +19,6 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" -#include "ustring.hpp" #include "widgets.hpp" namespace @@ -56,7 +56,8 @@ namespace MWGui MyGUI::Button* okButton; getWidget(okButton, "OKButton"); - okButton->setCaption(toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BirthDialog::onOkClicked); updateBirths(); @@ -70,10 +71,10 @@ namespace MWGui if (shown) okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void BirthDialog::onOpen() @@ -235,7 +236,7 @@ namespace MWGui { MyGUI::TextBox* label = mSpellArea->createWidget("SandBrightText", coord, MyGUI::Align::Default, "Label"); - label->setCaption(toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString( + label->setCaption(MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( categories[category].label, {}))); mSpellItems.push_back(label); coord.top += lineHeight; diff --git a/apps/openmw/mwgui/class.cpp b/apps/openmw/mwgui/class.cpp index f71da8bdf5..d6b4e7f635 100644 --- a/apps/openmw/mwgui/class.cpp +++ b/apps/openmw/mwgui/class.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -20,7 +21,6 @@ #include #include "tooltips.hpp" -#include "ustring.hpp" namespace { @@ -129,10 +129,10 @@ namespace MWGui if (shown) okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void PickClassDialog::onOpen() @@ -546,10 +546,10 @@ namespace MWGui if (shown) okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } // widget controls @@ -869,7 +869,7 @@ namespace MWGui getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &DescriptionDialog::onOkClicked); okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sInputMenu1", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sInputMenu1", {}))); // Make sure the edit box has focus MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); 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/debugwindow.cpp b/apps/openmw/mwgui/debugwindow.cpp index 5d3948e76d..59f695e7f8 100644 --- a/apps/openmw/mwgui/debugwindow.cpp +++ b/apps/openmw/mwgui/debugwindow.cpp @@ -129,6 +129,7 @@ namespace MWGui static std::mutex sBufferMutex; static int64_t sLogStartIndex; static int64_t sLogEndIndex; + static bool hasPrefix = false; void DebugWindow::startLogRecording() { @@ -170,11 +171,17 @@ namespace MWGui addChar(c); if (c == '#') addChar(c); + if (c == '\n') + hasPrefix = false; } }; for (char c : color) addChar(c); - addShieldedStr(prefix); + if (!hasPrefix) + { + addShieldedStr(prefix); + hasPrefix = true; + } addShieldedStr(msg); if (bufferOverflow) sLogStartIndex = (sLogEndIndex + 1) % bufSize; diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 4ab77c3956..0e44b8c03e 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -22,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" @@ -337,7 +340,7 @@ namespace MWGui void DialogueWindow::onTradeComplete() { MyGUI::UString message = MyGUI::LanguageManager::getInstance().replaceTags("#{sBarterDialog5}"); - addResponse({}, message.asUTF8()); + addResponse({}, message); } bool DialogueWindow::exit() @@ -735,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 e70599697a..8264dd60b6 100644 --- a/apps/openmw/mwgui/enchantingdialog.cpp +++ b/apps/openmw/mwgui/enchantingdialog.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -26,7 +27,6 @@ #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" -#include "ustring.hpp" namespace MWGui { @@ -95,7 +95,7 @@ namespace MWGui else { std::string_view name = item.getClass().getName(item); - mName->setCaption(toUString(name)); + mName->setCaption(MyGUI::UString(name)); mItemBox->setItem(item); mItemBox->setUserString("ToolTipType", "ItemPtr"); mItemBox->setUserData(MWWorld::Ptr(item)); @@ -115,23 +115,26 @@ namespace MWGui switch (mEnchanting.getCastStyle()) { case ESM::Enchantment::CastOnce: - mTypeButton->setCaption(toUString( + mTypeButton->setCaption(MyGUI::UString( MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastOnce", "Cast Once"))); setConstantEffect(false); break; case ESM::Enchantment::WhenStrikes: - mTypeButton->setCaption(toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString( - "sItemCastWhenStrikes", "When Strikes"))); + mTypeButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( + "sItemCastWhenStrikes", "When Strikes"))); setConstantEffect(false); break; case ESM::Enchantment::WhenUsed: - mTypeButton->setCaption(toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString( - "sItemCastWhenUsed", "When Used"))); + mTypeButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( + "sItemCastWhenUsed", "When Used"))); setConstantEffect(false); break; case ESM::Enchantment::ConstantEffect: - mTypeButton->setCaption(toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString( - "sItemCastConstant", "Cast Constant"))); + mTypeButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( + "sItemCastConstant", "Cast Constant"))); setConstantEffect(true); break; } @@ -370,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 c4f0f804a6..7f62bbf49c 100644 --- a/apps/openmw/mwgui/formatting.cpp +++ b/apps/openmw/mwgui/formatting.cpp @@ -475,7 +475,7 @@ namespace MWGui::Formatting : GraphicElement(parent, pag, blockStyle) , mTextStyle(textStyle) { - MyGUI::EditBox* box = parent->createWidget("NormalText", + Gui::EditBox* box = parent->createWidget("NormalText", MyGUI::IntCoord(0, pag.getCurrentTop(), pag.getPageWidth(), 0), MyGUI::Align::Left | MyGUI::Align::Top, parent->getName() + MyGUI::utility::toString(parent->getChildCount())); box->setEditStatic(true); diff --git a/apps/openmw/mwgui/formatting.hpp b/apps/openmw/mwgui/formatting.hpp index f093a36dfe..9a215b200b 100644 --- a/apps/openmw/mwgui/formatting.hpp +++ b/apps/openmw/mwgui/formatting.hpp @@ -161,7 +161,7 @@ namespace MWGui private: int currentFontHeight() const; TextStyle mTextStyle; - MyGUI::EditBox* mEditBox; + Gui::EditBox* mEditBox; }; class ImageElement : public GraphicElement diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index d6eb07e8aa..1c8aad5447 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -421,18 +421,23 @@ namespace MWGui mSpellBox->setUserString("ToolTipType", "Spell"); 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) @@ -491,6 +496,7 @@ namespace MWGui mSpellStatus->setProgressPosition(0); mSpellImage->setItem(MWWorld::Ptr()); mSpellBox->clearUserStrings(); + mSpellBox->setUserData(MyGUI::Any::Null); } void HUD::unsetSelectedWeapon() @@ -520,6 +526,7 @@ namespace MWGui mWeapBox->setUserString("ToolTipLayout", "HandToHandToolTip"); mWeapBox->setUserString("Caption_HandToHandText", itemName); mWeapBox->setUserString("ImageTexture_HandToHandImage", icon); + mWeapBox->setUserData(MyGUI::Any::Null); } void HUD::setCrosshairVisible(bool visible) @@ -650,17 +657,28 @@ namespace MWGui updateEnemyHealthBar(); } - void HUD::resetEnemy() + void HUD::clear() { mEnemyActorId = -1; mEnemyHealthTimer = -1; - } - void HUD::clear() - { - unsetSelectedSpell(); - unsetSelectedWeapon(); - resetEnemy(); + mWeaponSpellTimer = 0.f; + mWeaponName = std::string(); + mSpellName = std::string(); + mWeaponSpellBox->setVisible(false); + + mWeapStatus->setProgressRange(100); + mWeapStatus->setProgressPosition(0); + mSpellStatus->setProgressRange(100); + mSpellStatus->setProgressPosition(0); + + mWeapImage->setItem(MWWorld::Ptr()); + mSpellImage->setItem(MWWorld::Ptr()); + + mWeapBox->clearUserStrings(); + mWeapBox->setUserData(MyGUI::Any::Null); + mSpellBox->clearUserStrings(); + mSpellBox->setUserData(MyGUI::Any::Null); } void HUD::customMarkerCreated(MyGUI::Widget* marker) diff --git a/apps/openmw/mwgui/hud.hpp b/apps/openmw/mwgui/hud.hpp index 1dd9cdb521..8dd98628c4 100644 --- a/apps/openmw/mwgui/hud.hpp +++ b/apps/openmw/mwgui/hud.hpp @@ -58,7 +58,6 @@ namespace MWGui MyGUI::Widget* getEffectBox() { return mEffectBox; } void setEnemy(const MWWorld::Ptr& enemy); - void resetEnemy(); void clear() override; 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 610e34e3cd..0fbd15dda2 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -556,6 +556,20 @@ namespace MWGui std::unique_ptr action = ptr.getClass().use(ptr, force); action->execute(player); + // Handles partial equipping (final part) + if (mEquippedStackableCount.has_value()) + { + // the count to unequip + int count = ptr.getCellRef().getCount() - mDragAndDrop->mDraggedCount - mEquippedStackableCount.value(); + if (count > 0) + { + MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); + invStore.unequipItemQuantity(ptr, count); + updateItemView(); + } + mEquippedStackableCount.reset(); + } + if (isVisible()) { mItemView->update(); @@ -581,27 +595,21 @@ namespace MWGui } // Handles partial equipping - const std::pair, bool> slots = ptr.getClass().getEquipmentSlots(ptr); + mEquippedStackableCount.reset(); + const auto slots = ptr.getClass().getEquipmentSlots(ptr); if (!slots.first.empty() && slots.second) { - int equippedStackableCount = 0; MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator slotIt = invStore.getSlot(slots.first.front()); - // Get the count before useItem() + // Save the currently equipped count before useItem() if (slotIt != invStore.end() && slotIt->getCellRef().getRefId() == ptr.getCellRef().getRefId()) - equippedStackableCount = slotIt->getRefData().getCount(); - - useItem(ptr); - int unequipCount = ptr.getRefData().getCount() - mDragAndDrop->mDraggedCount - equippedStackableCount; - if (unequipCount > 0) - { - invStore.unequipItemQuantity(ptr, unequipCount); - updateItemView(); - } + mEquippedStackableCount = slotIt->getCellRef().getCount(); + else + mEquippedStackableCount = 0; } - else - MWBase::Environment::get().getLuaManager()->useItem(ptr, MWMechanics::getPlayer(), false); + + MWBase::Environment::get().getLuaManager()->useItem(ptr, MWMechanics::getPlayer(), false); // If item is ingredient or potion don't stop drag and drop to simplify action of taking more than one 1 // item @@ -727,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); @@ -747,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/inventorywindow.hpp b/apps/openmw/mwgui/inventorywindow.hpp index f3d8e3dcd6..9fc77ceec5 100644 --- a/apps/openmw/mwgui/inventorywindow.hpp +++ b/apps/openmw/mwgui/inventorywindow.hpp @@ -74,6 +74,7 @@ namespace MWGui DragAndDrop* mDragAndDrop; int mSelectedItem; + std::optional mEquippedStackableCount; MWWorld::Ptr mPtr; diff --git a/apps/openmw/mwgui/itemchargeview.cpp b/apps/openmw/mwgui/itemchargeview.cpp index 92fff6f873..02c3cc182c 100644 --- a/apps/openmw/mwgui/itemchargeview.cpp +++ b/apps/openmw/mwgui/itemchargeview.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -18,7 +19,6 @@ #include "itemmodel.hpp" #include "itemwidget.hpp" -#include "ustring.hpp" namespace MWGui { @@ -128,6 +128,11 @@ namespace MWGui mLines.swap(lines); + std::stable_sort(mLines.begin(), mLines.end(), + [](const MWGui::ItemChargeView::Line& a, const MWGui::ItemChargeView::Line& b) { + return Misc::StringUtils::ciLess(a.mText->getCaption(), b.mText->getCaption()); + }); + layoutWidgets(); } @@ -177,7 +182,7 @@ namespace MWGui void ItemChargeView::updateLine(const ItemChargeView::Line& line) { std::string_view name = line.mItemPtr.getClass().getName(line.mItemPtr); - line.mText->setCaption(toUString(name)); + line.mText->setCaption(MyGUI::UString(name)); line.mCharge->setVisible(false); switch (mDisplayMode) 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/itemselection.hpp b/apps/openmw/mwgui/itemselection.hpp index 78f865bb55..fe87d7e38a 100644 --- a/apps/openmw/mwgui/itemselection.hpp +++ b/apps/openmw/mwgui/itemselection.hpp @@ -32,6 +32,8 @@ namespace MWGui void setCategory(int category); void setFilter(int filter); + SortFilterItemModel* getSortModel() { return mSortModel; } + private: ItemView* mItemView; SortFilterItemModel* mSortModel; 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/keyboardnavigation.cpp b/apps/openmw/mwgui/keyboardnavigation.cpp index a8fb52c95e..85c7d8ba88 100644 --- a/apps/openmw/mwgui/keyboardnavigation.cpp +++ b/apps/openmw/mwgui/keyboardnavigation.cpp @@ -183,6 +183,10 @@ namespace MWGui return switchFocus(D_Down, false); case MyGUI::KeyCode::Tab: return switchFocus(MyGUI::InputManager::getInstance().isShiftPressed() ? D_Prev : D_Next, true); + case MyGUI::KeyCode::Period: + return switchFocus(D_Prev, true); + case MyGUI::KeyCode::Slash: + return switchFocus(D_Next, true); case MyGUI::KeyCode::Return: case MyGUI::KeyCode::NumpadEnter: case MyGUI::KeyCode::Space: diff --git a/apps/openmw/mwgui/layout.cpp b/apps/openmw/mwgui/layout.cpp index fb0fb5e1c5..8d70bc956b 100644 --- a/apps/openmw/mwgui/layout.cpp +++ b/apps/openmw/mwgui/layout.cpp @@ -3,11 +3,10 @@ #include #include #include +#include #include #include -#include "ustring.hpp" - namespace MWGui { void Layout::initialise(std::string_view _layout) @@ -52,16 +51,15 @@ namespace MWGui { MyGUI::Widget* pt; getWidget(pt, name); - static_cast(pt)->setCaption(toUString(caption)); + static_cast(pt)->setCaption(MyGUI::UString(caption)); } void Layout::setTitle(std::string_view title) { MyGUI::Window* window = static_cast(mMainWidget); - MyGUI::UString uTitle = toUString(title); - if (window->getCaption() != uTitle) - window->setCaptionWithReplacing(uTitle); + if (window->getCaption() != title) + window->setCaptionWithReplacing(MyGUI::UString(title)); } MyGUI::Widget* Layout::getWidget(std::string_view _name) diff --git a/apps/openmw/mwgui/levelupdialog.cpp b/apps/openmw/mwgui/levelupdialog.cpp index 41b2dadeb9..2160a04b1b 100644 --- a/apps/openmw/mwgui/levelupdialog.cpp +++ b/apps/openmw/mwgui/levelupdialog.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -22,7 +23,6 @@ #include "../mwmechanics/npcstats.hpp" #include "class.hpp" -#include "ustring.hpp" namespace { @@ -176,7 +176,7 @@ namespace MWGui if (levelupdescription.empty()) levelupdescription = Fallback::Map::getString("Level_Up_Default"); - mLevelDescription->setCaption(toUString(levelupdescription)); + mLevelDescription->setCaption(MyGUI::UString(levelupdescription)); unsigned int availableAttributes = 0; for (const ESM::Attribute& attribute : MWBase::Environment::get().getESMStore()->get()) diff --git a/apps/openmw/mwgui/loadingscreen.cpp b/apps/openmw/mwgui/loadingscreen.cpp index fd7afa2d8a..1723841b32 100644 --- a/apps/openmw/mwgui/loadingscreen.cpp +++ b/apps/openmw/mwgui/loadingscreen.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -191,7 +192,7 @@ namespace MWGui // we may still want to show the label if the caller requested it if (mImportantLabel) { - MWBase::Environment::get().getWindowManager()->messageBox(mLoadingText->getCaption().asUTF8()); + MWBase::Environment::get().getWindowManager()->messageBox(mLoadingText->getCaption()); mImportantLabel = false; } } 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/merchantrepair.cpp b/apps/openmw/mwgui/merchantrepair.cpp index c5393fbfb7..3be0bb1c06 100644 --- a/apps/openmw/mwgui/merchantrepair.cpp +++ b/apps/openmw/mwgui/merchantrepair.cpp @@ -47,6 +47,8 @@ namespace MWGui MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); int categories = MWWorld::ContainerStore::Type_Weapon | MWWorld::ContainerStore::Type_Armor; + std::vector> items; + for (MWWorld::ContainerStoreIterator iter(store.begin(categories)); iter != store.end(); ++iter) { if (iter->getClass().hasItemHealth(*iter)) @@ -76,22 +78,31 @@ namespace MWGui name += " - " + MyGUI::utility::toString(price) + MWBase::Environment::get().getESMStore()->get().find("sgp")->mValue.getString(); - MyGUI::Button* button = mList->createWidget(price <= playerGold - ? "SandTextButton" - : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip - 0, currentY, 0, lineHeight, MyGUI::Align::Default); - - currentY += lineHeight; - - button->setUserString("Price", MyGUI::utility::toString(price)); - button->setUserData(MWWorld::Ptr(*iter)); - button->setCaptionWithReplacing(name); - button->setSize(mList->getWidth(), lineHeight); - button->eventMouseWheel += MyGUI::newDelegate(this, &MerchantRepair::onMouseWheel); - button->setUserString("ToolTipType", "ItemPtr"); - button->eventMouseButtonClick += MyGUI::newDelegate(this, &MerchantRepair::onRepairButtonClick); + items.emplace_back(name, price, *iter); } } + + std::stable_sort(items.begin(), items.end(), + [](const auto& a, const auto& b) { return Misc::StringUtils::ciLess(std::get<0>(a), std::get<0>(b)); }); + + for (const auto& [name, price, ptr] : items) + { + MyGUI::Button* button = mList->createWidget(price <= playerGold + ? "SandTextButton" + : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip + 0, currentY, 0, lineHeight, MyGUI::Align::Default); + + currentY += lineHeight; + + button->setUserString("Price", MyGUI::utility::toString(price)); + button->setUserData(MWWorld::Ptr(ptr)); + button->setCaptionWithReplacing(name); + button->setSize(mList->getWidth(), lineHeight); + button->eventMouseWheel += MyGUI::newDelegate(this, &MerchantRepair::onMouseWheel); + button->setUserString("ToolTipType", "ItemPtr"); + button->eventMouseButtonClick += MyGUI::newDelegate(this, &MerchantRepair::onRepairButtonClick); + } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the // scrollbar is hidden mList->setVisibleVScroll(false); diff --git a/apps/openmw/mwgui/messagebox.cpp b/apps/openmw/mwgui/messagebox.cpp index 49d474c826..b27adacd0f 100644 --- a/apps/openmw/mwgui/messagebox.cpp +++ b/apps/openmw/mwgui/messagebox.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -46,6 +47,20 @@ namespace MWGui mLastButtonPressed = -1; } + void MessageBoxManager::resetInteractiveMessageBox() + { + if (mInterMessageBoxe) + { + mInterMessageBoxe->setVisible(false); + mInterMessageBoxe.reset(); + } + } + + void MessageBoxManager::setLastButtonPressed(int index) + { + mLastButtonPressed = index; + } + void MessageBoxManager::onFrame(float frameDuration) { for (auto it = mMessageBoxes.begin(); it != mMessageBoxes.end();) @@ -112,7 +127,7 @@ namespace MWGui } bool MessageBoxManager::createInteractiveMessageBox( - std::string_view message, const std::vector& buttons) + std::string_view message, const std::vector& buttons, bool immediate, int defaultFocus) { if (mInterMessageBoxe != nullptr) { @@ -120,7 +135,8 @@ namespace MWGui mInterMessageBoxe->setVisible(false); } - mInterMessageBoxe = std::make_unique(*this, std::string{ message }, buttons); + mInterMessageBoxe + = std::make_unique(*this, std::string{ message }, buttons, immediate, defaultFocus); mLastButtonPressed = -1; return true; @@ -200,13 +216,15 @@ namespace MWGui mMainWidget->setVisible(value); } - InteractiveMessageBox::InteractiveMessageBox( - MessageBoxManager& parMessageBoxManager, const std::string& message, const std::vector& buttons) + InteractiveMessageBox::InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message, + const std::vector& buttons, bool immediate, int defaultFocus) : WindowModal(MWBase::Environment::get().getWindowManager()->isGuiMode() ? "openmw_interactive_messagebox_notransp.layout" : "openmw_interactive_messagebox.layout") , mMessageBoxManager(parMessageBoxManager) , mButtonPressed(-1) + , mDefaultFocus(defaultFocus) + , mImmediate(immediate) { int textPadding = 10; // padding between text-widget and main-widget int textButtonPadding = 10; // padding between the text-widget und the button-widget @@ -362,14 +380,17 @@ namespace MWGui MyGUI::Widget* InteractiveMessageBox::getDefaultKeyFocus() { - std::vector keywords{ "sOk", "sYes" }; + if (mDefaultFocus >= 0 && mDefaultFocus < static_cast(mButtons.size())) + return mButtons[mDefaultFocus]; + auto& languageManager = MyGUI::LanguageManager::getInstance(); + std::vector keywords{ languageManager.replaceTags("#{sOk}"), + languageManager.replaceTags("#{sYes}") }; + for (MyGUI::Button* button : mButtons) { - for (const std::string& keyword : keywords) + for (const MyGUI::UString& keyword : keywords) { - if (Misc::StringUtils::ciEqual( - MyGUI::LanguageManager::getInstance().replaceTags("#{" + keyword + "}").asUTF8(), - button->getCaption().asUTF8())) + if (Misc::StringUtils::ciEqual(keyword, button->getCaption())) { return button; } @@ -393,6 +414,12 @@ namespace MWGui { mButtonPressed = index; mMessageBoxManager.onButtonPressed(mButtonPressed); + if (!mImmediate) + return; + + mMessageBoxManager.setLastButtonPressed(mButtonPressed); + MWBase::Environment::get().getInputManager()->changeInputMode( + MWBase::Environment::get().getWindowManager()->isGuiMode()); return; } index++; diff --git a/apps/openmw/mwgui/messagebox.hpp b/apps/openmw/mwgui/messagebox.hpp index b10586549f..bb61bd6bd9 100644 --- a/apps/openmw/mwgui/messagebox.hpp +++ b/apps/openmw/mwgui/messagebox.hpp @@ -25,7 +25,8 @@ namespace MWGui void onFrame(float frameDuration); void createMessageBox(std::string_view message, bool stat = false); void removeStaticMessageBox(); - bool createInteractiveMessageBox(std::string_view message, const std::vector& buttons); + bool createInteractiveMessageBox(std::string_view message, const std::vector& buttons, + bool immediate = false, int defaultFocus = -1); bool isInteractiveMessageBox(); int getMessagesCount(); @@ -40,6 +41,10 @@ namespace MWGui /// @param reset Reset the pressed button to -1 after reading it. int readPressedButton(bool reset = true); + void resetInteractiveMessageBox(); + + void setLastButtonPressed(int index); + typedef MyGUI::delegates::MultiDelegate EventHandle_Int; // Note: this delegate unassigns itself after it was fired, i.e. works once. @@ -88,7 +93,7 @@ namespace MWGui { public: InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message, - const std::vector& buttons); + const std::vector& buttons, bool immediate, int defaultFocus); void mousePressed(MyGUI::Widget* _widget); int readPressedButton(); @@ -107,6 +112,8 @@ namespace MWGui std::vector mButtons; int mButtonPressed; + int mDefaultFocus; + bool mImmediate; }; } 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/race.cpp b/apps/openmw/mwgui/race.cpp index da5c0c9ca8..7b445d419f 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -19,7 +20,6 @@ #include "../mwworld/esmstore.hpp" #include "tooltips.hpp" -#include "ustring.hpp" namespace { @@ -114,7 +114,8 @@ namespace MWGui MyGUI::Button* okButton; getWidget(okButton, "OKButton"); - okButton->setCaption(toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onOkClicked); updateRaces(); @@ -129,10 +130,10 @@ namespace MWGui if (shown) okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void RaceDialog::onOpen() 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 aabd3d46ab..c1602b8407 100644 --- a/apps/openmw/mwgui/repair.cpp +++ b/apps/openmw/mwgui/repair.cpp @@ -49,7 +49,7 @@ namespace MWGui = new SortFilterItemModel(std::make_unique(MWMechanics::getPlayer())); model->setFilter(SortFilterItemModel::Filter_OnlyRepairable); mRepairBox->setModel(model); - + mRepairBox->update(); // Reset scrollbars mRepairBox->resetScrollbars(); } @@ -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 04c3806c0e..4ea21df00c 100644 --- a/apps/openmw/mwgui/review.cpp +++ b/apps/openmw/mwgui/review.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -19,7 +20,6 @@ #include "../mwworld/esmstore.hpp" #include "tooltips.hpp" -#include "ustring.hpp" namespace { @@ -272,7 +272,7 @@ namespace MWGui MyGUI::TextBox* groupWidget = mSkillView->createWidget("SandBrightText", MyGUI::IntCoord(0, coord1.top, coord1.width + coord2.width, coord1.height), MyGUI::Align::Default); groupWidget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); - groupWidget->setCaption(toUString(label)); + groupWidget->setCaption(MyGUI::UString(label)); mSkillWidgets.push_back(groupWidget); const int lineHeight = Settings::gui().mFontSize + 2; @@ -287,7 +287,7 @@ namespace MWGui MyGUI::TextBox* skillValueWidget; skillNameWidget = mSkillView->createWidget("SandText", coord1, MyGUI::Align::Default); - skillNameWidget->setCaption(toUString(text)); + skillNameWidget->setCaption(MyGUI::UString(text)); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); skillValueWidget = mSkillView->createWidget("SandTextRight", coord2, MyGUI::Align::Default); diff --git a/apps/openmw/mwgui/savegamedialog.cpp b/apps/openmw/mwgui/savegamedialog.cpp index 63e4fbc5cc..8330c23f2f 100644 --- a/apps/openmw/mwgui/savegamedialog.cpp +++ b/apps/openmw/mwgui/savegamedialog.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -35,7 +36,6 @@ #include "../mwstate/character.hpp" #include "confirmationdialog.hpp" -#include "ustring.hpp" namespace MWGui { @@ -198,7 +198,7 @@ namespace MWGui } title << " (#{sLevel} " << signature.mPlayerLevel << " " - << MyGUI::TextIterator::toTagsString(toUString(className)) << ")"; + << MyGUI::TextIterator::toTagsString(MyGUI::UString(className)) << ")"; mCharacterSelection->addItem(MyGUI::LanguageManager::getInstance().replaceTags(title.str())); @@ -302,7 +302,7 @@ namespace MWGui if (mSaving) { - MWBase::Environment::get().getStateManager()->saveGame(mSaveNameEdit->getCaption().asUTF8(), mCurrentSlot); + MWBase::Environment::get().getStateManager()->saveGame(mSaveNameEdit->getCaption(), mCurrentSlot); } else { @@ -413,8 +413,8 @@ namespace MWGui text << Misc::fileTimeToString(mCurrentSlot->mTimeStamp, "%Y.%m.%d %T") << "\n"; if (mCurrentSlot->mProfile.mMaximumHealth > 0) - text << std::fixed << std::setprecision(0) << "#{sHealth} " << mCurrentSlot->mProfile.mCurrentHealth << "/" - << mCurrentSlot->mProfile.mMaximumHealth << "\n"; + text << "#{sHealth} " << static_cast(mCurrentSlot->mProfile.mCurrentHealth) << "/" + << static_cast(mCurrentSlot->mProfile.mMaximumHealth) << "\n"; text << "#{sLevel} " << mCurrentSlot->mProfile.mPlayerLevel << "\n"; text << "#{sCell=" << mCurrentSlot->mProfile.mPlayerCellName << "}\n"; diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 71bf276a34..fbd54586df 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -13,6 +12,7 @@ #include #include #include +#include #include #include @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -38,7 +39,6 @@ #include "../mwbase/world.hpp" #include "confirmationdialog.hpp" -#include "ustring.hpp" namespace { @@ -93,20 +93,6 @@ namespace return left.first > right.first; } - std::string getAspect(int x, int y) - { - int gcd = std::gcd(x, y); - if (gcd == 0) - return std::string(); - - int xaspect = x / gcd; - int yaspect = y / gcd; - // special case: 8 : 5 is usually referred to as 16:10 - if (xaspect == 8 && yaspect == 5) - return "16 : 10"; - return MyGUI::utility::toString(xaspect) + " : " + MyGUI::utility::toString(yaspect); - } - const std::string_view checkButtonType = "CheckButton"; const std::string_view sliderType = "Slider"; @@ -254,7 +240,7 @@ namespace MWGui } SettingsWindow::SettingsWindow() - : WindowBase("openmw_settings_window.layout") + : WindowModal("openmw_settings_window.layout") , mKeyboardMode(true) , mCurrentPage(-1) { @@ -354,7 +340,7 @@ namespace MWGui += MyGUI::newDelegate(this, &SettingsWindow::onResetDefaultBindings); // fill resolution list - int screen = Settings::Manager::getInt("screen", "Video"); + const int screen = Settings::video().mScreen; int numDisplayModes = SDL_GetNumDisplayModes(screen); std::vector> resolutions; for (int i = 0; i < numDisplayModes; i++) @@ -366,11 +352,7 @@ namespace MWGui std::sort(resolutions.begin(), resolutions.end(), sortResolutions); for (std::pair& resolution : resolutions) { - std::string str - = MyGUI::utility::toString(resolution.first) + " x " + MyGUI::utility::toString(resolution.second); - std::string aspect = getAspect(resolution.first, resolution.second); - if (!aspect.empty()) - str = str + " (" + aspect + ")"; + std::string str = Misc::getResolutionText(resolution.first, resolution.second, "%i x %i (%i:%i)"); if (mResolutionList->findItemIndexWith(str) == MyGUI::ITEM_NONE) mResolutionList->addItem(str); @@ -396,8 +378,7 @@ namespace MWGui updateMaxLightsComboBox(mMaxLights); - Settings::WindowMode windowMode - = static_cast(Settings::Manager::getInt("window mode", "Video")); + const Settings::WindowMode windowMode = Settings::video().mWindowMode; mWindowBorderButton->setEnabled( windowMode != Settings::WindowMode::Fullscreen && windowMode != Settings::WindowMode::WindowedFullscreen); @@ -469,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) @@ -491,8 +472,8 @@ namespace MWGui int resX, resY; parseResolution(resX, resY, resStr); - Settings::Manager::setInt("resolution x", "Video", resX); - Settings::Manager::setInt("resolution y", "Video", resY); + Settings::video().mResolutionX.set(resX); + Settings::video().mResolutionY.set(resY); apply(); } @@ -506,8 +487,8 @@ namespace MWGui { mResolutionList->setIndexSelected(MyGUI::ITEM_NONE); - int currentX = Settings::Manager::getInt("resolution x", "Video"); - int currentY = Settings::Manager::getInt("resolution y", "Video"); + const int currentX = Settings::video().mResolutionX; + const int currentY = Settings::video().mResolutionY; for (size_t i = 0; i < mResolutionList->getItemCount(); ++i) { @@ -591,23 +572,22 @@ namespace MWGui "#{OMWEngine:ChangeRequiresRestart}", { "#{Interface:OK}" }, true); } - void SettingsWindow::onVSyncModeChanged(MyGUI::ComboBox* _sender, size_t pos) + void SettingsWindow::onVSyncModeChanged(MyGUI::ComboBox* sender, size_t pos) { if (pos == MyGUI::ITEM_NONE) return; - int index = static_cast(_sender->getIndexSelected()); - Settings::Manager::setInt("vsync mode", "Video", index); + Settings::video().mVsyncMode.set(static_cast(sender->getIndexSelected())); apply(); } - void SettingsWindow::onWindowModeChanged(MyGUI::ComboBox* _sender, size_t pos) + void SettingsWindow::onWindowModeChanged(MyGUI::ComboBox* sender, size_t pos) { if (pos == MyGUI::ITEM_NONE) return; - int index = static_cast(_sender->getIndexSelected()); - if (index == static_cast(Settings::WindowMode::WindowedFullscreen)) + const Settings::WindowMode windowMode = static_cast(sender->getIndexSelected()); + if (windowMode == Settings::WindowMode::WindowedFullscreen) { mResolutionList->setEnabled(false); mWindowModeHint->setVisible(true); @@ -618,12 +598,12 @@ namespace MWGui mWindowModeHint->setVisible(false); } - if (index == static_cast(Settings::WindowMode::Windowed)) + if (windowMode == Settings::WindowMode::Windowed) mWindowBorderButton->setEnabled(true); else mWindowBorderButton->setEnabled(false); - Settings::Manager::setInt("window mode", "Video", index); + Settings::video().mWindowMode.set(windowMode); apply(); } @@ -661,18 +641,17 @@ namespace MWGui void SettingsWindow::onButtonToggled(MyGUI::Widget* _sender) { - MyGUI::UString on = toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOn", "On")); + std::string_view on = MWBase::Environment::get().getWindowManager()->getGameSettingString("sOn", "On"); bool newState; if (_sender->castType()->getCaption() == on) { - MyGUI::UString off - = toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOff", "Off")); - _sender->castType()->setCaption(off); + _sender->castType()->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOff", "Off"))); newState = false; } else { - _sender->castType()->setCaption(on); + _sender->castType()->setCaption(MyGUI::UString(on)); newState = true; } @@ -849,14 +828,12 @@ namespace MWGui void SettingsWindow::updateWindowModeSettings() { - size_t index = static_cast(Settings::Manager::getInt("window mode", "Video")); + const Settings::WindowMode windowMode = Settings::video().mWindowMode; + const std::size_t windowModeIndex = static_cast(windowMode); - if (index > static_cast(Settings::WindowMode::Windowed)) - index = MyGUI::ITEM_NONE; + mWindowModeList->setIndexSelected(windowModeIndex); - mWindowModeList->setIndexSelected(index); - - if (index != static_cast(Settings::WindowMode::Windowed) && index != MyGUI::ITEM_NONE) + if (windowMode != Settings::WindowMode::Windowed && windowModeIndex != MyGUI::ITEM_NONE) { // check if this resolution is supported in fullscreen if (mResolutionList->getIndexSelected() != MyGUI::ITEM_NONE) @@ -864,8 +841,8 @@ namespace MWGui const std::string& resStr = mResolutionList->getItemNameAt(mResolutionList->getIndexSelected()); int resX, resY; parseResolution(resX, resY, resStr); - Settings::Manager::setInt("resolution x", "Video", resX); - Settings::Manager::setInt("resolution y", "Video", resY); + Settings::video().mResolutionX.set(resX); + Settings::video().mResolutionY.set(resY); } bool supported = false; @@ -882,8 +859,7 @@ namespace MWGui fallbackY = resY; } - if (resX == Settings::Manager::getInt("resolution x", "Video") - && resY == Settings::Manager::getInt("resolution y", "Video")) + if (resX == Settings::video().mResolutionX && resY == Settings::video().mResolutionY) supported = true; } @@ -891,26 +867,21 @@ namespace MWGui { if (fallbackX != 0 && fallbackY != 0) { - Settings::Manager::setInt("resolution x", "Video", fallbackX); - Settings::Manager::setInt("resolution y", "Video", fallbackY); + Settings::video().mResolutionX.set(fallbackX); + Settings::video().mResolutionY.set(fallbackY); } } mWindowBorderButton->setEnabled(false); } - if (index == static_cast(Settings::WindowMode::WindowedFullscreen)) + if (windowMode == Settings::WindowMode::WindowedFullscreen) mResolutionList->setEnabled(false); } void SettingsWindow::updateVSyncModeSettings() { - int index = static_cast(Settings::Manager::getInt("vsync mode", "Video")); - - if (index < 0 || index > 2) - index = 0; - - mVSyncModeList->setIndexSelected(index); + mVSyncModeList->setIndexSelected(static_cast(Settings::video().mVsyncMode)); } void SettingsWindow::layoutControlsBox() @@ -1070,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/sortfilteritemmodel.cpp b/apps/openmw/mwgui/sortfilteritemmodel.cpp index 4a5e02a881..e5b87abff7 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.cpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.cpp @@ -174,6 +174,7 @@ namespace MWGui : mCategory(Category_All) , mFilter(0) , mSortByType(true) + , mApparatusTypeFilter(-1) { mSourceModel = std::move(sourceModel); } @@ -311,6 +312,16 @@ namespace MWGui return false; } + if ((mFilter & Filter_OnlyAlchemyTools)) + { + if (base.getType() != ESM::Apparatus::sRecordId) + return false; + + int32_t apparatusType = base.get()->mBase->mData.mType; + if (mApparatusTypeFilter >= 0 && apparatusType != mApparatusTypeFilter) + return false; + } + std::string compare = Utf8Stream::lowerCaseUtf8(item.mBase.getClass().getName(item.mBase)); if (compare.find(mNameFilter) == std::string::npos) return false; @@ -352,6 +363,11 @@ namespace MWGui mEffectFilter = Utf8Stream::lowerCaseUtf8(filter); } + void SortFilterItemModel::setApparatusTypeFilter(const int32_t type) + { + mApparatusTypeFilter = type; + } + void SortFilterItemModel::update() { mSourceModel->update(); diff --git a/apps/openmw/mwgui/sortfilteritemmodel.hpp b/apps/openmw/mwgui/sortfilteritemmodel.hpp index 66a22b3afa..d8490f7db1 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.hpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.hpp @@ -27,6 +27,7 @@ namespace MWGui void setFilter(int filter); void setNameFilter(const std::string& filter); void setEffectFilter(const std::string& filter); + void setApparatusTypeFilter(const int32_t type); /// Use ItemStack::Type for sorting? void setSortByType(bool sort) { mSortByType = sort; } @@ -49,6 +50,7 @@ namespace MWGui static constexpr int Filter_OnlyRepairable = (1 << 5); static constexpr int Filter_OnlyRechargable = (1 << 6); static constexpr int Filter_OnlyRepairTools = (1 << 7); + static constexpr int Filter_OnlyAlchemyTools = (1 << 8); private: std::vector mItems; @@ -59,6 +61,7 @@ namespace MWGui int mFilter; bool mSortByType; + int32_t mApparatusTypeFilter; // filter by apparatus type std::string mNameFilter; // filter by item name std::string mEffectFilter; // filter by magic effect }; diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 1618f34a7a..bb97ccb82f 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) @@ -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/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/spellview.cpp b/apps/openmw/mwgui/spellview.cpp index 97de7dbc06..678f6ffe1f 100644 --- a/apps/openmw/mwgui/spellview.cpp +++ b/apps/openmw/mwgui/spellview.cpp @@ -238,7 +238,7 @@ namespace MWGui mLines.emplace_back(separator, (MyGUI::Widget*)nullptr, NoSpellIndex); } - MyGUI::TextBox* groupWidget = mScrollView->createWidget("SandBrightText", + MyGUI::TextBox* groupWidget = mScrollView->createWidget("SandBrightText", MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 24), MyGUI::Align::Left | MyGUI::Align::Top); groupWidget->setCaptionWithReplacing(label); groupWidget->setTextAlign(MyGUI::Align::Left); @@ -246,7 +246,7 @@ namespace MWGui if (!label2.empty()) { - MyGUI::TextBox* groupWidget2 = mScrollView->createWidget("SandBrightText", + MyGUI::TextBox* groupWidget2 = mScrollView->createWidget("SandBrightText", MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 24), MyGUI::Align::Left | MyGUI::Align::Top); groupWidget2->setCaptionWithReplacing(label2); groupWidget2->setTextAlign(MyGUI::Align::Right); diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp index cb16ab6d15..6e7d2c2ba2 100644 --- a/apps/openmw/mwgui/statswindow.cpp +++ b/apps/openmw/mwgui/statswindow.cpp @@ -32,7 +32,6 @@ #include "../mwmechanics/npcstats.hpp" #include "tooltips.hpp" -#include "ustring.hpp" namespace MWGui { @@ -417,7 +416,7 @@ namespace MWGui MyGUI::TextBox* groupWidget = mSkillView->createWidget("SandBrightText", MyGUI::IntCoord(0, coord1.top, coord1.width + coord2.width, coord1.height), MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); - groupWidget->setCaption(toUString(label)); + groupWidget->setCaption(MyGUI::UString(label)); groupWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); mSkillWidgets.push_back(groupWidget); @@ -433,7 +432,7 @@ namespace MWGui skillNameWidget = mSkillView->createWidget( "SandText", coord1, MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); - skillNameWidget->setCaption(toUString(text)); + skillNameWidget->setCaption(MyGUI::UString(text)); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); skillValueWidget = mSkillView->createWidget( diff --git a/apps/openmw/mwgui/textinput.cpp b/apps/openmw/mwgui/textinput.cpp index 18a56e7284..5f47b96f03 100644 --- a/apps/openmw/mwgui/textinput.cpp +++ b/apps/openmw/mwgui/textinput.cpp @@ -5,8 +5,7 @@ #include #include - -#include "ustring.hpp" +#include namespace MWGui { @@ -35,10 +34,10 @@ namespace MWGui if (shown) okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void TextInputDialog::setTextLabel(std::string_view label) diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index 929d78f3b1..9ee7d08f31 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -119,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); @@ -186,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") { @@ -210,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") { @@ -236,13 +237,15 @@ 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") @@ -303,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, @@ -443,7 +446,7 @@ namespace MWGui const std::string realImage = Misc::ResourceHelpers::correctIconPath(image, MWBase::Environment::get().getResourceSystem()->getVFS()); - MyGUI::EditBox* captionWidget = mDynamicToolTipBox->createWidget( + Gui::EditBox* captionWidget = mDynamicToolTipBox->createWidget( "NormalText", MyGUI::IntCoord(0, 0, 300, 300), MyGUI::Align::Left | MyGUI::Align::Top, "ToolTipCaption"); captionWidget->setEditStatic(true); captionWidget->setNeedKeyFocus(false); @@ -452,7 +455,7 @@ namespace MWGui int captionHeight = std::max(!caption.empty() ? captionSize.height : 0, imageSize); - MyGUI::EditBox* textWidget = mDynamicToolTipBox->createWidget("SandText", + Gui::EditBox* textWidget = mDynamicToolTipBox->createWidget("SandText", MyGUI::IntCoord(0, captionHeight + imageCaptionVPadding, 300, 300 - captionHeight - imageCaptionVPadding), MyGUI::Align::Stretch, "ToolTipText"); textWidget->setEditStatic(true); @@ -474,14 +477,17 @@ namespace MWGui MyGUI::ImageBox* icon = mDynamicToolTipBox->createWidget("MarkerButton", MyGUI::IntCoord(padding.left, totalSize.height + padding.top, 8, 8), MyGUI::Align::Default); icon->setColour(MyGUI::Colour(1.0f, 0.3f, 0.3f)); - MyGUI::EditBox* edit = mDynamicToolTipBox->createWidget("SandText", + Gui::EditBox* edit = mDynamicToolTipBox->createWidget("SandText", 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); @@ -953,8 +959,7 @@ namespace MWGui widget->setUserString("Caption_MagicEffectSchool", "#{sSchool}: " + MyGUI::TextIterator::toTagsString( - store->get().find(effect->mData.mSchool)->mSchool->mName) - .asUTF8()); + store->get().find(effect->mData.mSchool)->mSchool->mName)); widget->setUserString("ImageTexture_MagicEffectImage", icon); } } 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/ustring.hpp b/apps/openmw/mwgui/ustring.hpp deleted file mode 100644 index 5a6c30312a..0000000000 --- a/apps/openmw/mwgui/ustring.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef MWGUI_USTRING_H -#define MWGUI_USTRING_H - -#include - -namespace MWGui -{ - // FIXME: Remove once we get a version of MyGUI that supports string_view - inline MyGUI::UString toUString(std::string_view string) - { - return { string.data(), string.size() }; - } -} - -#endif \ No newline at end of file diff --git a/apps/openmw/mwgui/widgets.cpp b/apps/openmw/mwgui/widgets.cpp index debacfbed9..d824682308 100644 --- a/apps/openmw/mwgui/widgets.cpp +++ b/apps/openmw/mwgui/widgets.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -20,8 +21,6 @@ #include "../mwworld/esmstore.hpp" -#include "ustring.hpp" - namespace MWGui::Widgets { /* MWSkill */ @@ -135,8 +134,7 @@ namespace MWGui::Widgets } else { - MyGUI::UString name = toUString(attribute->mName); - mAttributeNameWidget->setCaption(name); + mAttributeNameWidget->setCaption(MyGUI::UString(attribute->mName)); } } if (mAttributeValueWidget) @@ -355,12 +353,10 @@ namespace MWGui::Widgets const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - const ESM::MagicEffect* magicEffect = store.get().search(mEffectParams.mEffectID); + const ESM::MagicEffect* magicEffect = store.get().find(mEffectParams.mEffectID); const ESM::Attribute* attribute = store.get().search(mEffectParams.mAttribute); const ESM::Skill* skill = store.get().search(mEffectParams.mSkill); - assert(magicEffect); - auto windowManager = MWBase::Environment::get().getWindowManager(); std::string_view pt = windowManager->getGameSettingString("spoint", {}); @@ -499,13 +495,13 @@ namespace MWGui::Widgets { std::stringstream out; out << mValue << "/" << mMax; - mBarTextWidget->setCaption(out.str().c_str()); + mBarTextWidget->setCaption(out.str()); } } void MWDynamicStat::setTitle(std::string_view text) { if (mTextWidget) - mTextWidget->setCaption(toUString(text)); + mTextWidget->setCaption(MyGUI::UString(text)); } MWDynamicStat::~MWDynamicStat() {} 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 d832a25023..09b4bd5d5f 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -117,7 +117,6 @@ #include "tradewindow.hpp" #include "trainingwindow.hpp" #include "travelwindow.hpp" -#include "ustring.hpp" #include "videowidget.hpp" #include "waitdialog.hpp" @@ -294,8 +293,7 @@ namespace MWGui += MyGUI::newDelegate(this, &WindowManager::onClipboardRequested); mVideoWrapper = std::make_unique(window, viewer); - mVideoWrapper->setGammaContrast( - Settings::Manager::getFloat("gamma", "Video"), Settings::Manager::getFloat("contrast", "Video")); + mVideoWrapper->setGammaContrast(Settings::video().mGamma, Settings::video().mContrast); if (useShaders) mGuiPlatform->getRenderManagerPtr()->enableShaders(mResourceSystem->getSceneManager()->getShaderManager()); @@ -410,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(); @@ -745,9 +742,9 @@ namespace MWGui } void WindowManager::interactiveMessageBox( - std::string_view message, const std::vector& buttons, bool block) + std::string_view message, const std::vector& buttons, bool block, int defaultFocus) { - mMessageBoxManager->createInteractiveMessageBox(message, buttons); + mMessageBoxManager->createInteractiveMessageBox(message, buttons, block, defaultFocus); updateVisible(); if (block) @@ -780,6 +777,8 @@ namespace MWGui frameRateLimiter.limit(); } + + mMessageBoxManager->resetInteractiveMessageBox(); } } @@ -787,8 +786,8 @@ namespace MWGui { if (getMode() == GM_Dialogue && showInDialogueMode != MWGui::ShowInDialogueMode_Never) { - MyGUI::UString text = MyGUI::LanguageManager::getInstance().replaceTags(toUString(message)); - mDialogueWindow->addMessageBox(text.asUTF8()); + MyGUI::UString text = MyGUI::LanguageManager::getInstance().replaceTags(MyGUI::UString(message)); + mDialogueWindow->addMessageBox(text); } else if (showInDialogueMode != MWGui::ShowInDialogueMode_Only) { @@ -1088,7 +1087,7 @@ namespace MWGui void WindowManager::onRetrieveTag(const MyGUI::UString& _tag, MyGUI::UString& _result) { - std::string_view tag = _tag.asUTF8(); + std::string_view tag = _tag; std::string_view MyGuiPrefix = "setting="; @@ -1101,7 +1100,7 @@ namespace MWGui std::string_view settingSection = tag.substr(0, comma_pos); std::string_view settingTag = tag.substr(comma_pos + 1, tag.length()); - _result = Settings::Manager::getString(settingTag, settingSection); + _result = Settings::get(settingSection, settingTag).get().print(); } else if (tag.starts_with(tokenToFind)) { @@ -1116,7 +1115,7 @@ namespace MWGui else { std::vector split; - Misc::StringUtils::split(std::string{ tag }, split, ":"); + Misc::StringUtils::split(tag, split, ":"); l10n::Manager& l10nManager = *MWBase::Environment::get().getL10nManager(); @@ -1157,25 +1156,22 @@ namespace MWGui changeRes = true; else if (setting.first == "Video" && setting.second == "vsync mode") - mVideoWrapper->setSyncToVBlank(Settings::Manager::getInt("vsync mode", "Video")); + mVideoWrapper->setSyncToVBlank(Settings::video().mVsyncMode); else if (setting.first == "Video" && (setting.second == "gamma" || setting.second == "contrast")) - mVideoWrapper->setGammaContrast( - Settings::Manager::getFloat("gamma", "Video"), Settings::Manager::getFloat("contrast", "Video")); + mVideoWrapper->setGammaContrast(Settings::video().mGamma, Settings::video().mContrast); } if (changeRes) { - mVideoWrapper->setVideoMode(Settings::Manager::getInt("resolution x", "Video"), - Settings::Manager::getInt("resolution y", "Video"), - static_cast(Settings::Manager::getInt("window mode", "Video")), - Settings::Manager::getBool("window border", "Video")); + mVideoWrapper->setVideoMode(Settings::video().mResolutionX, Settings::video().mResolutionY, + Settings::video().mWindowMode, Settings::video().mWindowBorder); } } void WindowManager::windowResized(int x, int y) { - Settings::Manager::setInt("resolution x", "Video", x); - Settings::Manager::setInt("resolution y", "Video", y); + Settings::video().mResolutionX.set(x); + Settings::video().mResolutionY.set(y); // We only want to process changes to window-size related settings. Settings::CategorySettingVector filter = { { "Video", "resolution x" }, { "Video", "resolution y" } }; @@ -1478,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 c9eced4e94..d6a286632c 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; @@ -269,8 +270,8 @@ namespace MWGui enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) override; void staticMessageBox(std::string_view message) override; void removeStaticMessageBox() override; - void interactiveMessageBox( - std::string_view message, const std::vector& buttons = {}, bool block = false) override; + void interactiveMessageBox(std::string_view message, const std::vector& buttons = {}, + bool block = false, int defaultFocus = -1) override; int readPressedButton() override; ///< returns the index of the pressed button or -1 if no button was pressed ///< (->MessageBoxmanager->InteractiveMessageBox) diff --git a/apps/openmw/mwinput/actionmanager.cpp b/apps/openmw/mwinput/actionmanager.cpp index eb82c160f9..5f9a3fde85 100644 --- a/apps/openmw/mwinput/actionmanager.cpp +++ b/apps/openmw/mwinput/actionmanager.cpp @@ -4,7 +4,7 @@ #include -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" @@ -170,10 +170,9 @@ namespace MWInput void ActionManager::screenshot() { - const std::string& settingStr = Settings::Manager::getString("screenshot type", "Video"); - bool regularScreenshot = settingStr.empty() || settingStr == "regular"; + const Settings::ScreenshotSettings& settings = Settings::video().mScreenshotType; - if (regularScreenshot) + if (settings.mType == Settings::ScreenshotType::Regular) { mScreenCaptureHandler->setFramesToCapture(1); mScreenCaptureHandler->captureNextFrame(*mViewer); 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/mwinput/sensormanager.cpp b/apps/openmw/mwinput/sensormanager.cpp index 32e48a008e..298006030a 100644 --- a/apps/openmw/mwinput/sensormanager.cpp +++ b/apps/openmw/mwinput/sensormanager.cpp @@ -42,8 +42,7 @@ namespace MWInput float angle = 0; - SDL_DisplayOrientation currentOrientation - = SDL_GetDisplayOrientation(Settings::Manager::getInt("screen", "Video")); + SDL_DisplayOrientation currentOrientation = SDL_GetDisplayOrientation(Settings::video().mScreen); switch (currentOrientation) { case SDL_ORIENTATION_UNKNOWN: diff --git a/apps/openmw/mwlua/camerabindings.cpp b/apps/openmw/mwlua/camerabindings.cpp index bbdba00ee2..e3470eb853 100644 --- a/apps/openmw/mwlua/camerabindings.cpp +++ b/apps/openmw/mwlua/camerabindings.cpp @@ -94,8 +94,8 @@ namespace MWLua api["getViewTransform"] = [camera]() { return LuaUtil::TransformM{ camera->getViewMatrix() }; }; api["viewportToWorldVector"] = [camera, renderingManager](osg::Vec2f pos) -> osg::Vec3f { - double width = Settings::Manager::getInt("resolution x", "Video"); - double height = Settings::Manager::getInt("resolution y", "Video"); + const double width = Settings::video().mResolutionX; + const double height = Settings::video().mResolutionY; double aspect = (height == 0.0) ? 1.0 : width / height; double fovTan = std::tan(osg::DegreesToRadians(renderingManager->getFieldOfView()) / 2); osg::Matrixf invertedViewMatrix; @@ -106,8 +106,8 @@ namespace MWLua }; api["worldToViewportVector"] = [camera](osg::Vec3f pos) { - double width = Settings::Manager::getInt("resolution x", "Video"); - double height = Settings::Manager::getInt("resolution y", "Video"); + const double width = Settings::video().mResolutionX; + const double height = Settings::video().mResolutionY; osg::Matrix windowMatrix = osg::Matrix::translate(1.0, 1.0, 1.0) * osg::Matrix::scale(0.5 * width, 0.5 * height, 0.5); diff --git a/apps/openmw/mwlua/cellbindings.cpp b/apps/openmw/mwlua/cellbindings.cpp index 7c3b31c7b6..62dcc69330 100644 --- a/apps/openmw/mwlua/cellbindings.cpp +++ b/apps/openmw/mwlua/cellbindings.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -120,7 +121,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()) @@ -232,6 +233,9 @@ namespace MWLua case ESM::REC_FURN4: cell.mStore->template forEachType(visitor); break; + case ESM::REC_IMOD4: + cell.mStore->template forEachType(visitor); + break; case ESM::REC_INGR4: cell.mStore->template forEachType(visitor); break; diff --git a/apps/openmw/mwlua/classbindings.cpp b/apps/openmw/mwlua/classbindings.cpp new file mode 100644 index 0000000000..339b724f19 --- /dev/null +++ b/apps/openmw/mwlua/classbindings.cpp @@ -0,0 +1,80 @@ +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" + +#include "classbindings.hpp" +#include "luamanagerimp.hpp" +#include "stats.hpp" +#include "types/types.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical> : std::false_type + { + }; +} + +namespace MWLua +{ + + sol::table initCoreClassBindings(const Context& context) + { + sol::state_view& lua = context.mLua->sol(); + sol::table classes(context.mLua->sol(), sol::create); + addRecordFunctionBinding(classes, context); + + auto classT = lua.new_usertype("ESM3_Class"); + classT[sol::meta_function::to_string] + = [](const ESM::Class& rec) -> std::string { return "ESM3_Class[" + rec.mId.toDebugString() + "]"; }; + classT["id"] = sol::readonly_property([](const ESM::Class& rec) { return rec.mId.serializeText(); }); + classT["name"] = sol::readonly_property([](const ESM::Class& rec) -> std::string_view { return rec.mName; }); + classT["description"] + = sol::readonly_property([](const ESM::Class& rec) -> std::string_view { return rec.mDescription; }); + + classT["attributes"] = sol::readonly_property([lua](const ESM::Class& rec) -> sol::table { + sol::table res(lua, sol::create); + auto attribute = rec.mData.mAttribute; + for (size_t i = 0; i < attribute.size(); ++i) + { + ESM::RefId attributeId = ESM::Attribute::indexToRefId(attribute[i]); + res[i + 1] = attributeId.serializeText(); + } + return res; + }); + classT["majorSkills"] = sol::readonly_property([lua](const ESM::Class& rec) -> sol::table { + sol::table res(lua, sol::create); + auto skills = rec.mData.mSkills; + for (size_t i = 0; i < skills.size(); ++i) + { + ESM::RefId skillId = ESM::Skill::indexToRefId(skills[i][1]); + res[i + 1] = skillId.serializeText(); + } + return res; + }); + classT["minorSkills"] = sol::readonly_property([lua](const ESM::Class& rec) -> sol::table { + sol::table res(lua, sol::create); + auto skills = rec.mData.mSkills; + for (size_t i = 0; i < skills.size(); ++i) + { + ESM::RefId skillId = ESM::Skill::indexToRefId(skills[i][0]); + res[i + 1] = skillId.serializeText(); + } + return res; + }); + + classT["specialization"] = sol::readonly_property([](const ESM::Class& rec) -> std::string_view { + return ESM::Class::specializationIndexToLuaId.at(rec.mData.mSpecialization); + }); + classT["isPlayable"] + = sol::readonly_property([](const ESM::Class& rec) -> bool { return rec.mData.mIsPlayable; }); + return LuaUtil::makeReadOnly(classes); + } +} diff --git a/apps/openmw/mwlua/classbindings.hpp b/apps/openmw/mwlua/classbindings.hpp new file mode 100644 index 0000000000..9dd9befae4 --- /dev/null +++ b/apps/openmw/mwlua/classbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_CLASSBINDINGS_H +#define MWLUA_CLASSBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initCoreClassBindings(const Context& context); +} + +#endif // MWLUA_CLASSBINDINGS_H diff --git a/apps/openmw/mwlua/corebindings.cpp b/apps/openmw/mwlua/corebindings.cpp index b9ff991406..62f22d0992 100644 --- a/apps/openmw/mwlua/corebindings.cpp +++ b/apps/openmw/mwlua/corebindings.cpp @@ -60,6 +60,8 @@ namespace MWLua api["getRealTime"] = []() { return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); }; + // TODO: remove in global context? + api["getRealFrameDuration"] = []() { return MWBase::Environment::get().getFrameDuration(); }; } sol::table initCorePackage(const Context& context) diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp index 41b2b5c94f..3aaf101a6b 100644 --- a/apps/openmw/mwlua/inputbindings.cpp +++ b/apps/openmw/mwlua/inputbindings.cpp @@ -4,12 +4,14 @@ #include #include +#include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwinput/actions.hpp" +#include "luamanagerimp.hpp" namespace sol { @@ -17,6 +19,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 @@ -52,9 +64,121 @@ 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; + } + + context.mLua->sol().new_usertype("ActionInfo", "key", + sol::property([](const LuaUtil::InputAction::Info& info) { return info.mKey; }), "name", + sol::property([](const LuaUtil::InputAction::Info& info) { return info.mName; }), "description", + sol::property([](const LuaUtil::InputAction::Info& info) { return info.mDescription; }), "type", + sol::property([](const LuaUtil::InputAction::Info& info) { return info.mType; }), "l10n", + sol::property([](const LuaUtil::InputAction::Info& info) { return info.mL10n; }), "defaultValue", + sol::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; + } + + context.mLua->sol().new_usertype("TriggerInfo", "key", + sol::property([](const LuaUtil::InputTrigger::Info& info) { return info.mKey; }), "name", + sol::property([](const LuaUtil::InputTrigger::Info& info) { return info.mName; }), "description", + sol::property([](const LuaUtil::InputTrigger::Info& info) { return info.mDescription; }), "l10n", + sol::property([](const LuaUtil::InputTrigger::Info& info) { 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/itemdata.cpp b/apps/openmw/mwlua/itemdata.cpp new file mode 100644 index 0000000000..6dea2360e7 --- /dev/null +++ b/apps/openmw/mwlua/itemdata.cpp @@ -0,0 +1,134 @@ +#include "itemdata.hpp" + +#include "context.hpp" + +#include "luamanagerimp.hpp" + +#include "../mwworld/class.hpp" + +#include "objectvariant.hpp" + +namespace +{ + using SelfObject = MWLua::SelfObject; + using Index = const SelfObject::CachedStat::Index&; + + constexpr std::array properties = { "condition", /*"enchantmentCharge", "soul", "owner", etc..*/ }; + + void invalidPropErr(std::string_view prop, const MWWorld::Ptr& ptr) + { + throw std::runtime_error("'" + std::string(prop) + "'" + " property does not exist for item " + + std::string(ptr.getClass().getName(ptr)) + "(" + std::string(ptr.getTypeDescription()) + ")"); + } +} + +namespace MWLua +{ + static void addStatUpdateAction(MWLua::LuaManager* manager, const SelfObject& obj) + { + if (!obj.mStatsCache.empty()) + return; // was already added before + manager->addAction( + [obj = Object(obj)] { + LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts(); + if (scripts) + scripts->applyStatsCache(); + }, + "StatUpdateAction"); + } + + class ItemData + { + ObjectVariant mObject; + + public: + ItemData(ObjectVariant object) + : mObject(object) + { + } + + sol::object get(const Context& context, std::string_view prop) const + { + if (mObject.isSelfObject()) + { + SelfObject* self = mObject.asSelfObject(); + auto it = self->mStatsCache.find({ &ItemData::setValue, std::monostate{}, prop }); + if (it != self->mStatsCache.end()) + return it->second; + } + return sol::make_object(context.mLua->sol(), getValue(context, prop)); + } + + void set(const Context& context, std::string_view prop, const sol::object& value) const + { + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &ItemData::setValue, std::monostate{}, prop }] = value; + } + + sol::object getValue(const Context& context, std::string_view prop) const + { + if (prop == "condition") + { + MWWorld::Ptr o = mObject.ptr(); + if (o.mRef->getType() == ESM::REC_LIGH) + return sol::make_object(context.mLua->sol(), o.getClass().getRemainingUsageTime(o)); + else if (o.getClass().hasItemHealth(o)) + return sol::make_object( + context.mLua->sol(), o.getClass().getItemHealth(o) + o.getCellRef().getChargeIntRemainder()); + } + + return sol::lua_nil; + } + + static void setValue(Index i, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + if (prop == "condition") + { + float cond = LuaUtil::cast(value); + if (ptr.mRef->getType() == ESM::REC_LIGH) + ptr.getClass().setRemainingUsageTime(ptr, cond); + else if (ptr.getClass().hasItemHealth(ptr)) + { + // if the value set is less than 0, chargeInt and chargeIntRemainder is set to 0 + ptr.getCellRef().setChargeIntRemainder(std::max(0.f, std::modf(cond, &cond))); + ptr.getCellRef().setCharge(std::max(0.f, cond)); + } + else + invalidPropErr(prop, ptr); + } + } + }; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + void addItemDataBindings(sol::table& item, const Context& context) + { + item["itemData"] = [](const sol::object& object) -> sol::optional { + ObjectVariant o(object); + if (o.ptr().getClass().isItem(o.ptr()) || o.ptr().mRef->getType() == ESM::REC_LIGH) + return ItemData(std::move(o)); + return {}; + }; + + sol::usertype itemData = context.mLua->sol().new_usertype("ItemData"); + itemData[sol::meta_function::new_index] = [](const ItemData& stat, const sol::variadic_args args) { + throw std::runtime_error("Unknown ItemData property '" + args.get() + "'"); + }; + + for (std::string_view prop : properties) + { + itemData[prop] = sol::property([context, prop](const ItemData& stat) { return stat.get(context, prop); }, + [context, prop](const ItemData& stat, const sol::object& value) { stat.set(context, prop, value); }); + } + } +} diff --git a/apps/openmw/mwlua/itemdata.hpp b/apps/openmw/mwlua/itemdata.hpp new file mode 100644 index 0000000000..f70705fb6c --- /dev/null +++ b/apps/openmw/mwlua/itemdata.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_ITEMDATA_H +#define MWLUA_ITEMDATA_H + +#include + +namespace MWLua +{ + struct Context; + + void addItemDataBindings(sol::table& item, const Context& context); + +} +#endif // MWLUA_ITEMDATA_H diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index 6b1555868d..b87b628a89 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -22,7 +22,7 @@ namespace MWLua class CachedStat { public: - using Index = std::variant; + using Index = std::variant; using Setter = void (*)(const Index&, std::string_view, const MWWorld::Ptr&, const sol::object&); CachedStat(Setter setter, Index index, std::string_view prop) diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 52b5dace3b..2e60d4ea2d 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -129,8 +129,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. @@ -146,9 +146,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) @@ -223,6 +222,7 @@ namespace MWLua PlayerScripts* playerScripts = mPlayer.isEmpty() ? nullptr : dynamic_cast(mPlayer.getRefData().getLuaScripts()); MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); + // TODO: handle main menu input events if (playerScripts && !windowManager->containsMode(MWGui::GM_MainMenu)) { for (const auto& event : mInputEvents) @@ -230,11 +230,13 @@ namespace MWLua } mInputEvents.clear(); mLuaEvents.callMenuEventHandlers(); + double frameDuration = MWBase::Environment::get().getWorld()->getTimeManager()->isPaused() + ? 0.0 + : MWBase::Environment::get().getFrameDuration(); + mInputActions.update(frameDuration); mMenuScripts.update(0); 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) @@ -296,6 +298,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); } @@ -351,6 +355,19 @@ 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); + mEngineEvents.addToQueue(EngineEvents::OnUseItem{ getId(actor), getId(object), force }); + } + void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) { mObjectLists.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. @@ -514,6 +531,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 53031516a8..ccd5386c4a 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -78,10 +79,7 @@ namespace MWLua { mEngineEvents.addToQueue(EngineEvents::OnActivate{ getId(actor), getId(object) }); } - void useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) override - { - mEngineEvents.addToQueue(EngineEvents::OnUseItem{ getId(actor), getId(object), force }); - } + void useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) override; void exteriorCreated(MWWorld::CellStore& cell) override { mEngineEvents.addToQueue(EngineEvents::OnNewExterior{ cell }); @@ -89,6 +87,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; @@ -147,6 +146,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, @@ -210,6 +212,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..0a34c008a7 100644 --- a/apps/openmw/mwlua/magicbindings.cpp +++ b/apps/openmw/mwlua/magicbindings.cpp @@ -839,7 +839,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/mwscriptbindings.cpp b/apps/openmw/mwlua/mwscriptbindings.cpp index 92957efb71..af88249d3e 100644 --- a/apps/openmw/mwlua/mwscriptbindings.cpp +++ b/apps/openmw/mwlua/mwscriptbindings.cpp @@ -140,7 +140,7 @@ namespace MWLua = sol::overload([](const GlobalStore& store, std::string_view globalId, float val) { auto g = store.search(ESM::RefId::deserializeText(globalId)); if (g == nullptr) - return; + throw std::runtime_error("No variable \"" + std::string(globalId) + "\" in GlobalStore"); char varType = MWBase::Environment::get().getWorld()->getGlobalVariableType(globalId); if (varType == 's' || varType == 'l') { diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp index 86c8ef31e8..7e1845aeac 100644 --- a/apps/openmw/mwlua/nearbybindings.cpp +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -3,11 +3,15 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwphysics/raycasting.hpp" +#include "../mwworld/cell.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/scene.hpp" #include "luamanagerimp.hpp" #include "objectlists.hpp" @@ -262,6 +266,39 @@ namespace MWLua *MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, from, to, includeFlags); }; + api["findNearestNavMeshPosition"] = [](const osg::Vec3f& position, const sol::optional& options) { + DetourNavigator::AgentBounds agentBounds = defaultAgentBounds; + std::optional searchAreaHalfExtents; + DetourNavigator::Flags includeFlags = defaultIncludeFlags; + + if (options.has_value()) + { + if (const auto& t = options->get>("agentBounds")) + { + if (const auto& v = t->get>("shapeType")) + agentBounds.mShapeType = *v; + if (const auto& v = t->get>("halfExtents")) + agentBounds.mHalfExtents = *v; + } + if (const auto& v = options->get>("searchAreaHalfExtents")) + searchAreaHalfExtents = *v; + if (const auto& v = options->get>("includeFlags")) + includeFlags = *v; + } + + if (!searchAreaHalfExtents.has_value()) + { + const bool isEsm4 = MWBase::Environment::get().getWorldScene()->getCurrentCell()->getCell()->isEsm4(); + const float halfExtents = isEsm4 + ? (1 + 2 * Constants::ESM4CellGridRadius) * Constants::ESM4CellSizeInUnits + : (1 + 2 * Constants::CellGridRadius) * Constants::CellSizeInUnits; + searchAreaHalfExtents = osg::Vec3f(halfExtents, halfExtents, halfExtents); + } + + return DetourNavigator::findNearestNavMeshPosition(*MWBase::Environment::get().getWorld()->getNavigator(), + agentBounds, position, *searchAreaHalfExtents, includeFlags); + }; + return LuaUtil::makeReadOnly(api); } } diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index 7db7877245..f45decea3c 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -124,7 +124,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 +256,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) { @@ -312,11 +312,17 @@ namespace MWLua }; objectT["ownerFactionId"] = sol::property(getOwnerFactionId, setOwnerFactionId); - auto getOwnerFactionRank = [](const ObjectT& o) -> int { return o.ptr().getCellRef().getFactionRank(); }; - auto setOwnerFactionRank = [](const ObjectT& object, int factionRank) { + auto getOwnerFactionRank = [](const ObjectT& o) -> sol::optional { + int rank = o.ptr().getCellRef().getFactionRank(); + if (rank < 0) + return sol::nullopt; + else + return rank; + }; + auto setOwnerFactionRank = [](const ObjectT& object, sol::optional factionRank) { if (std::is_same_v && !dynamic_cast(&object)) throw std::runtime_error("Local scripts can set an owner faction rank only on self"); - object.ptr().getCellRef().setFactionRank(factionRank); + object.ptr().getCellRef().setFactionRank(factionRank.value_or(-1)); }; objectT["ownerFactionRank"] = sol::property(getOwnerFactionRank, setOwnerFactionRank); @@ -334,10 +340,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()) { @@ -411,20 +417,23 @@ namespace MWLua using DelayedRemovalFn = std::function; auto removeFn = [](const MWWorld::Ptr ptr, int countToRemove) -> std::optional { - int currentCount = ptr.getRefData().getCount(); + 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(currentCount - countToRemove); // 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 [countToRemove](MWWorld::Ptr ptr) { + return [signedCountToRemove](MWWorld::Ptr ptr) { // Restore the original count - ptr.getRefData().setCount(ptr.getRefData().getCount() + countToRemove); + ptr.getCellRef().setCount(ptr.getCellRef().getCount(false) + signedCountToRemove); // And now remove properly if (ptr.getContainerStore()) - ptr.getContainerStore()->remove(ptr, countToRemove, false); + ptr.getContainerStore()->remove(ptr, std::abs(signedCountToRemove), false); else { MWBase::Environment::get().getWorld()->disable(ptr); @@ -434,7 +443,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()); }); }; @@ -454,7 +463,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(); @@ -465,9 +474,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()) @@ -478,7 +487,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(); @@ -499,9 +508,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); @@ -513,10 +522,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 5c1e536dd6..ff74704ed8 100644 --- a/apps/openmw/mwlua/stats.cpp +++ b/apps/openmw/mwlua/stats.cpp @@ -85,7 +85,7 @@ namespace MWLua public: sol::object getCurrent(const Context& context) const { - return getValue(context, mObject, &LevelStat::setValue, 0, "current", + return getValue(context, mObject, &LevelStat::setValue, std::monostate{}, "current", [](const MWWorld::Ptr& ptr) { return ptr.getClass().getCreatureStats(ptr).getLevel(); }); } @@ -93,7 +93,7 @@ namespace MWLua { SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); - obj->mStatsCache[SelfObject::CachedStat{ &LevelStat::setValue, 0, "current" }] = value; + obj->mStatsCache[SelfObject::CachedStat{ &LevelStat::setValue, std::monostate{}, "current" }] = value; } sol::object getProgress(const Context& context) const @@ -446,6 +446,9 @@ namespace MWLua skillT["name"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string_view { return rec.mName; }); skillT["description"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string_view { return rec.mDescription; }); + skillT["specialization"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string_view { + return ESM::Class::specializationIndexToLuaId.at(rec.mData.mSpecialization); + }); skillT["icon"] = sol::readonly_property([vfs](const ESM::Skill& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); @@ -454,6 +457,9 @@ namespace MWLua return nullptr; return &*rec.mSchool; }); + skillT["attribute"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string { + return ESM::Attribute::indexToRefId(rec.mData.mAttribute).serializeText(); + }); auto schoolT = context.mLua->sol().new_usertype("MagicSchool"); schoolT[sol::meta_function::to_string] diff --git a/apps/openmw/mwlua/types/activator.cpp b/apps/openmw/mwlua/types/activator.cpp index a9edc1493f..5ffae0ccd7 100644 --- a/apps/openmw/mwlua/types/activator.cpp +++ b/apps/openmw/mwlua/types/activator.cpp @@ -43,8 +43,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 +52,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 2afe3386ce..a4449f6fb0 100644 --- a/apps/openmw/mwlua/types/actor.cpp +++ b/apps/openmw/mwlua/types/actor.cpp @@ -37,7 +37,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 +51,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 }; @@ -395,6 +395,11 @@ namespace MWLua return dist <= actorsProcessingRange; }; + actor["isDead"] = [](const Object& o) { + const auto& target = o.ptr(); + return target.getClass().getCreatureStats(target).isDead(); + }; + actor["getEncumbrance"] = [](const Object& actor) -> float { const MWWorld::Ptr ptr = actor.ptr(); return ptr.getClass().getEncumbrance(ptr); diff --git a/apps/openmw/mwlua/types/actor.hpp b/apps/openmw/mwlua/types/actor.hpp index 409559475f..4a16b65cbf 100644 --- a/apps/openmw/mwlua/types/actor.hpp +++ b/apps/openmw/mwlua/types/actor.hpp @@ -45,7 +45,7 @@ namespace MWLua providedServices[name] = (services & flag) != 0; } providedServices["Travel"] = !rec.getTransport().empty(); - return providedServices; + return LuaUtil::makeReadOnly(providedServices); }); } } diff --git a/apps/openmw/mwlua/types/apparatus.cpp b/apps/openmw/mwlua/types/apparatus.cpp index 10bdbcdd29..05eae8b2aa 100644 --- a/apps/openmw/mwlua/types/apparatus.cpp +++ b/apps/openmw/mwlua/types/apparatus.cpp @@ -38,8 +38,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..b0df533327 100644 --- a/apps/openmw/mwlua/types/armor.cpp +++ b/apps/openmw/mwlua/types/armor.cpp @@ -95,9 +95,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..dfdbcff1ca 100644 --- a/apps/openmw/mwlua/types/book.cpp +++ b/apps/openmw/mwlua/types/book.cpp @@ -104,9 +104,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..74b03148cb 100644 --- a/apps/openmw/mwlua/types/clothing.cpp +++ b/apps/openmw/mwlua/types/clothing.cpp @@ -90,9 +90,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..ac2e75dea4 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 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..54a3c37750 100644 --- a/apps/openmw/mwlua/types/creature.cpp +++ b/apps/openmw/mwlua/types/creature.cpp @@ -29,8 +29,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 +37,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..a4185434c3 100644 --- a/apps/openmw/mwlua/types/door.cpp +++ b/apps/openmw/mwlua/types/door.cpp @@ -55,8 +55,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 +63,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 +92,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 +101,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..72b9f27263 100644 --- a/apps/openmw/mwlua/types/ingredient.cpp +++ b/apps/openmw/mwlua/types/ingredient.cpp @@ -33,8 +33,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 e15be1a2e4..648229a5e5 100644 --- a/apps/openmw/mwlua/types/item.cpp +++ b/apps/openmw/mwlua/types/item.cpp @@ -2,17 +2,21 @@ #include "../../mwworld/class.hpp" +#include "../itemdata.hpp" + #include "types.hpp" namespace MWLua { - void addItemBindings(sol::table item) + void addItemBindings(sol::table item, const Context& context) { item["getEnchantmentCharge"] = [](const Object& object) { return object.ptr().getCellRef().getEnchantmentCharge(); }; 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..d2cf195219 100644 --- a/apps/openmw/mwlua/types/light.cpp +++ b/apps/openmw/mwlua/types/light.cpp @@ -31,9 +31,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/lockpick.cpp b/apps/openmw/mwlua/types/lockpick.cpp index 786471461a..6de1f7f670 100644 --- a/apps/openmw/mwlua/types/lockpick.cpp +++ b/apps/openmw/mwlua/types/lockpick.cpp @@ -31,9 +31,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..9e6b2d6ae5 100644 --- a/apps/openmw/mwlua/types/misc.cpp +++ b/apps/openmw/mwlua/types/misc.cpp @@ -83,8 +83,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 25997b6468..177c0327ab 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -12,6 +12,7 @@ #include #include +#include "../classbindings.hpp" #include "../localscripts.hpp" #include "../stats.hpp" @@ -81,6 +82,8 @@ namespace MWLua record["baseGold"] = sol::readonly_property([](const ESM::NPC& rec) -> int { return rec.mNpdt.mGold; }); addActorServicesBindings(record, context); + npc["classes"] = initCoreClassBindings(context); + // This function is game-specific, in future we should replace it with something more universal. npc["isWerewolf"] = [](const Object& o) { const MWWorld::Class& cls = o.ptr().getClass(); @@ -265,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/potion.cpp b/apps/openmw/mwlua/types/potion.cpp index 022af56b02..33302a3d34 100644 --- a/apps/openmw/mwlua/types/potion.cpp +++ b/apps/openmw/mwlua/types/potion.cpp @@ -73,9 +73,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..6a3784b41a 100644 --- a/apps/openmw/mwlua/types/probe.cpp +++ b/apps/openmw/mwlua/types/probe.cpp @@ -31,9 +31,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..5e97e8c787 100644 --- a/apps/openmw/mwlua/types/repair.cpp +++ b/apps/openmw/mwlua/types/repair.cpp @@ -6,8 +6,6 @@ #include #include -#include -#include 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..960218ca68 100644 --- a/apps/openmw/mwlua/types/static.cpp +++ b/apps/openmw/mwlua/types/static.cpp @@ -21,8 +21,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 +28,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.cpp b/apps/openmw/mwlua/types/types.cpp index cf03d1baef..632b70938b 100644 --- a/apps/openmw/mwlua/types/types.cpp +++ b/apps/openmw/mwlua/types/types.cpp @@ -45,6 +45,7 @@ namespace MWLua constexpr std::string_view ESM4Flora = "ESM4Flora"; constexpr std::string_view ESM4Furniture = "ESM4Furniture"; constexpr std::string_view ESM4Ingredient = "ESM4Ingredient"; + constexpr std::string_view ESM4ItemMod = "ESM4ItemMod"; constexpr std::string_view ESM4Light = "ESM4Light"; constexpr std::string_view ESM4MiscItem = "ESM4Miscellaneous"; constexpr std::string_view ESM4MovableStatic = "ESM4MovableStatic"; @@ -90,6 +91,7 @@ namespace MWLua { ESM::REC_FLOR4, ObjectTypeName::ESM4Flora }, { ESM::REC_FURN4, ObjectTypeName::ESM4Furniture }, { ESM::REC_INGR4, ObjectTypeName::ESM4Ingredient }, + { ESM::REC_IMOD4, ObjectTypeName::ESM4ItemMod }, { ESM::REC_LIGH4, ObjectTypeName::ESM4Light }, { ESM::REC_MISC4, ObjectTypeName::ESM4MiscItem }, { ESM::REC_MSTT4, ObjectTypeName::ESM4MovableStatic }, @@ -193,9 +195,11 @@ namespace MWLua addActorBindings( addType(ObjectTypeName::Actor, { ESM::REC_INTERNAL_PLAYER, ESM::REC_CREA, ESM::REC_NPC_ }), context); - addItemBindings(addType(ObjectTypeName::Item, - { ESM::REC_ARMO, ESM::REC_BOOK, ESM::REC_CLOT, ESM::REC_INGR, ESM::REC_LIGH, ESM::REC_MISC, ESM::REC_ALCH, - ESM::REC_WEAP, ESM::REC_APPA, ESM::REC_LOCK, ESM::REC_PROB, ESM::REC_REPA })); + addItemBindings( + addType(ObjectTypeName::Item, + { ESM::REC_ARMO, ESM::REC_BOOK, ESM::REC_CLOT, ESM::REC_INGR, ESM::REC_LIGH, ESM::REC_MISC, + ESM::REC_ALCH, ESM::REC_WEAP, ESM::REC_APPA, ESM::REC_LOCK, ESM::REC_PROB, ESM::REC_REPA }), + context); addLockableBindings( addType(ObjectTypeName::Lockable, { ESM::REC_CONT, ESM::REC_DOOR, ESM::REC_CONT4, ESM::REC_DOOR4 })); @@ -234,6 +238,7 @@ namespace MWLua addType(ObjectTypeName::ESM4Flora, { ESM::REC_FLOR4 }); addType(ObjectTypeName::ESM4Furniture, { ESM::REC_FURN4 }); addType(ObjectTypeName::ESM4Ingredient, { ESM::REC_INGR4 }); + addType(ObjectTypeName::ESM4ItemMod, { ESM::REC_IMOD4 }); addType(ObjectTypeName::ESM4Light, { ESM::REC_LIGH4 }); addType(ObjectTypeName::ESM4MiscItem, { ESM::REC_MISC4 }); addType(ObjectTypeName::ESM4MovableStatic, { ESM::REC_MSTT4 }); diff --git a/apps/openmw/mwlua/types/types.hpp b/apps/openmw/mwlua/types/types.hpp index 8fc0932dfe..adac372277 100644 --- a/apps/openmw/mwlua/types/types.hpp +++ b/apps/openmw/mwlua/types/types.hpp @@ -47,7 +47,7 @@ namespace MWLua void addBookBindings(sol::table book, const Context& context); void addContainerBindings(sol::table container, const Context& context); void addDoorBindings(sol::table door, const Context& context); - void addItemBindings(sol::table item); + void addItemBindings(sol::table item, const Context& context); void addActorBindings(sol::table actor, const Context& context); void addWeaponBindings(sol::table weapon, const Context& context); void addNpcBindings(sol::table npc, const Context& context); diff --git a/apps/openmw/mwlua/types/weapon.cpp b/apps/openmw/mwlua/types/weapon.cpp index 993f1ecc95..de9b2efb95 100644 --- a/apps/openmw/mwlua/types/weapon.cpp +++ b/apps/openmw/mwlua/types/weapon.cpp @@ -129,9 +129,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 96f3246ebe..3a838f4544 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" }, @@ -101,6 +100,13 @@ namespace MWLua MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); auto element = context.mLua->sol().new_usertype("Element"); + element[sol::meta_function::to_string] = [](const LuaUi::Element& element) { + std::stringstream res; + res << "UiElement"; + if (element.mLayer != "") + res << "[" << element.mLayer << "]"; + return res.str(); + }; element["layout"] = sol::property([](LuaUi::Element& element) { return element.mLayout; }, [](LuaUi::Element& element, const sol::table& layout) { element.mLayout = layout; }); element["update"] = [luaManager = context.mLuaManager](const std::shared_ptr& element) { @@ -246,10 +252,7 @@ namespace MWLua return luaManager->uiResourceManager()->registerTexture(data); }; - api["screenSize"] = []() { - return osg::Vec2f( - Settings::Manager::getInt("resolution x", "Video"), Settings::Manager::getInt("resolution y", "Video")); - }; + api["screenSize"] = []() { return osg::Vec2f(Settings::video().mResolutionX, Settings::video().mResolutionY); }; api["_getAllUiModes"] = [](sol::this_state lua) { sol::table res(lua, sol::create); 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 743c5d5ab5..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); @@ -1998,12 +2005,12 @@ namespace MWMechanics } bool Actors::playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool persist) const + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool scripted) const { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) { - return iter->second->getCharacterController().playGroup(groupName, mode, number, persist); + return iter->second->getCharacterController().playGroup(groupName, mode, number, scripted); } else { @@ -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/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 98c64397ab..15a39136a6 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -113,7 +113,7 @@ namespace MWMechanics void forceStateUpdate(const MWWorld::Ptr& ptr) const; bool playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool persist = false) const; + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool scripted = false) const; void skipAnimation(const MWWorld::Ptr& ptr) const; bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) const; void persistAnimationStates() const; diff --git a/apps/openmw/mwmechanics/actorutil.cpp b/apps/openmw/mwmechanics/actorutil.cpp index c414ff3032..2d2980075e 100644 --- a/apps/openmw/mwmechanics/actorutil.cpp +++ b/apps/openmw/mwmechanics/actorutil.cpp @@ -39,6 +39,6 @@ namespace MWMechanics { const MagicEffects& magicEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); return (magicEffects.getOrDefault(ESM::MagicEffect::Invisibility).getMagnitude() > 0) - || (magicEffects.getOrDefault(ESM::MagicEffect::Chameleon).getMagnitude() > 75); + || (magicEffects.getOrDefault(ESM::MagicEffect::Chameleon).getMagnitude() >= 75); } } 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/aiescort.cpp b/apps/openmw/mwmechanics/aiescort.cpp index e1d657a207..380c0ace9f 100644 --- a/apps/openmw/mwmechanics/aiescort.cpp +++ b/apps/openmw/mwmechanics/aiescort.cpp @@ -30,8 +30,6 @@ namespace MWMechanics , mZ(z) , mDuration(duration) , mRemainingDuration(static_cast(duration)) - , mCellX(std::numeric_limits::max()) - , mCellY(std::numeric_limits::max()) { mTargetActorRefId = actorId; } @@ -45,8 +43,6 @@ namespace MWMechanics , mZ(z) , mDuration(duration) , mRemainingDuration(static_cast(duration)) - , mCellX(std::numeric_limits::max()) - , mCellY(std::numeric_limits::max()) { mTargetActorRefId = actorId; } @@ -59,8 +55,6 @@ namespace MWMechanics , mZ(escort->mData.mZ) , mDuration(escort->mData.mDuration) , mRemainingDuration(escort->mRemainingDuration) - , mCellX(std::numeric_limits::max()) - , mCellY(std::numeric_limits::max()) { mTargetActorRefId = escort->mTargetId; mTargetActorId = escort->mTargetActorId; @@ -96,6 +90,19 @@ namespace MWMechanics if ((leaderPos - followerPos).length2() <= mMaxDist * mMaxDist) { + // TESCS allows the creation of Escort packages without a specific destination + constexpr float nowhere = std::numeric_limits::max(); + if (mX == nowhere || mY == nowhere) + return true; + if (mZ == nowhere) + { + if (mCellId.empty() + && ESM::positionToExteriorCellLocation(mX, mY) + == actor.getCell()->getCell()->getExteriorCellLocation()) + return false; + return true; + } + const osg::Vec3f dest(mX, mY, mZ); if (pathTo(actor, dest, duration, characterController.getSupportedMovementDirections(), maxHalfExtent)) { diff --git a/apps/openmw/mwmechanics/aiescort.hpp b/apps/openmw/mwmechanics/aiescort.hpp index e22752446d..709b2bee59 100644 --- a/apps/openmw/mwmechanics/aiescort.hpp +++ b/apps/openmw/mwmechanics/aiescort.hpp @@ -59,9 +59,6 @@ namespace MWMechanics float mMaxDist = 450; const float mDuration; // In hours float mRemainingDuration; // In hours - - const int mCellX; - const int mCellY; }; } #endif 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 de626ace95..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. @@ -467,7 +483,7 @@ namespace MWMechanics { ESM::AITarget data = esmPackage.mTarget; package = std::make_unique(ESM::RefId::stringRefId(data.mId.toStringView()), - data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); + esmPackage.mCellName, data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); } else if (esmPackage.mType == ESM::AI_Travel) { @@ -484,7 +500,7 @@ namespace MWMechanics { ESM::AITarget data = esmPackage.mTarget; package = std::make_unique(ESM::RefId::stringRefId(data.mId.toStringView()), - data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); + esmPackage.mCellName, data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); } onPackageAdded(*package); 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 914de6da2b..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; } @@ -368,6 +387,8 @@ void MWMechanics::Alchemy::setAlchemist(const MWWorld::Ptr& npc) mTools.resize(4); + std::vector prevTools(mTools); + std::fill(mTools.begin(), mTools.end(), MWWorld::Ptr()); mEffects.clear(); @@ -384,6 +405,12 @@ void MWMechanics::Alchemy::setAlchemist(const MWWorld::Ptr& npc) if (type < 0 || type >= static_cast(mTools.size())) throw std::runtime_error("invalid apparatus type"); + if (prevTools[type] == *iter) + mTools[type] = *iter; // prefer the previous tool if still in the container + + if (!mTools[type].isEmpty() && !prevTools[type].isEmpty() && mTools[type] == prevTools[type]) + continue; + if (!mTools[type].isEmpty()) if (ref->mBase->mData.mQuality <= mTools[type].get()->mBase->mData.mQuality) continue; @@ -415,7 +442,6 @@ MWMechanics::Alchemy::TIngredientsIterator MWMechanics::Alchemy::endIngredients( void MWMechanics::Alchemy::clear() { mAlchemist = MWWorld::Ptr(); - mTools.clear(); mIngredients.clear(); mEffects.clear(); setPotionName(""); @@ -452,15 +478,33 @@ int MWMechanics::Alchemy::addIngredient(const MWWorld::Ptr& ingredient) return slot; } -void MWMechanics::Alchemy::removeIngredient(int index) +void MWMechanics::Alchemy::removeIngredient(size_t index) { - if (index >= 0 && index < static_cast(mIngredients.size())) + if (index < mIngredients.size()) { mIngredients[index] = MWWorld::Ptr(); updateEffects(); } } +void MWMechanics::Alchemy::addApparatus(const MWWorld::Ptr& apparatus) +{ + int32_t slot = apparatus.get()->mBase->mData.mType; + + mTools[slot] = apparatus; + + updateEffects(); +} + +void MWMechanics::Alchemy::removeApparatus(size_t index) +{ + if (index < mTools.size()) + { + mTools[index] = MWWorld::Ptr(); + updateEffects(); + } +} + MWMechanics::Alchemy::TEffectsIterator MWMechanics::Alchemy::beginEffects() const { return mEffects.begin(); @@ -510,6 +554,8 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::create(const std::string& nam if (readyStatus != Result_Success) return readyStatus; + MWBase::Environment::get().getWorld()->breakInvisibility(mAlchemist); + Result result = Result_RandomFailure; int brewedCount = 0; for (int i = 0; i < count; ++i) @@ -551,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 {}; @@ -568,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 ab6225e544..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); @@ -119,9 +118,15 @@ namespace MWMechanics /// \return Slot index or -1, if adding failed because of no free slot or the ingredient type being /// listed already. - void removeIngredient(int index); + void addApparatus(const MWWorld::Ptr& apparatus); + ///< Add apparatus into the appropriate slot. + + void removeIngredient(size_t index); ///< Remove ingredient from slot (calling this function on an empty slot is a no-op). + void removeApparatus(size_t index); + ///< Remove apparatus from slot. + std::string suggestPotionName(); ///< Suggest a name for the potion, based on the current effects 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 48abac8b06..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 @@ -538,7 +539,7 @@ namespace MWMechanics if (mAnimation->isPlaying("containerclose")) return false; - mAnimation->play("containeropen", Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, + mAnimation->play("containeropen", Priority_Scripted, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.f, 0); if (mAnimation->isPlaying("containeropen")) return false; @@ -559,7 +560,7 @@ namespace MWMechanics if (animPlaying) startPoint = 1.f - complete; - mAnimation->play("containerclose", Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, + mAnimation->play("containerclose", Priority_Scripted, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", startPoint, 0); } } @@ -827,8 +828,8 @@ namespace MWMechanics void CharacterController::refreshCurrentAnims( CharacterState idle, CharacterState movement, JumpingState jump, bool force) { - // If the current animation is persistent, do not touch it - if (isPersistentAnimPlaying()) + // If the current animation is scripted, do not touch it + if (isScriptedAnimPlaying()) return; refreshHitRecoilAnims(); @@ -882,7 +883,7 @@ namespace MWMechanics mDeathState = chooseRandomDeathState(); // Do not interrupt scripted animation by death - if (isPersistentAnimPlaying()) + if (isScriptedAnimPlaying()) return; playDeath(startpoint, mDeathState); @@ -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 persistent animations obviously will be buggy - if (isPersistentAnimPlaying()) - 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 { @@ -1852,23 +1884,63 @@ namespace MWMechanics void CharacterController::updateAnimQueue() { - if (mAnimQueue.size() > 1) + if (mAnimQueue.empty()) + return; + + if (!mAnimation->isPlaying(mAnimQueue.front().mGroup)) { - if (mAnimation->isPlaying(mAnimQueue.front().mGroup) == false) + // 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(); - - bool loopfallback = mAnimQueue.front().mGroup.starts_with("idle"); - mAnimation->play(mAnimQueue.front().mGroup, Priority_Default, MWRender::Animation::BlendMask_All, false, - 1.0f, "start", "stop", 0.0f, mAnimQueue.front().mLoopCount, loopfallback); + 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 (shouldPlayOrRestart) + { + // Move on to the remaining items of the queue + playAnimQueue(); + } + } + else + { + 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(); @@ -1889,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; } @@ -2291,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) { @@ -2344,7 +2415,7 @@ namespace MWMechanics } } - if (!isMovementAnimationControlled()) + if (!isMovementAnimationControlled() && !isScriptedAnimPlaying()) world->queueMovement(mPtr, vec); } @@ -2371,50 +2442,55 @@ namespace MWMechanics } } - bool isPersist = isPersistentAnimPlaying(); - osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim && !isPersist ? 0.f : duration); - if (duration > 0.0f) - moved /= duration; - else - moved = osg::Vec3f(0.f, 0.f, 0.f); + osg::Vec3f movementFromAnimation + = mAnimation->runAnimation(mSkipAnim && !isScriptedAnimPlaying() ? 0.f : duration); - moved.x() *= scale; - moved.y() *= scale; - - // Ensure we're moving in generally the right direction... - if (speed > 0.f && moved != osg::Vec3f()) + if (mPtr.getClass().isActor() && isMovementAnimationControlled() && !isScriptedAnimPlaying()) { - float l = moved.length(); - if (std::abs(movement.x() - moved.x()) > std::abs(moved.x()) / 2 - || std::abs(movement.y() - moved.y()) > std::abs(moved.y()) / 2 - || std::abs(movement.z() - moved.z()) > std::abs(moved.z()) / 2) - { - moved = movement; - // For some creatures getSpeed doesn't work, so we adjust speed to the animation. - // TODO: Fix Creature::getSpeed. - float newLength = moved.length(); - if (newLength > 0 && !cls.isNpc()) - moved *= (l / newLength); - } - } + if (duration > 0.0f) + movementFromAnimation /= duration; + else + movementFromAnimation = osg::Vec3f(0.f, 0.f, 0.f); - if (mFloatToSurface && cls.isActor()) - { - if (cls.getCreatureStats(mPtr).isDead() - || (!godmode - && cls.getCreatureStats(mPtr) - .getMagicEffects() - .getOrDefault(ESM::MagicEffect::Paralyze) - .getModifier() - > 0)) - { - moved.z() = 1.0; - } - } + movementFromAnimation.x() *= scale; + movementFromAnimation.y() *= scale; - // Update movement - if (isMovementAnimationControlled() && mPtr.getClass().isActor()) - world->queueMovement(mPtr, moved); + if (speed > 0.f && movementFromAnimation != osg::Vec3f()) + { + // Ensure we're moving in the right general direction. In vanilla, all horizontal movement is taken from + // animations, even when moving diagonally (which doesn't have a corresponding animation). So to acheive + // diagonal movement, we have to rotate the movement taken from the animation to the intended + // direction. + // + // Note that while a complete movement animation cycle will have a well defined direction, no individual + // frame will, and therefore we have to determine the direction based on the currently playing cycle + // instead. + float animMovementAngle = getAnimationMovementDirection(); + float targetMovementAngle = std::atan2(-movement.x(), movement.y()); + float diff = targetMovementAngle - animMovementAngle; + movementFromAnimation = osg::Quat(diff, osg::Vec3f(0, 0, 1)) * movementFromAnimation; + } + + if (!(isPlayer && Settings::game().mPlayerMovementIgnoresAnimation)) + movement = movementFromAnimation; + + if (mFloatToSurface) + { + if (cls.getCreatureStats(mPtr).isDead() + || (!godmode + && cls.getCreatureStats(mPtr) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::Paralyze) + .getModifier() + > 0)) + { + movement.z() = 1.0; + } + } + + // Update movement + world->queueMovement(mPtr, movement); + } mSkipAnim = false; @@ -2428,7 +2504,7 @@ namespace MWMechanics state.mScriptedAnims.clear(); for (AnimationQueue::const_iterator iter = mAnimQueue.begin(); iter != mAnimQueue.end(); ++iter) { - if (!iter->mPersist) + if (!iter->mScripted) continue; ESM::AnimationState::ScriptedAnimation anim; @@ -2436,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 { @@ -2464,46 +2541,41 @@ namespace MWMechanics AnimationQueueEntry entry; entry.mGroup = iter->mGroup; entry.mLoopCount = iter->mLoopCount; - entry.mPersist = true; + 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_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, "start", - "stop", complete, anim.mLoopCount, loopfallback); + playAnimQueue(); } } - bool CharacterController::playGroup(std::string_view groupname, int mode, int count, bool persist) + bool CharacterController::playGroup(std::string_view groupname, int mode, int count, bool scripted) { if (!mAnimation || !mAnimation->hasAnimation(groupname)) return false; - // We should not interrupt persistent animations by non-persistent ones - if (isPersistentAnimPlaying() && !persist) + // We should not interrupt scripted animations with non-scripted ones + 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"); @@ -2518,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.mPersist = persist; + 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(persist); + clearAnimQueue(scripted); - clearStateAnimation(mCurrentIdle); - - mIdleState = CharState_SpecialIdle; - bool loopfallback = entry.mGroup.starts_with("idle"); - mAnimation->play(groupname, persist && groupname != "idle" ? Priority_Persistent : 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.mPersist = false; + entry.mScripted = false; mAnimQueue.push_back(entry); + if (playImmediately) + playAnimQueue(mode == 2); + return true; } @@ -2556,13 +2635,12 @@ namespace MWMechanics mSkipAnim = true; } - bool CharacterController::isPersistentAnimPlaying() const + 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.mPersist && isAnimPlaying(first.mGroup); - } + return mAnimQueue.front().mScripted; return false; } @@ -2584,21 +2662,22 @@ namespace MWMechanics return movementAnimationControlled; } - void CharacterController::clearAnimQueue(bool clearPersistAnims) + void CharacterController::clearAnimQueue(bool clearScriptedAnims) { // Do not interrupt scripted animations, if we want to keep them - if ((!isPersistentAnimPlaying() || clearPersistAnims) && !mAnimQueue.empty()) + if ((!isScriptedAnimPlaying() || clearScriptedAnims) && !mAnimQueue.empty()) mAnimation->disable(mAnimQueue.front().mGroup); - if (clearPersistAnims) + if (clearScriptedAnims) { + mAnimation->setPlayScriptedOnly(false); mAnimQueue.clear(); return; } for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();) { - if (!it->mPersist) + if (!it->mScripted) it = mAnimQueue.erase(it); else ++it; @@ -2626,6 +2705,8 @@ namespace MWMechanics playRandomDeath(); } + updateAnimQueue(); + mAnimation->runAnimation(0.f); } @@ -2895,6 +2976,39 @@ namespace MWMechanics MWBase::Environment::get().getSoundManager()->playSound3D(mPtr, *soundId, volume, pitch); } + float CharacterController::getAnimationMovementDirection() const + { + switch (mMovementState) + { + case CharState_RunLeft: + case CharState_SneakLeft: + case CharState_SwimWalkLeft: + case CharState_SwimRunLeft: + case CharState_WalkLeft: + return osg::PI_2f; + case CharState_RunRight: + case CharState_SneakRight: + case CharState_SwimWalkRight: + case CharState_SwimRunRight: + case CharState_WalkRight: + return -osg::PI_2f; + case CharState_RunForward: + case CharState_SneakForward: + case CharState_SwimRunForward: + case CharState_SwimWalkForward: + case CharState_WalkForward: + return mAnimation->getLegsYawRadians(); + case CharState_RunBack: + case CharState_SneakBack: + case CharState_SwimWalkBack: + case CharState_SwimRunBack: + case CharState_WalkBack: + return mAnimation->getLegsYawRadians() - osg::PIf; + default: + return 0.0f; + } + } + void CharacterController::updateHeadTracking(float duration) { const osg::Node* head = mAnimation->getNode("Bip01 Head"); diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 316a1cff0e..ee26b61a25 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -40,7 +40,7 @@ namespace MWMechanics Priority_Torch, Priority_Storm, Priority_Death, - Priority_Persistent, + Priority_Scripted, Num_Priorities }; @@ -135,7 +135,9 @@ namespace MWMechanics { std::string mGroup; size_t mLoopCount; - bool mPersist; + float mTime; + bool mLooping; + bool mScripted; }; typedef std::deque AnimationQueue; AnimationQueue mAnimQueue; @@ -207,7 +209,7 @@ namespace MWMechanics void refreshMovementAnims(CharacterState movement, bool force = false); void refreshIdleAnims(CharacterState idle, bool force = false); - void clearAnimQueue(bool clearPersistAnims = false); + void clearAnimQueue(bool clearScriptedAnims = false); bool updateWeaponState(); void updateIdleStormState(bool inwater) const; @@ -215,10 +217,11 @@ namespace MWMechanics std::string chooseRandomAttackAnimation() const; static bool isRandomAttackAnimation(std::string_view group); - bool isPersistentAnimPlaying() const; + bool isScriptedAnimPlaying() const; 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(); @@ -270,7 +275,7 @@ namespace MWMechanics void persistAnimationState() const; void unpersistAnimationState(); - bool playGroup(std::string_view groupname, int mode, int count, bool persist = false); + bool playGroup(std::string_view groupname, int mode, int count, bool scripted = false); void skipAnim(); bool isAnimPlaying(std::string_view groupName) const; @@ -319,6 +324,8 @@ namespace MWMechanics void playSwishSound() const; + float getAnimationMovementDirection() const; + MWWorld::MovementDirectionFlags getSupportedMovementDirections() const; }; } 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 071ac164f3..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(); @@ -760,12 +749,12 @@ namespace MWMechanics } bool MechanicsManager::playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool persist) + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool scripted) { if (ptr.getClass().isActor()) - return mActors.playAnimationGroup(ptr, groupName, mode, number, persist); + return mActors.playAnimationGroup(ptr, groupName, mode, number, scripted); else - return mObjects.playAnimationGroup(ptr, groupName, mode, number, persist); + return mObjects.playAnimationGroup(ptr, groupName, mode, number, scripted); } void MechanicsManager::skipAnimation(const MWWorld::Ptr& ptr) { @@ -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/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 36bb18e022..997636522e 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -142,7 +142,7 @@ namespace MWMechanics /// Attempt to play an animation group /// @return Success or error bool playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool persist = false) override; + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool scripted = false) override; void skipAnimation(const MWWorld::Ptr& ptr) override; bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) override; void persistAnimationStates() override; 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/objects.cpp b/apps/openmw/mwmechanics/objects.cpp index ab981dd459..5bdfc91ac7 100644 --- a/apps/openmw/mwmechanics/objects.cpp +++ b/apps/openmw/mwmechanics/objects.cpp @@ -99,12 +99,12 @@ namespace MWMechanics } bool Objects::playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool persist) + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool scripted) { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) { - return iter->second->playGroup(groupName, mode, number, persist); + return iter->second->playGroup(groupName, mode, number, scripted); } else { diff --git a/apps/openmw/mwmechanics/objects.hpp b/apps/openmw/mwmechanics/objects.hpp index 8b5962109c..296f454e4f 100644 --- a/apps/openmw/mwmechanics/objects.hpp +++ b/apps/openmw/mwmechanics/objects.hpp @@ -46,7 +46,7 @@ namespace MWMechanics void onClose(const MWWorld::Ptr& ptr); bool playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool persist = false); + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool scripted = false); void skipAnimation(const MWWorld::Ptr& ptr); void persistAnimationStates(); diff --git a/apps/openmw/mwmechanics/recharge.cpp b/apps/openmw/mwmechanics/recharge.cpp index fc8a0e8a72..6e16436bcc 100644 --- a/apps/openmw/mwmechanics/recharge.cpp +++ b/apps/openmw/mwmechanics/recharge.cpp @@ -38,12 +38,14 @@ 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(); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); + MWBase::Environment::get().getWorld()->breakInvisibility(player); + float luckTerm = 0.1f * stats.getAttribute(ESM::Attribute::Luck).getModified(); if (luckTerm < 1 || luckTerm > 10) luckTerm = 1; @@ -85,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/repair.cpp b/apps/openmw/mwmechanics/repair.cpp index 4894b93e7f..3011004244 100644 --- a/apps/openmw/mwmechanics/repair.cpp +++ b/apps/openmw/mwmechanics/repair.cpp @@ -22,6 +22,8 @@ namespace MWMechanics MWWorld::Ptr player = getPlayer(); MWWorld::LiveCellRef* ref = mTool.get(); + MWBase::Environment::get().getWorld()->breakInvisibility(player); + // unstack tool if required player.getClass().getContainerStore(player).unstack(mTool); 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 2e28aaa1f3..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()) @@ -932,6 +927,9 @@ namespace MWMechanics if (target.getCellRef().getLockLevel() < magnitude) // If the door is not already locked to a higher value, lock it to spell magnitude { + MWBase::Environment::get().getSoundManager()->playSound3D( + target, ESM::RefId::stringRefId("Open Lock"), 1.f, 1.f); + if (caster == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}"); target.getCellRef().lock(magnitude); 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/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 b49f382f66..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,24 +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_Persistent)) - && 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_Persistent))) + if (mPlayScriptedOnly && !state.mPriority.contains(MWMechanics::Priority_Scripted)) { ++stateiter; continue; @@ -1236,9 +1221,11 @@ namespace MWRender mRootController->setEnabled(enable); if (enable) { - mRootController->setRotate(osg::Quat(mLegsYawRadians, osg::Vec3f(0, 0, 1)) - * osg::Quat(mBodyPitchRadians, osg::Vec3f(1, 0, 0))); + osg::Quat legYaw = osg::Quat(mLegsYawRadians, osg::Vec3f(0, 0, 1)); + mRootController->setRotate(legYaw * osg::Quat(mBodyPitchRadians, osg::Vec3f(1, 0, 0))); yawOffset = mLegsYawRadians; + // When yawing the root, also update the accumulated movement. + movement = legYaw * movement; } } if (mSpineController) @@ -1262,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; } @@ -1592,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/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index fbf5bf112a..88ceeabd23 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -154,7 +154,7 @@ namespace MWRender public: CharacterPreviewRTTNode(uint32_t sizeX, uint32_t sizeY) - : RTTNode(sizeX, sizeY, Settings::Manager::getInt("antialiasing", "Video"), false, 0, + : RTTNode(sizeX, sizeY, Settings::video().mAntialiasing, false, 0, StereoAwareness::Unaware_MultiViewShaders, shouldAddMSAAIntermediateTarget()) , mAspectRatio(static_cast(sizeX) / static_cast(sizeY)) { @@ -247,7 +247,7 @@ namespace MWRender defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); stateset->setAttribute(defaultMat); - SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); + SceneUtil::ShadowManager::disableShadowsForStateSet(Settings::shadows(), *stateset); // assign large value to effectively turn off fog // shaders don't respect glDisable(GL_FOG) 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 1f06e68bc2..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); @@ -124,15 +124,17 @@ namespace MWRender auto findArmorAddons = [&](const ESM4::Armor* armor) { for (ESM::FormId armaId : armor->mAddOns) { + if (armaId.isZeroOrUnset()) + continue; const ESM4::ArmorAddon* arma = store->get().search(armaId); if (!arma) { 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); @@ -141,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()) { @@ -181,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/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/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 3d28bf477d..892a8b5428 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -763,7 +763,7 @@ namespace MWRender lightSource->setStateSetModes(*stateset, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); - SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); + SceneUtil::ShadowManager::disableShadowsForStateSet(Settings::shadows(), *stateset); // override sun for local map SceneUtil::configureStateSetSunOverride(static_cast(mSceneRoot), light, stateset); diff --git a/apps/openmw/mwrender/luminancecalculator.cpp b/apps/openmw/mwrender/luminancecalculator.cpp index 5b7fe272aa..ae29b7fdcc 100644 --- a/apps/openmw/mwrender/luminancecalculator.cpp +++ b/apps/openmw/mwrender/luminancecalculator.cpp @@ -20,11 +20,6 @@ namespace MWRender mResolveProgram = shaderManager.getProgram(vertex, std::move(resolveFragment)); mLuminanceProgram = shaderManager.getProgram(vertex, std::move(luminanceFragment)); - } - - void LuminanceCalculator::compile() - { - int mipmapLevels = osg::Image::computeNumberOfMipmapLevels(mWidth, mHeight); for (auto& buffer : mBuffers) { @@ -38,7 +33,6 @@ namespace MWRender osg::Texture2D::MIN_FILTER, osg::Texture2D::LINEAR_MIPMAP_NEAREST); buffer.mipmappedSceneLuminanceTex->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::LINEAR); buffer.mipmappedSceneLuminanceTex->setTextureSize(mWidth, mHeight); - buffer.mipmappedSceneLuminanceTex->setNumMipmapLevels(mipmapLevels); buffer.luminanceTex = new osg::Texture2D; buffer.luminanceTex->setInternalFormat(GL_R16F); @@ -62,14 +56,6 @@ namespace MWRender buffer.luminanceProxyFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, osg::FrameBufferAttachment(buffer.luminanceProxyTex)); - buffer.resolveSceneLumFbo = new osg::FrameBufferObject; - buffer.resolveSceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, - osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex, mipmapLevels - 1)); - - buffer.sceneLumFbo = new osg::FrameBufferObject; - buffer.sceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, - osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex)); - buffer.sceneLumSS = new osg::StateSet; buffer.sceneLumSS->setAttributeAndModes(mLuminanceProgram); buffer.sceneLumSS->addUniform(new osg::Uniform("sceneTex", 0)); @@ -84,6 +70,26 @@ namespace MWRender mBuffers[0].resolveSS->setTextureAttributeAndModes(1, mBuffers[1].luminanceTex); mBuffers[1].resolveSS->setTextureAttributeAndModes(1, mBuffers[0].luminanceTex); + } + + void LuminanceCalculator::compile() + { + int mipmapLevels = osg::Image::computeNumberOfMipmapLevels(mWidth, mHeight); + + for (auto& buffer : mBuffers) + { + buffer.mipmappedSceneLuminanceTex->setTextureSize(mWidth, mHeight); + buffer.mipmappedSceneLuminanceTex->setNumMipmapLevels(mipmapLevels); + buffer.mipmappedSceneLuminanceTex->dirtyTextureObject(); + + buffer.resolveSceneLumFbo = new osg::FrameBufferObject; + buffer.resolveSceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex, mipmapLevels - 1)); + + buffer.sceneLumFbo = new osg::FrameBufferObject; + buffer.sceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex)); + } mCompiled = true; } @@ -114,13 +120,14 @@ namespace MWRender buffer.luminanceProxyFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); ext->glBlitFramebuffer(0, 0, 1, 1, 0, 0, 1, 1, GL_COLOR_BUFFER_BIT, GL_NEAREST); - if (dirty) + if (mIsBlank) { // Use current frame data for previous frame to warm up calculations and prevent popin mBuffers[(frameId + 1) % 2].resolveFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); ext->glBlitFramebuffer(0, 0, 1, 1, 0, 0, 1, 1, GL_COLOR_BUFFER_BIT, GL_NEAREST); buffer.luminanceProxyFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + mIsBlank = false; } buffer.resolveFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); diff --git a/apps/openmw/mwrender/luminancecalculator.hpp b/apps/openmw/mwrender/luminancecalculator.hpp index 71ea2f7971..8b51081e2f 100644 --- a/apps/openmw/mwrender/luminancecalculator.hpp +++ b/apps/openmw/mwrender/luminancecalculator.hpp @@ -58,6 +58,7 @@ namespace MWRender bool mCompiled = false; bool mEnabled = false; + bool mIsBlank = true; int mWidth = 1; int mHeight = 1; diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index cb7ef3626f..1559ebdd5d 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); } } @@ -328,46 +328,34 @@ namespace MWRender { osg::State* state = renderInfo.getState(); - PostProcessor* postProcessor = dynamic_cast(renderInfo.getCurrentCamera()->getUserData()); + PostProcessor* postProcessor = static_cast(renderInfo.getCurrentCamera()->getUserData()); state->applyAttribute(mDepth); unsigned int frameId = state->getFrameStamp()->getFrameNumber() % 2; - if (postProcessor && postProcessor->getFbo(PostProcessor::FBO_FirstPerson, frameId)) + postProcessor->getFbo(PostProcessor::FBO_FirstPerson, frameId)->apply(*state); + if (mPassNormals) { - postProcessor->getFbo(PostProcessor::FBO_FirstPerson, frameId)->apply(*state); - if (mPassNormals) - { - state->get()->glColorMaski(1, true, true, true, true); - state->haveAppliedAttribute(osg::StateAttribute::COLORMASK); - } - glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - // color accumulation pass - bin->drawImplementation(renderInfo, previous); - - auto primaryFBO = postProcessor->getPrimaryFbo(frameId); - - if (postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)) - postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)->apply(*state); - else - primaryFBO->apply(*state); - - // depth accumulation pass - osg::ref_ptr restore = bin->getStateSet(); - bin->setStateSet(mStateSet); - bin->drawImplementation(renderInfo, previous); - bin->setStateSet(restore); - - if (postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)) - primaryFBO->apply(*state); - } - else - { - // fallback to standard depth clear when we are not rendering our main scene via an intermediate FBO - glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - bin->drawImplementation(renderInfo, previous); + state->get()->glColorMaski(1, true, true, true, true); + state->haveAppliedAttribute(osg::StateAttribute::COLORMASK); } + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + // color accumulation pass + bin->drawImplementation(renderInfo, previous); + + auto primaryFBO = postProcessor->getPrimaryFbo(frameId); + primaryFBO->apply(*state); + + postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)->apply(*state); + + // depth accumulation pass + osg::ref_ptr restore = bin->getStateSet(); + bin->setStateSet(mStateSet); + bin->drawImplementation(renderInfo, previous); + bin->setStateSet(restore); + + primaryFBO->apply(*state); state->checkGLErrors("after DepthClearCallback::drawImplementation"); } @@ -479,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 << "'"; } @@ -488,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; @@ -504,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); @@ -516,24 +507,26 @@ 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) addAnimSource(defaultSkeleton, smodel); - addAnimSource(smodel, smodel); + if (!isBase) + 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)); @@ -658,9 +651,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)); } @@ -678,13 +670,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)); } } @@ -860,7 +848,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); } @@ -913,11 +901,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); } @@ -932,13 +917,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 6a56f7e5f7..9c8b08adfd 100644 --- a/apps/openmw/mwrender/pingpongcanvas.cpp +++ b/apps/openmw/mwrender/pingpongcanvas.cpp @@ -10,9 +10,11 @@ namespace MWRender { - PingPongCanvas::PingPongCanvas(Shader::ShaderManager& shaderManager) + PingPongCanvas::PingPongCanvas( + Shader::ShaderManager& shaderManager, const std::shared_ptr& luminanceCalculator) : mFallbackStateSet(new osg::StateSet) , mMultiviewResolveStateSet(new osg::StateSet) + , mLuminanceCalculator(luminanceCalculator) { setUseDisplayList(false); setUseVertexBufferObjects(true); @@ -26,8 +28,7 @@ namespace MWRender addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES, 0, 3)); - mLuminanceCalculator = LuminanceCalculator(shaderManager); - mLuminanceCalculator.disable(); + mLuminanceCalculator->disable(); Shader::ShaderManager::DefineMap defines; Stereo::shaderStereoDefines(defines); @@ -43,19 +44,16 @@ namespace MWRender mMultiviewResolveStateSet->addUniform(new osg::Uniform("lastShader", 0)); } - void PingPongCanvas::setCurrentFrameData(size_t frameId, fx::DispatchArray&& data) + void PingPongCanvas::setPasses(fx::DispatchArray&& passes) { - mBufferData[frameId].data = std::move(data); + mPasses = std::move(passes); } - void PingPongCanvas::setMask(size_t frameId, bool underwater, bool exterior) + void PingPongCanvas::setMask(bool underwater, bool exterior) { - mBufferData[frameId].mask = 0; - - mBufferData[frameId].mask - |= underwater ? fx::Technique::Flag_Disable_Underwater : fx::Technique::Flag_Disable_Abovewater; - mBufferData[frameId].mask - |= exterior ? fx::Technique::Flag_Disable_Exteriors : fx::Technique::Flag_Disable_Interiors; + mMask = 0; + mMask |= underwater ? fx::Technique::Flag_Disable_Underwater : fx::Technique::Flag_Disable_Abovewater; + mMask |= exterior ? fx::Technique::Flag_Disable_Exteriors : fx::Technique::Flag_Disable_Interiors; } void PingPongCanvas::drawGeometry(osg::RenderInfo& renderInfo) const @@ -77,19 +75,15 @@ namespace MWRender size_t frameId = state.getFrameStamp()->getFrameNumber() % 2; - auto& bufferData = mBufferData[frameId]; - - const auto& data = bufferData.data; - std::vector filtered; - filtered.reserve(data.size()); + filtered.reserve(mPasses.size()); - for (size_t i = 0; i < data.size(); ++i) + for (size_t i = 0; i < mPasses.size(); ++i) { - const auto& node = data[i]; + const auto& node = mPasses[i]; - if (bufferData.mask & node.mFlags) + if (mMask & node.mFlags) continue; filtered.push_back(i); @@ -97,7 +91,7 @@ namespace MWRender auto* resolveViewport = state.getCurrentViewport(); - if (filtered.empty() || !bufferData.postprocessing) + if (filtered.empty() || !mPostprocessing) { state.pushStateSet(mFallbackStateSet); state.apply(); @@ -108,7 +102,7 @@ namespace MWRender state.apply(); } - state.applyTextureAttribute(0, bufferData.sceneTex); + state.applyTextureAttribute(0, mTextureScene); resolveViewport->apply(state); drawGeometry(renderInfo); @@ -124,13 +118,12 @@ namespace MWRender const unsigned int handle = mFbos[0] ? mFbos[0]->getHandle(state.getContextID()) : 0; - if (handle == 0 || bufferData.dirty) + if (handle == 0 || mDirty) { for (auto& fbo : mFbos) { fbo = new osg::FrameBufferObject; - attachCloneOfTemplate( - fbo, osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, bufferData.sceneTexLDR); + attachCloneOfTemplate(fbo, osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, mTextureScene); fbo->apply(state); glClearColor(0.5, 0.5, 0.5, 1); glClear(GL_COLOR_BUFFER_BIT); @@ -140,7 +133,7 @@ namespace MWRender { mMultiviewResolveFramebuffer = new osg::FrameBufferObject(); attachCloneOfTemplate(mMultiviewResolveFramebuffer, - osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, bufferData.sceneTexLDR); + osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, mTextureScene); mMultiviewResolveFramebuffer->apply(state); glClearColor(0.5, 0.5, 0.5, 1); glClear(GL_COLOR_BUFFER_BIT); @@ -150,15 +143,15 @@ namespace MWRender .getTexture()); } - mLuminanceCalculator.dirty(bufferData.sceneTex->getTextureWidth(), bufferData.sceneTex->getTextureHeight()); + mLuminanceCalculator->dirty(mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight()); if (Stereo::getStereo()) - mRenderViewport = new osg::Viewport( - 0, 0, bufferData.sceneTex->getTextureWidth(), bufferData.sceneTex->getTextureHeight()); + mRenderViewport + = new osg::Viewport(0, 0, mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight()); else mRenderViewport = nullptr; - bufferData.dirty = false; + mDirty = false; } constexpr std::array, 3> buffers @@ -166,11 +159,11 @@ namespace MWRender { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT2_EXT }, { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT } } }; - (bufferData.hdr) ? mLuminanceCalculator.enable() : mLuminanceCalculator.disable(); + (mAvgLum) ? mLuminanceCalculator->enable() : mLuminanceCalculator->disable(); // A histogram based approach is superior way to calculate scene luminance. Using mipmaps is more broadly // supported, so that's what we use for now. - mLuminanceCalculator.draw(*this, renderInfo, state, ext, frameId); + mLuminanceCalculator->draw(*this, renderInfo, state, ext, frameId); auto buffer = buffers[0]; @@ -181,8 +174,7 @@ namespace MWRender const unsigned int cid = state.getContextID(); - const osg::ref_ptr& destinationFbo - = bufferData.destination ? bufferData.destination : nullptr; + const osg::ref_ptr& destinationFbo = mDestinationFBO ? mDestinationFBO : nullptr; unsigned int destinationHandle = destinationFbo ? destinationFbo->getHandle(cid) : 0; auto bindDestinationFbo = [&]() { @@ -204,19 +196,55 @@ namespace MWRender } }; + // When textures are created (or resized) we need to either dirty them and/or clear them. + // Otherwise, there will be undefined behavior when reading from a texture that has yet to be written to in a + // later pass. + for (const auto& attachment : mDirtyAttachments) + { + const auto [w, h] + = attachment.mSize.get(mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight()); + + attachment.mTarget->setTextureSize(w, h); + if (attachment.mMipMap) + attachment.mTarget->setNumMipmapLevels(osg::Image::computeNumberOfMipmapLevels(w, h)); + attachment.mTarget->dirtyTextureObject(); + + osg::ref_ptr fbo = new osg::FrameBufferObject; + + fbo->setAttachment( + osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, osg::FrameBufferAttachment(attachment.mTarget)); + fbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + + glViewport(0, 0, attachment.mTarget->getTextureWidth(), attachment.mTarget->getTextureHeight()); + state.haveAppliedAttribute(osg::StateAttribute::VIEWPORT); + glClearColor(attachment.mClearColor.r(), attachment.mClearColor.g(), attachment.mClearColor.b(), + attachment.mClearColor.a()); + glClear(GL_COLOR_BUFFER_BIT); + + if (attachment.mTarget->getNumMipmapLevels() > 0) + { + state.setActiveTextureUnit(0); + state.applyTextureAttribute(0, attachment.mTarget); + ext->glGenerateMipmap(GL_TEXTURE_2D); + } + } + for (const size_t& index : filtered) { - const auto& node = data[index]; + const auto& node = mPasses[index]; - node.mRootStateSet->setTextureAttribute(PostProcessor::Unit_Depth, bufferData.depthTex); + node.mRootStateSet->setTextureAttribute(PostProcessor::Unit_Depth, mTextureDepth); - if (bufferData.hdr) + if (mAvgLum) + node.mRootStateSet->setTextureAttribute(PostProcessor::TextureUnits::Unit_EyeAdaptation, + mLuminanceCalculator->getLuminanceTexture(frameId)); + + if (mTextureNormals) + node.mRootStateSet->setTextureAttribute(PostProcessor::TextureUnits::Unit_Normals, mTextureNormals); + + if (mTextureDistortion) node.mRootStateSet->setTextureAttribute( - PostProcessor::TextureUnits::Unit_EyeAdaptation, mLuminanceCalculator.getLuminanceTexture(frameId)); - - if (bufferData.normalsTex) - node.mRootStateSet->setTextureAttribute( - PostProcessor::TextureUnits::Unit_Normals, bufferData.normalsTex); + PostProcessor::TextureUnits::Unit_Distortion, mTextureDistortion); state.pushStateSet(node.mRootStateSet); state.apply(); @@ -231,7 +259,7 @@ namespace MWRender // VR-TODO: This won't actually work for tex2darrays if (lastShader == 0) - pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastShader, bufferData.sceneTex); + pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastShader, mTextureScene); else pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastShader, (osg::Texture*)mFbos[lastShader - GL_COLOR_ATTACHMENT0_EXT] @@ -239,7 +267,7 @@ namespace MWRender .getTexture()); if (lastDraw == 0) - pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastPass, bufferData.sceneTex); + pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastPass, mTextureScene); else pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastPass, (osg::Texture*)mFbos[lastDraw - GL_COLOR_ATTACHMENT0_EXT] @@ -248,6 +276,23 @@ namespace MWRender if (pass.mRenderTarget) { + if (mDirtyAttachments.size() > 0) + { + const auto [w, h] + = pass.mSize.get(mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight()); + + // Custom render targets must be shared between frame ids, so it's impossible to double buffer + // without expensive copies. That means the only thread-safe place to resize is in the draw + // thread. + osg::Texture2D* texture = const_cast(dynamic_cast( + pass.mRenderTarget->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0) + .getTexture())); + + texture->setTextureSize(w, h); + texture->setNumMipmapLevels(pass.mRenderTexture->getNumMipmapLevels()); + texture->dirtyTextureObject(); + } + pass.mRenderTarget->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); if (pass.mRenderTexture->getNumMipmapLevels() > 0) @@ -260,7 +305,6 @@ namespace MWRender } lastApplied = pass.mRenderTarget->getHandle(state.getContextID()); - ; } else if (pass.mResolve && index == filtered.back()) { @@ -322,5 +366,7 @@ namespace MWRender { bindDestinationFbo(); } + + mDirtyAttachments.clear(); } } diff --git a/apps/openmw/mwrender/pingpongcanvas.hpp b/apps/openmw/mwrender/pingpongcanvas.hpp index a5557a6d6e..5a37b7fbc9 100644 --- a/apps/openmw/mwrender/pingpongcanvas.hpp +++ b/apps/openmw/mwrender/pingpongcanvas.hpp @@ -22,78 +22,64 @@ namespace MWRender class PingPongCanvas : public osg::Geometry { public: - PingPongCanvas(Shader::ShaderManager& shaderManager); - - void drawImplementation(osg::RenderInfo& renderInfo) const override; - - void dirty(size_t frameId) { mBufferData[frameId].dirty = true; } - - const fx::DispatchArray& getCurrentFrameData(size_t frame) { return mBufferData[frame % 2].data; } - - // Sets current frame pass data and stores copy of dispatch array to apply to next frame data - void setCurrentFrameData(size_t frameId, fx::DispatchArray&& data); - - void setMask(size_t frameId, bool underwater, bool exterior); - - void setSceneTexture(size_t frameId, osg::ref_ptr tex) { mBufferData[frameId].sceneTex = tex; } - - void setLDRSceneTexture(size_t frameId, osg::ref_ptr tex) - { - mBufferData[frameId].sceneTexLDR = tex; - } - - void setDepthTexture(size_t frameId, osg::ref_ptr tex) { mBufferData[frameId].depthTex = tex; } - - void setNormalsTexture(size_t frameId, osg::ref_ptr tex) - { - mBufferData[frameId].normalsTex = tex; - } - - void setHDR(size_t frameId, bool hdr) { mBufferData[frameId].hdr = hdr; } - - void setPostProcessing(size_t frameId, bool postprocessing) - { - mBufferData[frameId].postprocessing = postprocessing; - } - - const osg::ref_ptr& getSceneTexture(size_t frameId) const - { - return mBufferData[frameId].sceneTex; - } + PingPongCanvas( + Shader::ShaderManager& shaderManager, const std::shared_ptr& luminanceCalculator); void drawGeometry(osg::RenderInfo& renderInfo) const; - private: - void copyNewFrameData(size_t frameId) const; + void drawImplementation(osg::RenderInfo& renderInfo) const override; - mutable LuminanceCalculator mLuminanceCalculator; + void dirty() { mDirty = true; } + + void setDirtyAttachments(const std::vector& attachments) + { + mDirtyAttachments = attachments; + } + + const fx::DispatchArray& getPasses() { return mPasses; } + + void setPasses(fx::DispatchArray&& passes); + + void setMask(bool underwater, bool exterior); + + void setTextureScene(osg::ref_ptr tex) { mTextureScene = tex; } + + void setTextureDepth(osg::ref_ptr tex) { mTextureDepth = tex; } + + 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; } + + const osg::ref_ptr& getSceneTexture(size_t frameId) const { return mTextureScene; } + + private: + bool mAvgLum = false; + bool mPostprocessing = false; + + fx::DispatchArray mPasses; + fx::FlagsType mMask = 0; osg::ref_ptr mFallbackProgram; osg::ref_ptr mMultiviewResolveProgram; osg::ref_ptr mFallbackStateSet; osg::ref_ptr mMultiviewResolveStateSet; - mutable osg::ref_ptr mMultiviewResolveFramebuffer; - struct BufferData - { - bool dirty = false; - bool hdr = false; - bool postprocessing = true; + osg::ref_ptr mTextureScene; + osg::ref_ptr mTextureDepth; + osg::ref_ptr mTextureNormals; + osg::ref_ptr mTextureDistortion; - fx::DispatchArray data; - fx::FlagsType mask; - - osg::ref_ptr destination; - - osg::ref_ptr sceneTex; - osg::ref_ptr depthTex; - osg::ref_ptr sceneTexLDR; - osg::ref_ptr normalsTex; - }; - - mutable std::array mBufferData; - mutable std::array, 3> mFbos; + mutable bool mDirty = false; + mutable std::vector mDirtyAttachments; mutable osg::ref_ptr mRenderViewport; + mutable osg::ref_ptr mMultiviewResolveFramebuffer; + mutable osg::ref_ptr mDestinationFBO; + mutable std::array, 3> mFbos; + mutable std::shared_ptr mLuminanceCalculator; }; } diff --git a/apps/openmw/mwrender/pingpongcull.cpp b/apps/openmw/mwrender/pingpongcull.cpp index 8dfff5a60c..497c6c734a 100644 --- a/apps/openmw/mwrender/pingpongcull.cpp +++ b/apps/openmw/mwrender/pingpongcull.cpp @@ -21,7 +21,7 @@ namespace MWRender if (Stereo::getStereo()) { mViewportStateset = new osg::StateSet(); - mViewport = new osg::Viewport(0, 0, pp->renderWidth(), pp->renderHeight()); + mViewport = new osg::Viewport; mViewportStateset->setAttribute(mViewport); } } @@ -37,41 +37,31 @@ namespace MWRender size_t frame = cv->getTraversalNumber(); size_t frameId = frame % 2; - MWRender::PostProcessor* postProcessor - = dynamic_cast(cv->getCurrentCamera()->getUserData()); - if (!postProcessor) - throw std::runtime_error("PingPongCull: failed to get a PostProcessor!"); - if (Stereo::getStereo()) { auto& sm = Stereo::Manager::instance(); auto view = sm.getEye(cv); int index = view == Stereo::Eye::Right ? 1 : 0; auto projectionMatrix = sm.computeEyeProjection(index, true); - postProcessor->getStateUpdater()->setProjectionMatrix(projectionMatrix); + mPostProcessor->getStateUpdater()->setProjectionMatrix(projectionMatrix); } - postProcessor->getStateUpdater()->setViewMatrix(cv->getCurrentCamera()->getViewMatrix()); - postProcessor->getStateUpdater()->setPrevViewMatrix(mLastViewMatrix[0]); + mPostProcessor->getStateUpdater()->setViewMatrix(cv->getCurrentCamera()->getViewMatrix()); + mPostProcessor->getStateUpdater()->setPrevViewMatrix(mLastViewMatrix[0]); mLastViewMatrix[0] = cv->getCurrentCamera()->getViewMatrix(); - postProcessor->getStateUpdater()->setEyePos(cv->getEyePoint()); - postProcessor->getStateUpdater()->setEyeVec(cv->getLookVectorLocal()); + mPostProcessor->getStateUpdater()->setEyePos(cv->getEyePoint()); + mPostProcessor->getStateUpdater()->setEyeVec(cv->getLookVectorLocal()); - if (!postProcessor->getFbo(PostProcessor::FBO_Primary, frameId)) + if (!mPostProcessor->getFbo(PostProcessor::FBO_Multisample, frameId)) { - renderStage->setMultisampleResolveFramebufferObject(nullptr); - renderStage->setFrameBufferObject(nullptr); - } - else if (!postProcessor->getFbo(PostProcessor::FBO_Multisample, frameId)) - { - renderStage->setFrameBufferObject(postProcessor->getFbo(PostProcessor::FBO_Primary, frameId)); + renderStage->setFrameBufferObject(mPostProcessor->getFbo(PostProcessor::FBO_Primary, frameId)); } else { renderStage->setMultisampleResolveFramebufferObject( - postProcessor->getFbo(PostProcessor::FBO_Primary, frameId)); - renderStage->setFrameBufferObject(postProcessor->getFbo(PostProcessor::FBO_Multisample, frameId)); + mPostProcessor->getFbo(PostProcessor::FBO_Primary, frameId)); + renderStage->setFrameBufferObject(mPostProcessor->getFbo(PostProcessor::FBO_Multisample, frameId)); // The MultiView patch has a bug where it does not update resolve layers if the resolve framebuffer is // changed. So we do blit manually in this case diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp index 9162657e30..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 @@ -110,30 +114,77 @@ namespace MWRender PostProcessor::PostProcessor( RenderingManager& rendering, osgViewer::Viewer* viewer, osg::Group* rootNode, const VFS::Manager* vfs) : osg::Group() - , mEnableLiveReload(false) , mRootNode(rootNode) - , mSamples(Settings::Manager::getInt("antialiasing", "Video")) - , mDirty(false) - , mDirtyFrameId(0) + , mHUDCamera(new osg::Camera) , mRendering(rendering) , mViewer(viewer) , mVFS(vfs) - , mTriggerShaderReload(false) - , mReload(false) - , mEnabled(false) , mUsePostProcessing(Settings::postProcessing().mEnabled) - , mDisableDepthPasses(false) - , mLastFrameNumber(0) - , mLastSimulationTime(0.f) - , mExteriorFlag(false) - , mUnderwater(false) - , mHDR(false) - , mNormals(false) - , mPrevNormals(false) - , mNormalsSupported(false) - , mPassLights(false) - , mPrevPassLights(false) + , mSamples(Settings::video().mAntialiasing) + , mPingPongCull(new PingPongCull(this)) + , mDistortionCallback(new DistortionCallback) { + auto& shaderManager = mRendering.getResourceSystem()->getSceneManager()->getShaderManager(); + + std::shared_ptr luminanceCalculator = std::make_shared(shaderManager); + + for (auto& canvas : mCanvases) + canvas = new PingPongCanvas(shaderManager, luminanceCalculator); + + mHUDCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); + mHUDCamera->setRenderOrder(osg::Camera::POST_RENDER); + mHUDCamera->setClearColor(osg::Vec4(0.45, 0.45, 0.14, 1.0)); + mHUDCamera->setClearMask(0); + mHUDCamera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1, 0, 1)); + mHUDCamera->setAllowEventFocus(false); + mHUDCamera->setViewport(0, 0, mWidth, mHeight); + mHUDCamera->setNodeMask(Mask_RenderToTexture); + mHUDCamera->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + mHUDCamera->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + mHUDCamera->addChild(mCanvases[0]); + mHUDCamera->addChild(mCanvases[1]); + mHUDCamera->setCullCallback(new HUDCullCallback); + mViewer->getCamera()->addCullCallback(mPingPongCull); + + // 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(); @@ -152,28 +203,22 @@ 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); - if (!Stereo::getStereo() && !SceneUtil::AutoDepth::isReversed() && !Settings::shaders().mSoftParticles - && !mUsePostProcessing) - return; + addChild(mHUDCamera); + addChild(mRootNode); - enable(mUsePostProcessing); + mViewer->setSceneData(this); + mViewer->getCamera()->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); + mViewer->getCamera()->getGraphicsContext()->setResizedCallback(new ResizedCallback(this)); + mViewer->getCamera()->setUserData(this); + + setCullCallback(mStateUpdater); + + if (mUsePostProcessing) + enable(); } PostProcessor::~PostProcessor() @@ -189,28 +234,14 @@ namespace MWRender if (Stereo::getStereo()) Stereo::Manager::instance().screenResolutionChanged(); - auto width = renderWidth(); - auto height = renderHeight(); - for (auto& technique : mTechniques) - { - for (auto& [name, rt] : technique->getRenderTargetsMap()) - { - const auto [w, h] = rt.mSize.get(width, height); - rt.mTarget->setTextureSize(w, h); - } - } - size_t frameId = frame() % 2; - createTexturesAndCamera(frameId); createObjectsForFrame(frameId); mRendering.updateProjectionMatrix(); - mRendering.setScreenRes(width, height); + mRendering.setScreenRes(renderWidth(), renderHeight()); - dirtyTechniques(); - - mPingPongCanvas->dirty(frameId); + dirtyTechniques(true); mDirty = true; mDirtyFrameId = !frameId; @@ -230,77 +261,20 @@ namespace MWRender } } - void PostProcessor::enable(bool usePostProcessing) + void PostProcessor::enable() { mReload = true; - mEnabled = true; - const bool postPass = Settings::postProcessing().mTransparentPostpass; - mUsePostProcessing = usePostProcessing; - - mDisableDepthPasses = !Settings::shaders().mSoftParticles && !postPass; - -#ifdef ANDROID - mDisableDepthPasses = true; -#endif - - if (!mDisableDepthPasses) - { - mTransparentDepthPostPass = new TransparentDepthBinCallback( - mRendering.getResourceSystem()->getSceneManager()->getShaderManager(), postPass); - osgUtil::RenderBin::getRenderBinPrototype("DepthSortedBin")->setDrawCallback(mTransparentDepthPostPass); - } - - if (mUsePostProcessing && mTechniqueFileMap.empty()) - { - populateTechniqueFiles(); - } - - createTexturesAndCamera(frame() % 2); - - removeChild(mHUDCamera); - removeChild(mRootNode); - - addChild(mHUDCamera); - addChild(mRootNode); - - mViewer->setSceneData(this); - mViewer->getCamera()->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); - mViewer->getCamera()->getGraphicsContext()->setResizedCallback(new ResizedCallback(this)); - mViewer->getCamera()->setUserData(this); - - setCullCallback(mStateUpdater); - mHUDCamera->setCullCallback(new HUDCullCallback); + mUsePostProcessing = true; } void PostProcessor::disable() { - if (!Settings::shaders().mSoftParticles) - osgUtil::RenderBin::getRenderBinPrototype("DepthSortedBin")->setDrawCallback(nullptr); - - if (!SceneUtil::AutoDepth::isReversed() && !Settings::shaders().mSoftParticles) - { - removeChild(mHUDCamera); - setCullCallback(nullptr); - - mViewer->getCamera()->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER); - mViewer->getCamera()->getGraphicsContext()->setResizedCallback(nullptr); - mViewer->getCamera()->setUserData(nullptr); - - mEnabled = false; - } - mUsePostProcessing = false; mRendering.getSkyManager()->setSunglare(true); } void PostProcessor::traverse(osg::NodeVisitor& nv) { - if (!mEnabled) - { - osg::Group::traverse(nv); - return; - } - size_t frameId = nv.getTraversalNumber() % 2; if (nv.getVisitorType() == osg::NodeVisitor::CULL_VISITOR) @@ -313,33 +287,28 @@ namespace MWRender void PostProcessor::cull(size_t frameId, osgUtil::CullVisitor* cv) { - const auto& fbo = getFbo(FBO_Intercept, frameId); - if (fbo) + if (const auto& fbo = getFbo(FBO_Intercept, frameId)) { osgUtil::RenderStage* rs = cv->getRenderStage(); if (rs && rs->getMultisampleResolveFramebufferObject()) rs->setMultisampleResolveFramebufferObject(fbo); } - mPingPongCanvas->setPostProcessing(frameId, mUsePostProcessing); - mPingPongCanvas->setNormalsTexture(frameId, mNormals ? getTexture(Tex_Normal, frameId) : nullptr); - mPingPongCanvas->setMask(frameId, mUnderwater, mExteriorFlag); - mPingPongCanvas->setHDR(frameId, getHDR()); + mCanvases[frameId]->setPostProcessing(mUsePostProcessing); + mCanvases[frameId]->setTextureNormals(mNormals ? getTexture(Tex_Normal, frameId) : nullptr); + mCanvases[frameId]->setMask(mUnderwater, mExteriorFlag); + mCanvases[frameId]->setCalculateAvgLum(mHDR); - mPingPongCanvas->setSceneTexture(frameId, getTexture(Tex_Scene, frameId)); - if (mDisableDepthPasses) - mPingPongCanvas->setDepthTexture(frameId, getTexture(Tex_Depth, frameId)); - else - mPingPongCanvas->setDepthTexture(frameId, getTexture(Tex_OpaqueDepth, frameId)); + mCanvases[frameId]->setTextureScene(getTexture(Tex_Scene, frameId)); + mCanvases[frameId]->setTextureDepth(getTexture(Tex_OpaqueDepth, frameId)); + mCanvases[frameId]->setTextureDistortion(getTexture(Tex_Distortion, frameId)); - mPingPongCanvas->setLDRSceneTexture(frameId, getTexture(Tex_Scene_LDR, frameId)); + mTransparentDepthPostPass->mFbo[frameId] = mFbos[frameId][FBO_Primary]; + mTransparentDepthPostPass->mMsaaFbo[frameId] = mFbos[frameId][FBO_Multisample]; + mTransparentDepthPostPass->mOpaqueFbo[frameId] = mFbos[frameId][FBO_OpaqueDepth]; - if (mTransparentDepthPostPass) - { - 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(); @@ -355,7 +324,7 @@ namespace MWRender mStateUpdater->setDeltaSimulationTime(static_cast(stamp->getSimulationTime() - mLastSimulationTime)); mLastSimulationTime = stamp->getSimulationTime(); - for (const auto& dispatchNode : mPingPongCanvas->getCurrentFrameData(frame)) + for (const auto& dispatchNode : mCanvases[frameId]->getPasses()) { for (auto& uniform : dispatchNode.mHandle->getUniformMap()) { @@ -421,13 +390,15 @@ namespace MWRender reloadIfRequired(); + mCanvases[frameId]->setNodeMask(~0u); + mCanvases[!frameId]->setNodeMask(0); + if (mDirty && mDirtyFrameId == frameId) { - createTexturesAndCamera(frameId); createObjectsForFrame(frameId); - mDirty = false; - mPingPongCanvas->setCurrentFrameData(frameId, fx::DispatchArray(mTemplateData)); + mDirty = false; + mCanvases[frameId]->setPasses(fx::DispatchArray(mTemplateData)); } if ((mNormalsSupported && mNormals != mPrevNormals) || (mPassLights != mPrevPassLights)) @@ -448,7 +419,6 @@ namespace MWRender mViewer->startThreading(); - createTexturesAndCamera(frameId); createObjectsForFrame(frameId); mDirty = true; @@ -458,20 +428,55 @@ namespace MWRender void PostProcessor::createObjectsForFrame(size_t frameId) { - auto& fbos = mFbos[frameId]; auto& textures = mTextures[frameId]; - auto width = renderWidth(); - auto height = renderHeight(); - for (auto& tex : textures) + int width = renderWidth(); + int height = renderHeight(); + + for (osg::ref_ptr& texture : textures) { - if (!tex) - continue; - - Stereo::setMultiviewCompatibleTextureSize(tex, width, height); - tex->dirtyTextureObject(); + if (!texture) + { + if (Stereo::getMultiview()) + texture = new osg::Texture2DArray; + else + texture = new osg::Texture2D; + } + Stereo::setMultiviewCompatibleTextureSize(texture, width, height); + texture->setSourceFormat(GL_RGBA); + texture->setSourceType(GL_UNSIGNED_BYTE); + texture->setInternalFormat(GL_RGBA); + texture->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture::LINEAR); + texture->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture::LINEAR); + texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + texture->setResizeNonPowerOfTwoHint(false); + Stereo::setMultiviewCompatibleTextureSize(texture, width, height); + texture->dirtyTextureObject(); } + 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()); + tex->setInternalFormat(SceneUtil::AutoDepth::depthInternalFormat()); + }; + + setupDepth(textures[Tex_Depth]); + setupDepth(textures[Tex_OpaqueDepth]); + textures[Tex_OpaqueDepth]->setName("opaqueTexMap"); + + auto& fbos = mFbos[frameId]; + fbos[FBO_Primary] = new osg::FrameBufferObject; fbos[FBO_Primary]->setAttachment( osg::Camera::COLOR_BUFFER0, Stereo::createMultiviewCompatibleAttachment(textures[Tex_Scene])); @@ -498,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); @@ -521,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]) @@ -534,13 +541,12 @@ namespace MWRender osg::FrameBufferAttachment(new osg::RenderBuffer(textures[Tex_OpaqueDepth]->getTextureWidth(), textures[Tex_OpaqueDepth]->getTextureHeight(), textures[Tex_Scene]->getInternalFormat()))); #endif + + mCanvases[frameId]->dirty(); } - void PostProcessor::dirtyTechniques() + void PostProcessor::dirtyTechniques(bool dirtyAttachments) { - if (!isEnabled()) - return; - size_t frameId = frame() % 2; mDirty = true; @@ -553,6 +559,8 @@ namespace MWRender mNormals = false; mPassLights = false; + std::vector attachmentsToDirty; + for (const auto& technique : mTechniques) { if (!technique->isValid()) @@ -585,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)); @@ -592,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 @@ -618,8 +629,6 @@ namespace MWRender uniform->mName.c_str(), *type, uniform->getNumElements())); } - std::unordered_map renderTargetCache; - for (const auto& pass : technique->getPasses()) { int subTexUnit = texUnit; @@ -631,32 +640,39 @@ namespace MWRender if (!pass->getTarget().empty()) { - const auto& rt = technique->getRenderTargetsMap()[pass->getTarget()]; - - const auto [w, h] = rt.mSize.get(renderWidth(), renderHeight()); - - subPass.mRenderTexture = new osg::Texture2D(*rt.mTarget); - renderTargetCache[rt.mTarget] = subPass.mRenderTexture; - subPass.mRenderTexture->setTextureSize(w, h); - subPass.mRenderTexture->setName(std::string(pass->getTarget())); - - if (rt.mMipMap) - subPass.mRenderTexture->setNumMipmapLevels(osg::Image::computeNumberOfMipmapLevels(w, h)); + auto& renderTarget = technique->getRenderTargetsMap()[pass->getTarget()]; + subPass.mSize = renderTarget.mSize; + subPass.mRenderTexture = renderTarget.mTarget; + subPass.mMipMap = renderTarget.mMipMap; subPass.mRenderTarget = new osg::FrameBufferObject; subPass.mRenderTarget->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, osg::FrameBufferAttachment(subPass.mRenderTexture)); + + const auto [w, h] = renderTarget.mSize.get(renderWidth(), renderHeight()); subPass.mStateSet->setAttributeAndModes(new osg::Viewport(0, 0, w, h)); + + if (std::find_if(attachmentsToDirty.cbegin(), attachmentsToDirty.cend(), + [renderTarget](const auto& rt) { return renderTarget.mTarget == rt.mTarget; }) + == attachmentsToDirty.cend()) + { + attachmentsToDirty.push_back(fx::Types::RenderTarget(renderTarget)); + } } - for (const auto& whitelist : pass->getRenderTargets()) + for (const auto& name : pass->getRenderTargets()) { - auto it = technique->getRenderTargetsMap().find(whitelist); - if (it != technique->getRenderTargetsMap().end() && renderTargetCache[it->second.mTarget]) + auto& renderTarget = technique->getRenderTargetsMap()[name]; + subPass.mStateSet->setTextureAttribute(subTexUnit, renderTarget.mTarget); + subPass.mStateSet->addUniform(new osg::Uniform(name.c_str(), subTexUnit)); + + if (std::find_if(attachmentsToDirty.cbegin(), attachmentsToDirty.cend(), + [renderTarget](const auto& rt) { return renderTarget.mTarget == rt.mTarget; }) + == attachmentsToDirty.cend()) { - subPass.mStateSet->setTextureAttribute(subTexUnit, renderTargetCache[it->second.mTarget]); - subPass.mStateSet->addUniform(new osg::Uniform(std::string(it->first).c_str(), subTexUnit++)); + attachmentsToDirty.push_back(fx::Types::RenderTarget(renderTarget)); } + subTexUnit++; } node.mPasses.emplace_back(std::move(subPass)); @@ -667,32 +683,29 @@ namespace MWRender mTemplateData.emplace_back(std::move(node)); } - mPingPongCanvas->setCurrentFrameData(frameId, fx::DispatchArray(mTemplateData)); + mCanvases[frameId]->setPasses(fx::DispatchArray(mTemplateData)); if (auto hud = MWBase::Environment::get().getWindowManager()->getPostProcessorHud()) hud->updateTechniques(); mRendering.getSkyManager()->setSunglare(sunglare); + + if (dirtyAttachments) + mCanvases[frameId]->setDirtyAttachments(attachmentsToDirty); } PostProcessor::Status PostProcessor::enableTechnique( std::shared_ptr technique, std::optional location) { - if (!isEnabled()) - { - Log(Debug::Warning) << "PostProcessing disabled, cannot load technique '" << technique->getName() << "'"; - return Status_Error; - } - if (!technique || technique->getLocked() || (location.has_value() && location.value() < 0)) return Status_Error; 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(); + dirtyTechniques(Settings::ShaderManager::get().getMode() == Settings::ShaderManager::Mode::Debug); return Status_Toggled; } @@ -721,86 +734,8 @@ namespace MWRender return technique->isValid(); } - void PostProcessor::createTexturesAndCamera(size_t frameId) - { - auto& textures = mTextures[frameId]; - - auto width = renderWidth(); - auto height = renderHeight(); - - for (auto& texture : textures) - { - if (!texture) - { - if (Stereo::getMultiview()) - texture = new osg::Texture2DArray; - else - texture = new osg::Texture2D; - } - Stereo::setMultiviewCompatibleTextureSize(texture, width, height); - texture->setSourceFormat(GL_RGBA); - texture->setSourceType(GL_UNSIGNED_BYTE); - texture->setInternalFormat(GL_RGBA); - texture->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture::LINEAR); - texture->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture::LINEAR); - texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - texture->setResizeNonPowerOfTwoHint(false); - } - - textures[Tex_Normal]->setSourceFormat(GL_RGB); - textures[Tex_Normal]->setInternalFormat(GL_RGB); - - auto setupDepth = [](osg::Texture* tex) { - tex->setSourceFormat(GL_DEPTH_STENCIL_EXT); - tex->setSourceType(SceneUtil::AutoDepth::depthSourceType()); - tex->setInternalFormat(SceneUtil::AutoDepth::depthInternalFormat()); - }; - - setupDepth(textures[Tex_Depth]); - - if (mDisableDepthPasses) - { - textures[Tex_OpaqueDepth] = nullptr; - } - else - { - setupDepth(textures[Tex_OpaqueDepth]); - textures[Tex_OpaqueDepth]->setName("opaqueTexMap"); - } - - if (mHUDCamera) - return; - - mHUDCamera = new osg::Camera; - mHUDCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); - mHUDCamera->setRenderOrder(osg::Camera::POST_RENDER); - mHUDCamera->setClearColor(osg::Vec4(0.45, 0.45, 0.14, 1.0)); - mHUDCamera->setClearMask(0); - mHUDCamera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1, 0, 1)); - mHUDCamera->setAllowEventFocus(false); - mHUDCamera->setViewport(0, 0, mWidth, mHeight); - - mViewer->getCamera()->removeCullCallback(mPingPongCull); - mPingPongCull = new PingPongCull(this); - mViewer->getCamera()->addCullCallback(mPingPongCull); - - mPingPongCanvas = new PingPongCanvas(mRendering.getResourceSystem()->getSceneManager()->getShaderManager()); - mHUDCamera->addChild(mPingPongCanvas); - mHUDCamera->setNodeMask(Mask_RenderToTexture); - - mHUDCamera->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); - mHUDCamera->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - } - std::shared_ptr PostProcessor::loadTechnique(const std::string& name, bool loadNextFrame) { - if (!isEnabled()) - { - Log(Debug::Warning) << "PostProcessing disabled, cannot load technique '" << name << "'"; - return nullptr; - } - for (const auto& technique : mTemplates) if (Misc::StringUtils::ciEqual(technique->getName(), name)) return technique; @@ -831,11 +766,13 @@ namespace MWRender void PostProcessor::loadChain() { - if (!isEnabled()) - return; - mTechniques.clear(); + for (const auto& technique : mInternalTechniques) + { + mTechniques.push_back(technique); + } + for (const std::string& techniqueName : Settings::postProcessing().mChain.get()) { if (techniqueName.empty()) @@ -853,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()); } @@ -866,7 +803,7 @@ namespace MWRender for (auto& technique : mTemplates) technique->compile(); - dirtyTechniques(); + dirtyTechniques(true); } void PostProcessor::disableDynamicShaders() diff --git a/apps/openmw/mwrender/postprocessor.hpp b/apps/openmw/mwrender/postprocessor.hpp index 4473ade836..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 }; @@ -115,14 +119,14 @@ namespace MWRender return mFbos[frameId][FBO_Multisample] ? mFbos[frameId][FBO_Multisample] : mFbos[frameId][FBO_Primary]; } + osg::ref_ptr getHUDCamera() { return mHUDCamera; } + osg::ref_ptr getStateUpdater() { return mStateUpdater; } const TechniqueList& getTechniques() { return mTechniques; } const TechniqueList& getTemplates() const { return mTemplates; } - osg::ref_ptr getCanvas() { return mPingPongCanvas; } - const auto& getTechniqueMap() const { return mTechniqueFileMap; } void resize(); @@ -173,13 +177,11 @@ namespace MWRender std::shared_ptr loadTechnique(const std::string& name, bool loadNextFrame = false); - bool isEnabled() const { return mUsePostProcessing && mEnabled; } - - bool getHDR() const { return mHDR; } + bool isEnabled() const { return mUsePostProcessing; } void disable(); - void enable(bool usePostProcessing = true); + void enable(); void setRenderTargetSize(int width, int height) { @@ -194,7 +196,7 @@ namespace MWRender void triggerShaderReload(); - bool mEnableLiveReload; + bool mEnableLiveReload = false; void loadChain(); void saveChain(); @@ -206,11 +208,7 @@ namespace MWRender void createObjectsForFrame(size_t frameId); - void createTexturesAndCamera(size_t frameId); - - void reloadMainPass(fx::Technique& technique); - - void dirtyTechniques(); + void dirtyTechniques(bool dirtyAttachments = false); void update(size_t frameId); @@ -229,45 +227,43 @@ namespace MWRender TechniqueList mTechniques; TechniqueList mTemplates; TechniqueList mQueuedTemplates; + TechniqueList mInternalTechniques; std::unordered_map mTechniqueFileMap; - int mSamples; - - bool mDirty; - size_t mDirtyFrameId; - RenderingManager& mRendering; osgViewer::Viewer* mViewer; const VFS::Manager* mVFS; - bool mTriggerShaderReload; - bool mReload; - bool mEnabled; - bool mUsePostProcessing; - bool mDisableDepthPasses; + size_t mDirtyFrameId = 0; + size_t mLastFrameNumber = 0; + float mLastSimulationTime = 0.f; - size_t mLastFrameNumber; - float mLastSimulationTime; + bool mDirty = false; + bool mReload = true; + bool mTriggerShaderReload = false; + bool mUsePostProcessing = false; + + bool mUBO = false; + bool mHDR = false; + bool mNormals = false; + bool mUnderwater = false; + bool mPassLights = false; + bool mPrevNormals = false; + bool mExteriorFlag = false; + bool mNormalsSupported = false; + bool mPrevPassLights = false; - bool mExteriorFlag; - bool mUnderwater; - bool mHDR; - bool mNormals; - bool mPrevNormals; - bool mNormalsSupported; - bool mPassLights; - bool mPrevPassLights; - bool mUBO; int mGLSLVersion; + int mWidth; + int mHeight; + int mSamples; osg::ref_ptr mStateUpdater; osg::ref_ptr mPingPongCull; - osg::ref_ptr mPingPongCanvas; + std::array, 2> mCanvases; osg::ref_ptr mTransparentDepthPostPass; - - int mWidth; - int mHeight; + 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 23508e70bd..fa92fa1420 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -331,8 +331,8 @@ namespace MWRender // Shadows and radial fog have problems with fixed-function mode. bool forceShaders = Settings::fog().mRadialFog || Settings::fog().mExponentialFog || Settings::shaders().mSoftParticles || Settings::shaders().mForceShaders - || Settings::Manager::getBool("enable shadows", "Shadows") - || lightingMethod != SceneUtil::LightingMethod::FFP || reverseZ || mSkyBlending || Stereo::getMultiview(); + || Settings::shadows().mEnableShadows || lightingMethod != SceneUtil::LightingMethod::FFP || reverseZ + || mSkyBlending || Stereo::getMultiview(); resourceSystem->getSceneManager()->setForceShaders(forceShaders); // FIXME: calling dummy method because terrain needs to know whether lighting is clamped @@ -367,22 +367,22 @@ namespace MWRender sceneRoot->setName("Scene Root"); int shadowCastingTraversalMask = Mask_Scene; - if (Settings::Manager::getBool("actor shadows", "Shadows")) + if (Settings::shadows().mActorShadows) shadowCastingTraversalMask |= Mask_Actor; - if (Settings::Manager::getBool("player shadows", "Shadows")) + if (Settings::shadows().mPlayerShadows) shadowCastingTraversalMask |= Mask_Player; int indoorShadowCastingTraversalMask = shadowCastingTraversalMask; - if (Settings::Manager::getBool("object shadows", "Shadows")) + if (Settings::shadows().mObjectShadows) shadowCastingTraversalMask |= (Mask_Object | Mask_Static); - if (Settings::Manager::getBool("terrain shadows", "Shadows")) + if (Settings::shadows().mTerrainShadows) shadowCastingTraversalMask |= Mask_Terrain; mShadowManager = std::make_unique(sceneRoot, mRootNode, shadowCastingTraversalMask, - indoorShadowCastingTraversalMask, Mask_Terrain | Mask_Object | Mask_Static, + indoorShadowCastingTraversalMask, Mask_Terrain | Mask_Object | Mask_Static, Settings::shadows(), mResourceSystem->getSceneManager()->getShaderManager()); - Shader::ShaderManager::DefineMap shadowDefines = mShadowManager->getShadowDefines(); + Shader::ShaderManager::DefineMap shadowDefines = mShadowManager->getShadowDefines(Settings::shadows()); Shader::ShaderManager::DefineMap lightDefines = sceneRoot->getLightDefines(); Shader::ShaderManager::DefineMap globalDefines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); @@ -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); } @@ -702,7 +704,7 @@ namespace MWRender { // need to wrap this in a StateUpdater? mSunLight->setDiffuse(diffuse); - mSunLight->setSpecular(specular); + mSunLight->setSpecular(osg::Vec4f(specular.x(), specular.y(), specular.z(), specular.w() * sunVis)); mPostProcessor->getStateUpdater()->setSunColor(diffuse); mPostProcessor->getStateUpdater()->setSunVis(sunVis); @@ -770,7 +772,7 @@ namespace MWRender if (enabled) mShadowManager->enableOutdoorMode(); else - mShadowManager->enableIndoorMode(); + mShadowManager->enableIndoorMode(Settings::shadows()); mPostProcessor->getStateUpdater()->setIsInterior(!enabled); } @@ -847,6 +849,7 @@ namespace MWRender float rainIntensity = mSky->getPrecipitationAlpha(); mWater->setRainIntensity(rainIntensity); + mWater->setRainRipplesEnabled(mSky->getRainRipplesEnabled()); mWater->update(dt, paused); if (!paused) @@ -1230,8 +1233,8 @@ namespace MWRender if (mViewDistance < mNearClip) throw std::runtime_error("Viewing distance is less than near clip"); - double width = Settings::Manager::getInt("resolution x", "Video"); - double height = Settings::Manager::getInt("resolution y", "Video"); + const double width = Settings::video().mResolutionX; + const double height = Settings::video().mResolutionY; double aspect = (height == 0.0) ? 1.0 : width / height; float fov = mFieldOfView; @@ -1256,7 +1259,7 @@ namespace MWRender mSharedUniformStateUpdater->setScreenRes(res.x(), res.y()); Stereo::Manager::instance().setMasterProjectionMatrix(mPerViewUniformStateUpdater->getProjectionMatrix()); } - else if (!mPostProcessor->isEnabled()) + else { mSharedUniformStateUpdater->setScreenRes(width, height); } @@ -1319,6 +1322,7 @@ namespace MWRender const float lodFactor = Settings::terrain().mLodFactor; const bool groundcover = Settings::groundcover().mEnabled; const bool distantTerrain = Settings::terrain().mDistantTerrain; + const double expiryDelay = Settings::cells().mCacheExpiryDelay; if (distantTerrain || groundcover) { const int compMapResolution = Settings::terrain().mCompositeMapResolution; @@ -1329,7 +1333,7 @@ namespace MWRender const bool debugChunks = Settings::terrain().mDebugChunks; auto quadTreeWorld = std::make_unique(mSceneRoot, mRootNode, mResourceSystem, mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug, compMapResolution, compMapLevel, - lodFactor, vertexLodMod, maxCompGeometrySize, debugChunks, worldspace); + lodFactor, vertexLodMod, maxCompGeometrySize, debugChunks, worldspace, expiryDelay); if (Settings::terrain().mObjectPaging) { newChunkMgr.mObjectPaging @@ -1351,7 +1355,7 @@ namespace MWRender } else newChunkMgr.mTerrain = std::make_unique(mSceneRoot, mRootNode, mResourceSystem, - mTerrainStorage.get(), Mask_Terrain, worldspace, Mask_PreCompile, Mask_Debug); + mTerrainStorage.get(), Mask_Terrain, worldspace, expiryDelay, Mask_PreCompile, Mask_Debug); newChunkMgr.mTerrain->setTargetFrameRate(Settings::cells().mTargetFramerate); float distanceMult = std::cos(osg::DegreesToRadians(std::min(mFieldOfView, 140.f)) / 2.f); diff --git a/apps/openmw/mwrender/ripples.cpp b/apps/openmw/mwrender/ripples.cpp index 191ff0e714..130e005729 100644 --- a/apps/openmw/mwrender/ripples.cpp +++ b/apps/openmw/mwrender/ripples.cpp @@ -128,10 +128,11 @@ namespace MWRender { size_t frameId = nv.getFrameStamp()->getFrameNumber() % 2; - const ESM::Position& player = MWMechanics::getPlayer().getRefData().getPosition(); + const auto& player = MWMechanics::getPlayer(); + const ESM::Position& playerPos = player.getRefData().getPosition(); mCurrentPlayerPos = osg::Vec2f( - std::floor(player.pos[0] / mWorldScaleFactor), std::floor(player.pos[1] / mWorldScaleFactor)); + std::floor(playerPos.pos[0] / mWorldScaleFactor), std::floor(playerPos.pos[1] / mWorldScaleFactor)); osg::Vec2f offset = mCurrentPlayerPos - mLastPlayerPos; mLastPlayerPos = mCurrentPlayerPos; mState[frameId].mPaused = mPaused; diff --git a/apps/openmw/mwrender/screenshotmanager.cpp b/apps/openmw/mwrender/screenshotmanager.cpp index 336a321cf0..a23d242a15 100644 --- a/apps/openmw/mwrender/screenshotmanager.cpp +++ b/apps/openmw/mwrender/screenshotmanager.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -20,7 +21,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwgui/loadingscreen.hpp" +#include "../mwbase/world.hpp" #include "postprocessor.hpp" #include "util.hpp" @@ -29,7 +30,7 @@ namespace MWRender { - enum Screenshot360Type + enum class Screenshot360Type { Spherical, Cylindrical, @@ -102,24 +103,6 @@ namespace MWRender int width = screenW - leftPadding * 2; int height = screenH - topPadding * 2; - // Ensure we are reading from the resolved framebuffer and not the multisampled render buffer. Also ensure - // that the readbuffer is set correctly with rendeirng to FBO. glReadPixel() cannot read from multisampled - // targets - PostProcessor* postProcessor = dynamic_cast(renderInfo.getCurrentCamera()->getUserData()); - osg::GLExtensions* ext = osg::GLExtensions::Get(renderInfo.getContextID(), false); - - if (ext) - { - size_t frameId = renderInfo.getState()->getFrameStamp()->getFrameNumber() % 2; - osg::FrameBufferObject* fbo = nullptr; - - if (postProcessor && postProcessor->getFbo(PostProcessor::FBO_Primary, frameId)) - fbo = postProcessor->getFbo(PostProcessor::FBO_Primary, frameId); - - if (fbo) - fbo->apply(*renderInfo.getState(), osg::FrameBufferObject::READ_FRAMEBUFFER); - } - mImage->readPixels(leftPadding, topPadding, width, height, GL_RGB, GL_UNSIGNED_BYTE); mImage->scaleImage(mWidth, mHeight, 1); } @@ -145,7 +128,7 @@ namespace MWRender void ScreenshotManager::screenshot(osg::Image* image, int w, int h) { - osg::Camera* camera = mViewer->getCamera(); + osg::Camera* camera = MWBase::Environment::get().getWorld()->getPostProcessor()->getHUDCamera(); osg::ref_ptr tempDrw = new osg::Drawable; tempDrw->setDrawCallback(new ReadImageFromFramebufferCallback(image, w, h)); tempDrw->setCullingActive(false); @@ -161,59 +144,46 @@ namespace MWRender bool ScreenshotManager::screenshot360(osg::Image* image) { - int screenshotW = mViewer->getCamera()->getViewport()->width(); - int screenshotH = mViewer->getCamera()->getViewport()->height(); - Screenshot360Type screenshotMapping = Spherical; + const Settings::ScreenshotSettings& settings = Settings::video().mScreenshotType; - const std::string& settingStr = Settings::Manager::getString("screenshot type", "Video"); - std::vector settingArgs; - Misc::StringUtils::split(settingStr, settingArgs); + Screenshot360Type screenshotMapping = Screenshot360Type::Spherical; - if (settingArgs.size() > 0) + switch (settings.mType) { - std::string_view typeStrings[4] = { "spherical", "cylindrical", "planet", "cubemap" }; - bool found = false; - - for (int i = 0; i < 4; ++i) - { - if (settingArgs[0] == typeStrings[i]) - { - screenshotMapping = static_cast(i); - found = true; - break; - } - } - - if (!found) - { - Log(Debug::Warning) << "Wrong screenshot type: " << settingArgs[0] << "."; + case Settings::ScreenshotType::Regular: + Log(Debug::Warning) << "Wrong screenshot 360 type: regular."; return false; - } + case Settings::ScreenshotType::Cylindrical: + screenshotMapping = Screenshot360Type::Cylindrical; + break; + case Settings::ScreenshotType::Spherical: + screenshotMapping = Screenshot360Type::Spherical; + break; + case Settings::ScreenshotType::Planet: + screenshotMapping = Screenshot360Type::Planet; + break; + case Settings::ScreenshotType::Cubemap: + screenshotMapping = Screenshot360Type::RawCubemap; + break; } + int screenshotW = mViewer->getCamera()->getViewport()->width(); + + if (settings.mWidth.has_value()) + screenshotW = *settings.mWidth; + + int screenshotH = mViewer->getCamera()->getViewport()->height(); + + if (settings.mHeight.has_value()) + screenshotH = *settings.mHeight; + // planet mapping needs higher resolution - int cubeSize = screenshotMapping == Planet ? screenshotW : screenshotW / 2; - - if (settingArgs.size() > 1) - { - screenshotW = std::min(10000, Misc::StringUtils::toNumeric(settingArgs[1], 0)); - } - - if (settingArgs.size() > 2) - { - screenshotH = std::min(10000, Misc::StringUtils::toNumeric(settingArgs[2], 0)); - } - - if (settingArgs.size() > 3) - { - cubeSize = std::min(5000, Misc::StringUtils::toNumeric(settingArgs[3], 0)); - } - - bool rawCubemap = screenshotMapping == RawCubemap; + const int cubeSize = screenshotMapping == Screenshot360Type::Planet ? screenshotW : screenshotW / 2; + const bool rawCubemap = screenshotMapping == Screenshot360Type::RawCubemap; if (rawCubemap) screenshotW = cubeSize * 6; // the image will consist of 6 cube sides in a row - else if (screenshotMapping == Planet) + else if (screenshotMapping == Screenshot360Type::Planet) screenshotH = screenshotW; // use square resolution for planet mapping std::vector> images; @@ -276,7 +246,7 @@ namespace MWRender stateset->setAttributeAndModes(shaderMgr.getProgram("360"), osg::StateAttribute::ON); stateset->addUniform(new osg::Uniform("cubeMap", 0)); - stateset->addUniform(new osg::Uniform("mapping", screenshotMapping)); + stateset->addUniform(new osg::Uniform("mapping", static_cast(screenshotMapping))); stateset->setTextureAttributeAndModes(0, cubeTexture, osg::StateAttribute::ON); screenshotCamera->addChild(quad); diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 51018e93f9..6df3734252 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -220,7 +220,7 @@ namespace camera->setNodeMask(MWRender::Mask_RenderToTexture); camera->setCullMask(MWRender::Mask_Sky); camera->addChild(mEarlyRenderBinRoot); - SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet()); + SceneUtil::ShadowManager::disableShadowsForStateSet(Settings::shadows(), *camera->getOrCreateStateSet()); } private: @@ -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) @@ -271,7 +273,7 @@ namespace MWRender if (!mSceneManager->getForceShaders()) skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED | osg::StateAttribute::ON); - SceneUtil::ShadowManager::disableShadowsForStateSet(skyroot->getOrCreateStateSet()); + SceneUtil::ShadowManager::disableShadowsForStateSet(Settings::shadows(), *skyroot->getOrCreateStateSet()); parentNode->addChild(skyroot); mEarlyRenderBinRoot = new osg::Group; @@ -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/util.cpp b/apps/openmw/mwrender/util.cpp index 234b022f5d..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,19 +57,19 @@ 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() { - return Settings::shaders().mAntialiasAlphaTest && Settings::Manager::getInt("antialiasing", "Video") > 1; + return Settings::shaders().mAntialiasAlphaTest && Settings::video().mAntialiasing > 1; } } 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 091ab99821..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 @@ -265,7 +273,8 @@ namespace MWRender camera->setNodeMask(Mask_RenderToTexture); if (Settings::water().mRefractionScale != 1) // TODO: to be removed with issue #5709 - SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet()); + SceneUtil::ShadowManager::disableShadowsForStateSet( + Settings::shadows(), *camera->getOrCreateStateSet()); } void apply(osg::Camera* camera) override @@ -341,7 +350,7 @@ namespace MWRender camera->addChild(mClipCullNode); camera->setNodeMask(Mask_RenderToTexture); - SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet()); + SceneUtil::ShadowManager::disableShadowsForStateSet(Settings::shadows(), *camera->getOrCreateStateSet()); } void apply(osg::Camera* camera) override @@ -429,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) @@ -578,7 +587,7 @@ namespace MWRender node->setStateSet(stateset); node->setUpdateCallback(nullptr); - mRainIntensityUpdater = nullptr; + mRainSettingsUpdater = nullptr; // Add animated textures std::vector> textures; @@ -710,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); @@ -800,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..d617a02b9a 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -445,10 +445,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(); } }; diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 8819aaa29c..f29d34c72a 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -2,6 +2,8 @@ #include +#include + #include #include @@ -409,10 +411,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); @@ -440,7 +457,9 @@ void MWState::StateManager::loadGame(const Character* character, const std::file { ESM::SavedGame profile; profile.load(reader); - if (!verifyProfile(profile)) + const auto& selectedContentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); + auto missingFiles = profile.getMissingContentFiles(selectedContentFiles); + if (!missingFiles.empty() && !confirmLoading(missingFiles)) { cleanup(true); MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); @@ -453,7 +472,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()); @@ -603,11 +621,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); } @@ -680,30 +694,66 @@ void MWState::StateManager::update(float duration) } } -bool MWState::StateManager::verifyProfile(const ESM::SavedGame& profile) const +bool MWState::StateManager::confirmLoading(const std::vector& missingFiles) const { - const std::vector& selectedContentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); - bool notFound = false; - for (const std::string& contentFile : profile.mContentFiles) + std::ostringstream stream; + for (auto& contentFile : missingFiles) { - if (std::find(selectedContentFiles.begin(), selectedContentFiles.end(), contentFile) - == selectedContentFiles.end()) + Log(Debug::Warning) << "Warning: Saved game dependency " << contentFile << " is missing."; + stream << contentFile << "\n"; + } + + auto fullList = stream.str(); + if (!fullList.empty()) + fullList.pop_back(); + + constexpr size_t missingPluginsDisplayLimit = 12; + + std::vector buttons; + buttons.emplace_back("#{Interface:Yes}"); + buttons.emplace_back("#{Interface:Copy}"); + buttons.emplace_back("#{Interface:No}"); + std::string message = "#{OMWEngine:MissingContentFilesConfirmation}"; + + auto l10n = MWBase::Environment::get().getL10nManager()->getContext("OMWEngine"); + message += l10n->formatMessage("MissingContentFilesList", { "files" }, { static_cast(missingFiles.size()) }); + auto cappedSize = std::min(missingFiles.size(), missingPluginsDisplayLimit); + if (cappedSize == missingFiles.size()) + { + message += fullList; + } + else + { + for (size_t i = 0; i < cappedSize - 1; ++i) { - Log(Debug::Warning) << "Warning: Saved game dependency " << contentFile << " is missing."; - notFound = true; + message += missingFiles[i]; + message += "\n"; } + + message += "..."; } - if (notFound) + + message + += l10n->formatMessage("MissingContentFilesListCopy", { "files" }, { static_cast(missingFiles.size()) }); + + int selectedButton = -1; + while (true) { - std::vector buttons; - buttons.emplace_back("#{Interface:Yes}"); - buttons.emplace_back("#{Interface:No}"); - MWBase::Environment::get().getWindowManager()->interactiveMessageBox( - "#{OMWEngine:MissingContentFilesConfirmation}", buttons, true); - int selectedButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); - if (selectedButton == 1 || selectedButton == -1) - return false; + auto windowManager = MWBase::Environment::get().getWindowManager(); + windowManager->interactiveMessageBox(message, buttons, true, selectedButton); + selectedButton = windowManager->readPressedButton(); + if (selectedButton == 0) + break; + + if (selectedButton == 1) + { + SDL_SetClipboardText(fullList.c_str()); + continue; + } + + return false; } + return true; } diff --git a/apps/openmw/mwstate/statemanagerimp.hpp b/apps/openmw/mwstate/statemanagerimp.hpp index dfd4dd12f0..03843163d1 100644 --- a/apps/openmw/mwstate/statemanagerimp.hpp +++ b/apps/openmw/mwstate/statemanagerimp.hpp @@ -23,7 +23,7 @@ namespace MWState private: void cleanup(bool force = false); - bool verifyProfile(const ESM::SavedGame& profile) const; + bool confirmLoading(const std::vector& missingFiles) const; void writeScreenshot(std::vector& imageData) const; 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 d9a017fe68..fda7157010 100644 --- a/apps/openmw/mwworld/cellref.cpp +++ b/apps/openmw/mwworld/cellref.cpp @@ -188,19 +188,14 @@ namespace MWWorld void CellRef::applyChargeRemainderToBeSubtracted(float chargeRemainder) { auto esm3Visit = [&](ESM::CellRef& cellRef3) { - cellRef3.mChargeIntRemainder += std::abs(chargeRemainder); - if (cellRef3.mChargeIntRemainder > 1.0f) + cellRef3.mChargeIntRemainder -= std::abs(chargeRemainder); + if (cellRef3.mChargeIntRemainder <= -1.0f) { - float newChargeRemainder = (cellRef3.mChargeIntRemainder - std::floor(cellRef3.mChargeIntRemainder)); - if (cellRef3.mChargeInt <= static_cast(cellRef3.mChargeIntRemainder)) - { - cellRef3.mChargeInt = 0; - } - else - { - cellRef3.mChargeInt -= static_cast(cellRef3.mChargeIntRemainder); - } + float newChargeRemainder = std::modf(cellRef3.mChargeIntRemainder, &cellRef3.mChargeIntRemainder); + cellRef3.mChargeInt += static_cast(cellRef3.mChargeIntRemainder); cellRef3.mChargeIntRemainder = newChargeRemainder; + if (cellRef3.mChargeInt < 0) + cellRef3.mChargeInt = 0; } }; std::visit(ESM::VisitOverload{ @@ -211,6 +206,16 @@ namespace MWWorld mCellRef.mVariant); } + void CellRef::setChargeIntRemainder(float chargeRemainder) + { + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& /*ref*/) {}, + [&](ESM4::ActorCharacter&) {}, + [&](ESM::CellRef& ref) { ref.mChargeIntRemainder = chargeRemainder; }, + }, + mCellRef.mVariant); + } + void CellRef::setChargeFloat(float charge) { std::visit(ESM::VisitOverload{ @@ -372,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 954babe481..4dcac4def5 100644 --- a/apps/openmw/mwworld/cellref.hpp +++ b/apps/openmw/mwworld/cellref.hpp @@ -118,9 +118,22 @@ namespace MWWorld }; return std::visit(Visitor(), mCellRef.mVariant); } // Implemented as union with int charge + float getChargeIntRemainder() const + { + struct Visitor + { + float operator()(const ESM::CellRef& ref) { return ref.mChargeIntRemainder; } + float operator()(const ESM4::Reference& /*ref*/) { return 0; } + float operator()(const ESM4::ActorCharacter&) { throw std::logic_error("Not applicable"); } + }; + return std::visit(Visitor(), mCellRef.mVariant); + } void setCharge(int charge); void setChargeFloat(float charge); - void applyChargeRemainderToBeSubtracted(float chargeRemainder); // Stores remainders and applies if > 1 + void applyChargeRemainderToBeSubtracted(float chargeRemainder); // Stores remainders and applies if <= -1 + + // Stores fractional part of mChargeInt + void setChargeIntRemainder(float chargeRemainder); // The NPC that owns this object (and will get angry if you steal it) ESM::RefId getOwner() const @@ -218,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 ff79940540..fd24bb7271 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -151,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; } @@ -161,31 +162,32 @@ namespace template void writeReferenceCollection(ESM::ESMWriter& writer, const MWWorld::CellRefList& collection) { - if (!collection.mList.empty()) + // references + for (const MWWorld::LiveCellRef& liveCellRef : collection.mList) { - // references - for (typename MWWorld::CellRefList::List::const_iterator iter(collection.mList.begin()); - iter != collection.mList.end(); ++iter) + if (ESM::isESM4Rec(T::sRecordId)) { - if (!iter->mData.hasChanged() && !iter->mRef.hasChanged() && iter->mRef.hasContentFile()) - { - // Reference that came from a content file and has not been changed -> ignore - continue; - } - if (iter->mData.getCount() == 0 && !iter->mRef.hasContentFile()) - { - // Deleted reference that did not come from a content file -> ignore - continue; - } - using StateType = typename RecordToState::StateType; - StateType state; - iter->save(state); - - // recordId currently unused - writer.writeHNT("OBJE", collection.mList.front().mBase->sRecordId); - - state.save(writer); + // TODO: Implement loading/saving of REFR4 and ACHR4 with ESM3 reader/writer. + continue; } + if (!liveCellRef.mData.hasChanged() && !liveCellRef.mRef.hasChanged() && liveCellRef.mRef.hasContentFile()) + { + // Reference that came from a content file and has not been changed -> ignore + continue; + } + if (liveCellRef.mRef.getCount() == 0 && !liveCellRef.mRef.hasContentFile()) + { + // Deleted reference that did not come from a content file -> ignore + continue; + } + using StateType = typename RecordToState::StateType; + StateType state; + liveCellRef.save(state); + + // recordId currently unused + writer.writeHNT("OBJE", collection.mList.front().mBase->sRecordId); + + state.save(writer); } } @@ -199,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; } } } @@ -250,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); @@ -296,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; @@ -674,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; } @@ -1040,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(); @@ -1166,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); } @@ -1194,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); @@ -1260,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; }); @@ -1288,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 debd80a97b..0c6527ce22 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -72,6 +72,7 @@ namespace ESM4 struct Furniture; struct Flora; struct Ingredient; + struct ItemMod; struct MiscItem; struct MovableStatic; struct Terminal; @@ -96,7 +97,7 @@ namespace MWWorld CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, - CellRefList, CellRefList, CellRefList, + CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, CellRefList>; @@ -117,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 d82125a5ce..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); @@ -111,12 +111,12 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::getState( } void MWWorld::ContainerStore::storeEquipmentState( - const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const + const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const { } void MWWorld::ContainerStore::readEquipmentState( - const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory) + const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory) { } @@ -128,11 +128,11 @@ void MWWorld::ContainerStore::storeState(const LiveCellRef& ref, ESM::ObjectS template void MWWorld::ContainerStore::storeStates( - const CellRefList& collection, ESM::InventoryState& inventory, int& index, bool equipable) const + const CellRefList& collection, ESM::InventoryState& inventory, size_t& index, bool equipable) const { 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; @@ -926,7 +928,7 @@ void MWWorld::ContainerStore::writeState(ESM::InventoryState& state) const { state.mItems.clear(); - int index = 0; + size_t index = 0; storeStates(potions, state, index); storeStates(appas, state, index); storeStates(armors, state, index, true); @@ -947,12 +949,12 @@ void MWWorld::ContainerStore::readState(const ESM::InventoryState& inventory) mModified = true; mResolved = true; - int index = 0; + size_t index = 0; for (const ESM::ObjectState& state : inventory.mItems) { int type = MWBase::Environment::get().getESMStore()->find(state.mRef.mRefID); - int thisIndex = index++; + size_t thisIndex = index++; switch (type) { @@ -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/containerstore.hpp b/apps/openmw/mwworld/containerstore.hpp index 889fcf7463..fb2722dde8 100644 --- a/apps/openmw/mwworld/containerstore.hpp +++ b/apps/openmw/mwworld/containerstore.hpp @@ -161,16 +161,16 @@ namespace MWWorld void storeState(const LiveCellRef& ref, ESM::ObjectState& state) const; template - void storeStates( - const CellRefList& collection, ESM::InventoryState& inventory, int& index, bool equipable = false) const; + void storeStates(const CellRefList& collection, ESM::InventoryState& inventory, size_t& index, + bool equipable = false) const; void updateRechargingItems(); virtual void storeEquipmentState( - const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const; + const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const; virtual void readEquipmentState( - const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory); + const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory); public: ContainerStore(); diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 12fbc0d4df..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 " @@ -138,6 +156,59 @@ namespace return npcsToReplace; } + template + std::vector getSpellsToReplace( + const MWWorld::Store& spells, const MWWorld::Store& magicEffects) + { + std::vector spellsToReplace; + + for (RecordType spell : spells) + { + if (spell.mEffects.mList.empty()) + continue; + + bool changed = false; + auto iter = spell.mEffects.mList.begin(); + while (iter != spell.mEffects.mList.end()) + { + const ESM::MagicEffect* mgef = magicEffects.search(iter->mEffectID); + if (!mgef) + { + Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId + << ": dropping invalid effect (index " << iter->mEffectID << ")"; + iter = spell.mEffects.mList.erase(iter); + changed = true; + continue; + } + + if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute) && iter->mAttribute != -1) + { + iter->mAttribute = -1; + Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId + << ": dropping unexpected attribute argument of " + << ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect"; + changed = true; + } + + if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetSkill) && iter->mSkill != -1) + { + iter->mSkill = -1; + Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId + << ": dropping unexpected skill argument of " + << ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect"; + changed = true; + } + + ++iter; + } + + if (changed) + spellsToReplace.emplace_back(spell); + } + + return spellsToReplace; + } + // Custom enchanted items can reference scripts that no longer exist, this doesn't necessarily mean the base item no // longer exists however. So instead of removing the item altogether, we're only removing the script. template @@ -527,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) { @@ -538,71 +609,24 @@ namespace MWWorld removeMissingScripts(getWritable(), getWritable().mStatic); - // Validate spell effects for invalid arguments - std::vector spellsToReplace; + // Validate spell effects and enchantments for invalid arguments auto& spells = getWritable(); - for (ESM::Spell spell : spells) - { - if (spell.mEffects.mList.empty()) - continue; - - bool changed = false; - auto iter = spell.mEffects.mList.begin(); - while (iter != spell.mEffects.mList.end()) - { - const ESM::MagicEffect* mgef = getWritable().search(iter->mEffectID); - if (!mgef) - { - Log(Debug::Verbose) << "Spell '" << spell.mId << "' has an invalid effect (index " - << iter->mEffectID << ") present. Dropping the effect."; - iter = spell.mEffects.mList.erase(iter); - changed = true; - continue; - } - - if (mgef->mData.mFlags & ESM::MagicEffect::TargetSkill) - { - if (iter->mAttribute != -1) - { - iter->mAttribute = -1; - Log(Debug::Verbose) - << ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect of spell '" << spell.mId - << "' has an attribute argument present. Dropping the argument."; - changed = true; - } - } - else if (mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute) - { - if (iter->mSkill != -1) - { - iter->mSkill = -1; - Log(Debug::Verbose) - << ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect of spell '" << spell.mId - << "' has a skill argument present. Dropping the argument."; - changed = true; - } - } - else if (iter->mSkill != -1 || iter->mAttribute != -1) - { - iter->mSkill = -1; - iter->mAttribute = -1; - Log(Debug::Verbose) << ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect of spell '" - << spell.mId << "' has argument(s) present. Dropping the argument(s)."; - changed = true; - } - - ++iter; - } - - if (changed) - spellsToReplace.emplace_back(spell); - } + auto& enchantments = getWritable(); + auto& magicEffects = getWritable(); + std::vector spellsToReplace = getSpellsToReplace(spells, magicEffects); for (const ESM::Spell& spell : spellsToReplace) { spells.eraseStatic(spell.mId); spells.insertStatic(spell); } + + std::vector enchantmentsToReplace = getSpellsToReplace(enchantments, magicEffects); + for (const ESM::Enchantment& enchantment : enchantmentsToReplace) + { + enchantments.eraseStatic(enchantment.mId); + enchantments.insertStatic(enchantment); + } } void ESMStore::movePlayerRecord() @@ -617,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/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index ceba4a26ef..16062c97db 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -91,6 +91,7 @@ namespace ESM4 struct Hair; struct HeadPart; struct Ingredient; + struct ItemMod; struct Land; struct LandTexture; struct LevelledCreature; @@ -140,8 +141,8 @@ namespace MWWorld Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, - Store, Store, Store, Store, Store, - Store, Store, Store, + Store, Store, Store, Store, Store, + Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store>; diff --git a/apps/openmw/mwworld/groundcoverstore.cpp b/apps/openmw/mwworld/groundcoverstore.cpp index 85b9376f0d..17bf72b5d3 100644 --- a/apps/openmw/mwworld/groundcoverstore.cpp +++ b/apps/openmw/mwworld/groundcoverstore.cpp @@ -26,8 +26,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 +33,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 +42,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 9b3470a835..f48f4e6e31 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -46,32 +46,32 @@ void MWWorld::InventoryStore::initSlots(TSlots& slots_) } void MWWorld::InventoryStore::storeEquipmentState( - const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const + const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const { - for (int i = 0; i < static_cast(mSlots.size()); ++i) + for (int32_t i = 0; i < MWWorld::InventoryStore::Slots; ++i) + { if (mSlots[i].getType() != -1 && mSlots[i]->getBase() == &ref) - { - inventory.mEquipmentSlots[index] = i; - } + inventory.mEquipmentSlots[static_cast(index)] = i; + } if (mSelectedEnchantItem.getType() != -1 && mSelectedEnchantItem->getBase() == &ref) inventory.mSelectedEnchantItem = index; } void MWWorld::InventoryStore::readEquipmentState( - const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory) + const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory) { if (index == inventory.mSelectedEnchantItem) mSelectedEnchantItem = iter; - std::map::const_iterator found = inventory.mEquipmentSlots.find(index); + auto found = inventory.mEquipmentSlots.find(index); if (found != inventory.mEquipmentSlots.end()) { if (found->second < 0 || found->second >= MWWorld::InventoryStore::Slots) throw std::runtime_error("Invalid slot index in inventory state"); // make sure the item can actually be equipped in this slot - int slot = found->second; + int32_t slot = found->second; std::pair, bool> allowedSlots = iter->getClass().getEquipmentSlots(*iter); if (!allowedSlots.first.size()) return; @@ -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/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index 6df5fa1e5a..0af6ee2b28 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -81,9 +81,9 @@ namespace MWWorld void fireEquipmentChangedEvent(); void storeEquipmentState( - const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const override; + const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const override; void readEquipmentState( - const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory) override; + const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory) override; ContainerStoreIterator findSlot(int slot) 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..72b2dc3022 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); } @@ -115,9 +115,7 @@ namespace 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(nullptr); setNodeRotation(ptr, rendering, rotation); if (ptr.getClass().useAnim()) @@ -226,7 +224,7 @@ namespace { for (MWWorld::Ptr& ptr : mToInsert) { - if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled()) + if (!ptr.mRef->isDeleted() && ptr.getRefData().isEnabled()) { try { @@ -648,7 +646,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/store.cpp b/apps/openmw/mwworld/store.cpp index e42675c66e..ac3ee72a94 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -1362,6 +1362,7 @@ template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; +template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 0da70bdd48..aa75730b40 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -789,8 +789,7 @@ namespace MWWorld mRendering.configureFog( mResult.mFogDepth, underwaterFog, mResult.mDLFogFactor, mResult.mDLFogOffset / 100.0f, mResult.mFogColor); mRendering.setAmbientColour(mResult.mAmbientColor); - mRendering.setSunColour( - mResult.mSunColor, mResult.mSunColor * mResult.mGlareView * glareFade, mResult.mGlareView * glareFade); + mRendering.setSunColour(mResult.mSunColor, mResult.mSunColor, mResult.mGlareView * glareFade); mRendering.getSkyManager()->setWeather(mResult); @@ -899,36 +898,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); } } diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 5be1e52530..1b6af6038e 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -660,8 +660,8 @@ namespace MWWorld std::string_view World::getCellName(const MWWorld::Cell& cell) const { - if (!cell.isExterior() || !cell.getNameId().empty()) - return cell.getNameId(); + if (!cell.isExterior() || !cell.getDisplayName().empty()) + return cell.getDisplayName(); return ESM::visit(ESM::VisitOverload{ [&](const ESM::Cell& cellIn) -> std::string_view { return getCellName(&cellIn); }, @@ -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; } @@ -1737,13 +1737,14 @@ namespace MWWorld void World::updateSoundListener() { osg::Vec3f cameraPosition = mRendering->getCamera()->getPosition(); - const ESM::Position& refpos = getPlayerPtr().getRefData().getPosition(); + const auto& player = getPlayerPtr(); + const ESM::Position& refpos = player.getRefData().getPosition(); osg::Vec3f listenerPos; if (isFirstPerson()) listenerPos = cameraPosition; else - listenerPos = refpos.asVec3() + osg::Vec3f(0, 0, 1.85f * mPhysics->getHalfExtents(getPlayerPtr()).z()); + listenerPos = refpos.asVec3() + osg::Vec3f(0, 0, 1.85f * mPhysics->getHalfExtents(player).z()); osg::Quat listenerOrient = osg::Quat(refpos.rot[1], osg::Vec3f(0, -1, 0)) * osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1)); @@ -1751,7 +1752,7 @@ namespace MWWorld osg::Vec3f forward = listenerOrient * osg::Vec3f(0, 1, 0); osg::Vec3f up = listenerOrient * osg::Vec3f(0, 0, 1); - bool underwater = isUnderwater(getPlayerPtr().getCell(), cameraPosition); + bool underwater = isUnderwater(player.getCell(), cameraPosition); MWBase::Environment::get().getSoundManager()->setListenerPosDir(listenerPos, forward, up, underwater); } @@ -1892,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()) @@ -2540,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 @@ -3336,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 @@ -3631,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/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp index a93693c08b..df4d7a1e99 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/openmw_test_suite/detournavigator/navigator.cpp @@ -70,6 +70,22 @@ namespace } }; + constexpr std::array defaultHeightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, -25, -25, -25, -25, // row 1 + 0, -25, -100, -100, -100, // row 2 + 0, -25, -100, -100, -100, // row 3 + 0, -25, -100, -100, -100, // row 4 + } }; + + constexpr std::array defaultHeightfieldDataScalar{ { + 0, 0, 0, 0, 0, // row 0 + 0, -25, -25, -25, -25, // row 1 + 0, -25, -100, -100, -100, // row 2 + 0, -25, -100, -100, -100, // row 3 + 0, -25, -100, -100, -100, // row 4 + } }; + template std::unique_ptr makeSquareHeightfieldTerrainShape( const std::array& values, btScalar heightScale = 1, int upAxis = 2, @@ -150,14 +166,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_then_find_path_should_return_path) { - constexpr std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); @@ -177,20 +186,31 @@ namespace << mPath; } + TEST_F(DetourNavigatorNavigatorTest, find_path_to_the_start_position_should_contain_single_point) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, ElementsAre(Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125))) << mPath; + } + TEST_F(DetourNavigatorNavigatorTest, add_object_should_change_navmesh) { mSettings.mWaitUntilMinDistanceToPlayer = 0; mNavigator.reset(new NavigatorImpl( mSettings, std::make_unique(":memory:", std::numeric_limits::max()))); - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); @@ -235,14 +255,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_changed_object_should_change_navmesh) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); @@ -288,14 +301,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, for_overlapping_heightfields_objects_should_use_higher) { - const std::array heightfieldData1{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - CollisionShapeInstance heightfield1(makeSquareHeightfieldTerrainShape(heightfieldData1)); + CollisionShapeInstance heightfield1(makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar)); heightfield1.shape().setLocalScaling(btVector3(128, 128, 1)); const std::array heightfieldData2{ { @@ -328,14 +334,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, only_one_heightfield_per_cell_is_allowed) { - const std::array heightfieldData1{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface1 = makeSquareHeightfieldSurface(heightfieldData1); + const HeightfieldSurface surface1 = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize1 = mHeightfieldTileSize * (surface1.mSize - 1); const std::array heightfieldData2{ { @@ -366,14 +365,8 @@ namespace { osg::ref_ptr bulletShape(new Resource::BulletShape); - std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - std::unique_ptr shapePtr = makeSquareHeightfieldTerrainShape(heightfieldData); + std::unique_ptr shapePtr + = makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar); shapePtr->setLocalScaling(btVector3(128, 128, 1)); bulletShape->mCollisionShape.reset(shapePtr.release()); @@ -542,14 +535,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_object_remove_and_update_then_find_path_should_return_path) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - CollisionShapeInstance heightfield(makeSquareHeightfieldTerrainShape(heightfieldData)); + CollisionShapeInstance heightfield(makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar)); heightfield.shape().setLocalScaling(btVector3(128, 128, 1)); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); @@ -579,14 +565,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_heightfield_remove_and_update_then_find_path_should_return_path) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); @@ -649,14 +628,7 @@ namespace mNavigator.reset(new NavigatorImpl( mSettings, std::make_unique(":memory:", std::numeric_limits::max()))); - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); const btVector3 shift = getHeightfieldShift(mCellPosition, cellSize, surface.mMinHeight, surface.mMaxHeight); @@ -745,14 +717,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_then_raycast_should_return_position) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); @@ -771,14 +736,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_for_oscillating_object_that_does_not_change_navmesh_should_not_trigger_navmesh_update) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance oscillatingBox(std::make_unique(btVector3(20, 20, 20))); @@ -837,14 +795,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, for_not_reachable_destination_find_path_should_provide_partial_path) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); @@ -870,14 +821,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, end_tolerance_should_extent_available_destinations) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); @@ -1000,4 +944,58 @@ namespace INSTANTIATE_TEST_SUITE_P(NotSupportedAgentBounds, DetourNavigatorNavigatorNotSupportedAgentBoundsTest, ValuesIn(notSupportedAgentBounds)); + + TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nav_mesh_position) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + const osg::Vec3f position(250, 250, 0); + const osg::Vec3f searchAreaHalfExtents(1000, 1000, 1000); + EXPECT_THAT(findNearestNavMeshPosition(*mNavigator, mAgentBounds, position, searchAreaHalfExtents, Flag_walk), + Optional(Vec3fEq(250, 250, -62.5186))); + } + + TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nullopt_when_too_far) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + const osg::Vec3f position(250, 250, 250); + const osg::Vec3f searchAreaHalfExtents(100, 100, 100); + EXPECT_EQ(findNearestNavMeshPosition(*mNavigator, mAgentBounds, position, searchAreaHalfExtents, Flag_walk), + std::nullopt); + } + + TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nullopt_when_flags_do_not_match) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + const osg::Vec3f position(250, 250, 0); + const osg::Vec3f searchAreaHalfExtents(1000, 1000, 1000); + EXPECT_EQ(findNearestNavMeshPosition(*mNavigator, mAgentBounds, position, searchAreaHalfExtents, Flag_swim), + std::nullopt); + } } 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/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 72959f8591..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; } @@ -411,7 +432,7 @@ namespace EXPECT_EQ(*result, expected); } - TEST_F(TestBulletNifLoader, for_root_bounding_box_should_return_shape_with_compound_shape_and_box_inside) + TEST_F(TestBulletNifLoader, for_root_bounding_box_should_return_shape_with_bounding_box_data) { mNode.mName = "Bounding Box"; mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; @@ -427,15 +448,11 @@ namespace Resource::BulletShape expected; expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); - std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); - std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } - TEST_F(TestBulletNifLoader, for_child_bounding_box_should_return_shape_with_compound_shape_with_box_inside) + TEST_F(TestBulletNifLoader, for_child_bounding_box_should_return_shape_with_bounding_box_data) { mNode.mName = "Bounding Box"; mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; @@ -453,15 +470,11 @@ namespace Resource::BulletShape expected; expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); - std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); - std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } - TEST_F(TestBulletNifLoader, for_root_with_bounds_and_child_bounding_box_but_should_use_bounding_box) + TEST_F(TestBulletNifLoader, for_root_with_bounds_and_child_bounding_box_should_use_bounding_box) { mNode.mName = "Bounding Box"; mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; @@ -483,10 +496,6 @@ namespace Resource::BulletShape expected; expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); - std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); - std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -519,10 +528,6 @@ namespace Resource::BulletShape expected; expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); - std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); - std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -555,10 +560,6 @@ namespace Resource::BulletShape expected; expected.mCollisionBox.mExtents = osg::Vec3f(4, 5, 6); expected.mCollisionBox.mCenter = osg::Vec3f(-4, -5, -6); - std::unique_ptr box(new btBoxShape(btVector3(4, 5, 6))); - std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-4, -5, -6)), box.release()); - expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -592,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()); @@ -616,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()); @@ -639,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()); @@ -664,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()); @@ -689,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()); @@ -715,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()); @@ -737,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 } }; @@ -764,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 } }; @@ -796,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 } }; @@ -833,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 } }; @@ -869,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 } }; @@ -900,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()); @@ -931,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()); @@ -977,12 +994,12 @@ namespace } TEST_F(TestBulletNifLoader, - for_tri_shape_child_node_with_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision) + for_root_node_with_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision) { mNiStringExtraData.mData = "NCC__"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; - mNiTriShape.mExtra = Nif::ExtraPtr(&mNiStringExtraData); mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; Nif::NIFFile file("test.nif"); @@ -993,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()); @@ -1005,13 +1024,13 @@ namespace } TEST_F(TestBulletNifLoader, - for_tri_shape_child_node_with_not_first_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision) + for_root_node_with_not_first_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision) { mNiStringExtraData.mNext = Nif::ExtraPtr(&mNiStringExtraData2); mNiStringExtraData2.mData = "NCC__"; mNiStringExtraData2.recType = Nif::RC_NiStringExtraData; - mNiTriShape.mExtra = Nif::ExtraPtr(&mNiStringExtraData); mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; Nif::NIFFile file("test.nif"); @@ -1022,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()); @@ -1032,8 +1053,66 @@ namespace EXPECT_EQ(*result, expected); } + TEST_F( + TestBulletNifLoader, for_root_node_with_extra_data_string_starting_with_nc_should_return_shape_with_nocollision) + { + mNiStringExtraData.mData = "NC___"; + mNiStringExtraData.recType = Nif::RC_NiStringExtraData; + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + 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::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + expected.mVisualCollisionType = Resource::VisualCollisionType::Default; + + EXPECT_EQ(*result, expected); + } + TEST_F(TestBulletNifLoader, - for_tri_shape_child_node_with_extra_data_string_starting_with_nc_should_return_shape_with_nocollision) + for_root_node_with_not_first_extra_data_string_starting_with_nc_should_return_shape_with_nocollision) + { + mNiStringExtraData.mNext = Nif::ExtraPtr(&mNiStringExtraData2); + mNiStringExtraData2.mData = "NC___"; + mNiStringExtraData2.recType = Nif::RC_NiStringExtraData; + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + 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::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + expected.mVisualCollisionType = Resource::VisualCollisionType::Default; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_extra_data_string_should_ignore_extra_data) { mNiStringExtraData.mData = "NC___"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; @@ -1049,40 +1128,13 @@ 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()); - expected.mVisualCollisionType = Resource::VisualCollisionType::Default; - - EXPECT_EQ(*result, expected); - } - - TEST_F(TestBulletNifLoader, - for_tri_shape_child_node_with_not_first_extra_data_string_starting_with_nc_should_return_shape_with_nocollision) - { - mNiStringExtraData.mNext = Nif::ExtraPtr(&mNiStringExtraData2); - mNiStringExtraData2.mData = "NC___"; - mNiStringExtraData2.recType = Nif::RC_NiStringExtraData; - mNiTriShape.mExtra = Nif::ExtraPtr(&mNiStringExtraData); - mNiTriShape.mParents.push_back(&mNiNode); - mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - - Nif::NIFFile file("test.nif"); - file.mRoots.push_back(&mNiNode); - file.mHash = mHash; - - const auto result = mLoader.load(file); - - 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)); - - Resource::BulletShape expected; - expected.mCollisionShape.reset(compound.release()); - expected.mVisualCollisionType = Resource::VisualCollisionType::Default; EXPECT_EQ(*result, expected); } @@ -1111,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()); @@ -1121,33 +1175,13 @@ namespace EXPECT_EQ(*result, expected); } - TEST_F(TestBulletNifLoader, - for_tri_shape_child_node_with_extra_data_string_mrk_should_return_shape_with_null_collision_shape) - { - mNiStringExtraData.mData = "MRK"; - mNiStringExtraData.recType = Nif::RC_NiStringExtraData; - mNiTriShape.mExtra = Nif::ExtraPtr(&mNiStringExtraData); - mNiTriShape.mParents.push_back(&mNiNode); - mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - - Nif::NIFFile file("test.nif"); - file.mRoots.push_back(&mNiNode); - file.mHash = mHash; - - const auto result = mLoader.load(file); - - Resource::BulletShape expected; - - EXPECT_EQ(*result, expected); - } - TEST_F(TestBulletNifLoader, bsx_editor_marker_flag_disables_collision_for_markers) { - mNiIntegerExtraData.mData = 34; // BSXFlags "has collision" | "editor marker" - mNiIntegerExtraData.recType = Nif::RC_BSXFlags; - mNiTriShape.mExtraList.push_back(Nif::ExtraPtr(&mNiIntegerExtraData)); mNiTriShape.mParents.push_back(&mNiNode); mNiTriShape.mName = "EditorMarker"; + mNiIntegerExtraData.mData = 34; // BSXFlags "has collision" | "editor marker" + mNiIntegerExtraData.recType = Nif::RC_BSXFlags; + mNiNode.mExtraList.push_back(Nif::ExtraPtr(&mNiIntegerExtraData)); mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; Nif::NIFFile file("test.nif"); @@ -1162,18 +1196,14 @@ namespace EXPECT_EQ(*result, expected); } - TEST_F(TestBulletNifLoader, - for_tri_shape_child_node_with_extra_data_string_mrk_and_other_collision_node_should_return_shape_with_triangle_mesh_shape_with_all_meshes) + TEST_F(TestBulletNifLoader, mrk_editor_marker_flag_disables_collision_for_markers) { + mNiTriShape.mParents.push_back(&mNiNode); + mNiTriShape.mName = "Tri EditorMarker"; mNiStringExtraData.mData = "MRK"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; - mNiTriShape.mExtra = Nif::ExtraPtr(&mNiStringExtraData); - mNiTriShape.mParents.push_back(&mNiNode2); - mNiNode2.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - mNiNode2.recType = Nif::RC_RootCollisionNode; - mNiNode2.mParents.push_back(&mNiNode); - mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiNode2) }; - mNiNode.recType = Nif::RC_NiNode; + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; Nif::NIFFile file("test.nif"); file.mRoots.push_back(&mNiNode); @@ -1181,13 +1211,7 @@ namespace const auto result = mLoader.load(file); - 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)); - Resource::BulletShape expected; - expected.mCollisionShape.reset(compound.release()); EXPECT_EQ(*result, expected); } @@ -1203,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()); @@ -1292,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()); @@ -1323,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/nifosg/testnifloader.cpp b/apps/openmw_test_suite/nifosg/testnifloader.cpp index a82fba15ca..f05d651301 100644 --- a/apps/openmw_test_suite/nifosg/testnifloader.cpp +++ b/apps/openmw_test_suite/nifosg/testnifloader.cpp @@ -108,7 +108,7 @@ osg::Group { )"); } - std::string formatOsgNodeForShaderProperty(std::string_view shaderPrefix) + std::string formatOsgNodeForBSShaderProperty(std::string_view shaderPrefix) { std::ostringstream oss; oss << R"( @@ -165,6 +165,72 @@ osg::Group { return oss.str(); } + std::string formatOsgNodeForBSLightingShaderProperty(std::string_view shaderPrefix) + { + std::ostringstream oss; + oss << R"( +osg::Group { + UniqueID 1 + DataVariance STATIC + UserDataContainer TRUE { + osg::DefaultUserDataContainer { + UniqueID 2 + UDC_UserObjects 1 { + osg::StringValueObject { + UniqueID 3 + Name "fileHash" + } + } + } + } + Children 1 { + osg::Group { + UniqueID 4 + DataVariance STATIC + UserDataContainer TRUE { + osg::DefaultUserDataContainer { + UniqueID 5 + UDC_UserObjects 3 { + osg::UIntValueObject { + UniqueID 6 + Name "recIndex" + Value 4294967295 + } + osg::StringValueObject { + UniqueID 7 + Name "shaderPrefix" + Value ")" + << shaderPrefix << R"(" + } + osg::BoolValueObject { + UniqueID 8 + Name "shaderRequired" + Value TRUE + } + } + } + } + StateSet TRUE { + osg::StateSet { + UniqueID 9 + ModeList 1 { + GL_DEPTH_TEST ON + } + AttributeList 1 { + osg::Depth { + UniqueID 10 + } + Value OFF + } + } + } + } + } +} +)"; + return oss.str(); + } + struct ShaderPrefixParams { unsigned int mShaderType; @@ -194,7 +260,7 @@ osg::Group { Nif::NIFFile file("test.nif"); file.mRoots.push_back(&node); auto result = Loader::load(file, &mImageManager); - EXPECT_EQ(serialize(*result), formatOsgNodeForShaderProperty(GetParam().mExpectedShaderPrefix)); + EXPECT_EQ(serialize(*result), formatOsgNodeForBSShaderProperty(GetParam().mExpectedShaderPrefix)); } INSTANTIATE_TEST_SUITE_P(Params, NifOsgLoaderBSShaderPrefixTest, ValuesIn(NifOsgLoaderBSShaderPrefixTest::sParams)); @@ -218,11 +284,13 @@ osg::Group { property.mTextureSet = nullptr; property.mController = nullptr; property.mType = GetParam().mShaderType; + property.mShaderFlags1 |= Nif::BSShaderFlags1::BSSFlag1_DepthTest; + property.mShaderFlags2 |= Nif::BSShaderFlags2::BSSFlag2_DepthWrite; node.mProperties.push_back(Nif::RecordPtrT(&property)); Nif::NIFFile file("test.nif"); file.mRoots.push_back(&node); auto result = Loader::load(file, &mImageManager); - EXPECT_EQ(serialize(*result), formatOsgNodeForShaderProperty(GetParam().mExpectedShaderPrefix)); + EXPECT_EQ(serialize(*result), formatOsgNodeForBSLightingShaderProperty(GetParam().mExpectedShaderPrefix)); } INSTANTIATE_TEST_SUITE_P( 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/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/cmake/FindSDL2.cmake b/cmake/FindSDL2.cmake deleted file mode 100644 index 4f2be8c421..0000000000 --- a/cmake/FindSDL2.cmake +++ /dev/null @@ -1,129 +0,0 @@ -# Locate SDL2 library -# This module defines -# SDL2_LIBRARY, the SDL2 library, with no other libraries -# SDL2_LIBRARIES, the SDL library and required components with compiler flags -# SDL2_FOUND, if false, do not try to link to SDL2 -# SDL2_INCLUDE_DIR, where to find SDL.h -# SDL2_VERSION, the version of the found library -# -# This module accepts the following env variables -# SDL2DIR - Can be set to ./configure --prefix=$SDL2DIR used in building SDL2. l.e.galup 9-20-02 -# This module responds to the the flag: -# SDL2_BUILDING_LIBRARY -# If this is defined, then no SDL2_main will be linked in because -# only applications need main(). -# Otherwise, it is assumed you are building an application and this -# module will attempt to locate and set the the proper link flags -# as part of the returned SDL2_LIBRARIES variable. -# -# Don't forget to include SDL2main.h and SDL2main.m your project for the -# OS X framework based version. (Other versions link to -lSDL2main which -# this module will try to find on your behalf.) Also for OS X, this -# module will automatically add the -framework Cocoa on your behalf. -# -# -# Modified by Eric Wing. -# Added code to assist with automated building by using environmental variables -# and providing a more controlled/consistent search behavior. -# Added new modifications to recognize OS X frameworks and -# additional Unix paths (FreeBSD, etc). -# Also corrected the header search path to follow "proper" SDL2 guidelines. -# Added a search for SDL2main which is needed by some platforms. -# Added a search for threads which is needed by some platforms. -# Added needed compile switches for MinGW. -# -# On OSX, this will prefer the Framework version (if found) over others. -# People will have to manually change the cache values of -# SDL2_LIBRARY to override this selection or set the CMake environment -# CMAKE_INCLUDE_PATH to modify the search paths. -# -# Note that the header path has changed from SDL2/SDL.h to just SDL.h -# This needed to change because "proper" SDL2 convention -# is #include "SDL.h", not . This is done for portability -# reasons because not all systems place things in SDL2/ (see FreeBSD). -# -# Ported by Johnny Patterson. This is a literal port for SDL2 of the FindSDL.cmake -# module with the minor edit of changing "SDL" to "SDL2" where necessary. This -# was not created for redistribution, and exists temporarily pending official -# SDL2 CMake modules. - -#============================================================================= -# Copyright 2003-2009 Kitware, Inc. -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distribute this file outside of CMake, substitute the full -# License text for the above reference.) - - -if (CMAKE_SIZEOF_VOID_P EQUAL 8) - set(_sdl_lib_suffix lib/x64) -else() - set(_sdl_lib_suffix lib/x86) -endif() - -libfind_pkg_detect(SDL2 sdl2 - FIND_PATH SDL.h - HINTS $ENV{SDL2DIR} - PATH_SUFFIXES include SDL2 include/SDL2 - FIND_LIBRARY SDL2 - HINTS $ENV{SDL2DIR} - PATH_SUFFIXES lib ${_sdl_lib_suffix} -) -libfind_version_n_header(SDL2 NAMES SDL_version.h DEFINES SDL_MAJOR_VERSION SDL_MINOR_VERSION SDL_PATCHLEVEL) - -IF(NOT SDL2_BUILDING_LIBRARY AND NOT APPLE) - # Non-OS X framework versions expect you to also dynamically link to - # SDL2main. This is mainly for Windows and OS X. Other (Unix) platforms - # seem to provide SDL2main for compatibility even though they don't - # necessarily need it. - libfind_pkg_detect(SDL2MAIN sdl2 - FIND_LIBRARY SDL2main - HINTS $ENV{SDL2DIR} - PATH_SUFFIXES lib ${_sdl_lib_suffix} - ) - set(SDL2MAIN_FIND_QUIETLY TRUE) - libfind_process(SDL2MAIN) - list(APPEND SDL2_PROCESS_LIBS SDL2MAIN_LIBRARY) -ENDIF() - - -set(SDL2_TARGET_SPECIFIC) - -if (APPLE) - # For OS X, SDL2 uses Cocoa as a backend so it must link to Cocoa. - list(APPEND SDL2_TARGET_SPECIFIC "-framework Cocoa") -else() - # SDL2 may require threads on your system. - # The Apple build may not need an explicit flag because one of the - # frameworks may already provide it. - # But for non-OSX systems, I will use the CMake Threads package. - libfind_package(SDL2 Threads) - list(APPEND SDL2_TARGET_SPECIFIC ${CMAKE_THREAD_LIBS_INIT}) -endif() - -# MinGW needs an additional library, mwindows -# It's total link flags should look like -lmingw32 -lSDL2main -lSDL2 -lmwindows -# (Actually on second look, I think it only needs one of the m* libraries.) -if(MINGW) - list(APPEND SDL2_TARGET_SPECIFIC mingw32) -endif() - -if(WIN32) - list(APPEND SDL2_TARGET_SPECIFIC winmm imm32 version msimg32) -endif() - -set(SDL2_PROCESS_LIBS SDL2_TARGET_SPECIFIC) - -libfind_process(SDL2) - -if (SDL2_STATIC AND UNIX AND NOT APPLE) - execute_process(COMMAND sdl2-config --static-libs OUTPUT_VARIABLE SDL2_STATIC_FLAGS) - string(REGEX REPLACE "(\r?\n)+$" "" SDL2_STATIC_FLAGS "${SDL2_STATIC_FLAGS}") - set(SDL2_LIBRARIES ${SDL2_STATIC_FLAGS}) -endif() diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 36bd74d217..f25a4cc621 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -26,6 +26,7 @@ if (GIT_CHECKOUT) -DOPENMW_VERSION_MINOR=${OPENMW_VERSION_MINOR} -DOPENMW_VERSION_RELEASE=${OPENMW_VERSION_RELEASE} -DOPENMW_LUA_API_REVISION=${OPENMW_LUA_API_REVISION} + -DOPENMW_POSTPROCESSING_API_REVISION=${OPENMW_POSTPROCESSING_API_REVISION} -DOPENMW_VERSION=${OPENMW_VERSION} -DMACROSFILE=${CMAKE_SOURCE_DIR}/cmake/OpenMWMacros.cmake "-DCMAKE_CONFIGURATION_TYPES=${CMAKE_CONFIGURATION_TYPES}" @@ -43,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 @@ -87,6 +88,8 @@ add_component_dir (settings settingvalue shadermanager values + screenshotsettings + windowmode ) add_component_dir (bsa @@ -127,7 +130,7 @@ add_component_dir (nif ) add_component_dir (nifosg - nifloader controller particle matrixtransform + nifloader controller particle matrixtransform fog ) add_component_dir (nifbullet @@ -163,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 @@ -270,7 +273,7 @@ add_component_dir (esm4 ) add_component_dir (misc - barrier budgetmeasurement color compression constants convert coordinateconverter endianness float16 frameratelimiter + barrier budgetmeasurement color compression constants convert coordinateconverter display endianness float16 frameratelimiter guarded math mathutil messageformatparser notnullptr objectpool osguservalues progressreporter resourcehelpers rng strongtypedef thread timeconvert timer tuplehelpers tuplemeta utf8stream weakcache windows ) @@ -336,7 +339,15 @@ add_component_dir (fontloader ) add_component_dir (sdlutil - gl4es_init sdlgraphicswindow imagetosurface sdlinputwrapper sdlvideowrapper events sdlcursormanager sdlmappings + events + gl4es_init + imagetosurface + sdlcursormanager + sdlgraphicswindow + sdlinputwrapper + sdlmappings + sdlvideowrapper + vsyncmode ) add_component_dir (version @@ -479,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) @@ -545,7 +556,7 @@ target_link_libraries(components ${Boost_PROGRAM_OPTIONS_LIBRARY} ${Boost_IOSTREAMS_LIBRARY} - ${SDL2_LIBRARIES} + SDL2::SDL2 ${OPENGL_gl_LIBRARY} ${MyGUI_LIBRARIES} ${LUA_LIBRARIES} 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..9b7bb11f09 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -164,20 +165,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(); } @@ -494,7 +489,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 +505,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; } diff --git a/components/contentselector/model/esmfile.cpp b/components/contentselector/model/esmfile.cpp index 741ed173a2..39a33e710e 100644 --- a/components/contentselector/model/esmfile.cpp +++ b/components/contentselector/model/esmfile.cpp @@ -4,13 +4,6 @@ #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) @@ -33,7 +26,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; } @@ -58,7 +51,7 @@ QByteArray ContentSelectorModel::EsmFile::encodedData() const QByteArray encodedData; QDataStream stream(&encodedData, QIODevice::WriteOnly); - stream << mFileName << mAuthor << QString::number(mVersion) << mModified.toString() << mPath << mDescription + stream << mFileName << mAuthor << mVersion << mModified.toString(Qt::ISODate) << mPath << mDescription << mGameFiles; return encodedData; @@ -121,11 +114,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..a65c778294 100644 --- a/components/contentselector/model/esmfile.hpp +++ b/components/contentselector/model/esmfile.hpp @@ -40,7 +40,7 @@ namespace ContentSelectorModel 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 +51,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 +59,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) @@ -72,13 +72,20 @@ namespace ContentSelectorModel 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..3f75b82487 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,12 +100,12 @@ 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); buildContextMenu(); @@ -83,10 +113,10 @@ void ContentSelectorView::ContentSelector::buildAddonView() 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 +146,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 +156,7 @@ void ContentSelectorView::ContentSelector::setGameFile(const QString& filename) } } - ui.gameFileView->setCurrentIndex(index); + ui->gameFileView->setCurrentIndex(index); } void ContentSelectorView::ContentSelector::clearCheckStates() @@ -143,7 +173,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 +194,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 +224,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 +255,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 +283,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 +314,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 +329,5 @@ void ContentSelectorView::ContentSelector::slotCopySelectedItemsPaths() void ContentSelectorView::ContentSelector::slotSearchFilterTextChanged(const QString& newText) { - ui.addonView->setDragEnabled(newText.isEmpty()); + ui->addonView->setDragEnabled(newText.isEmpty()); } diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp index 5919d2e516..48377acb86 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(); diff --git a/components/detournavigator/findsmoothpath.hpp b/components/detournavigator/findsmoothpath.hpp index b038226797..e5efa8815f 100644 --- a/components/detournavigator/findsmoothpath.hpp +++ b/components/detournavigator/findsmoothpath.hpp @@ -16,55 +16,55 @@ #include #include #include +#include #include #include namespace DetourNavigator { - template - class OutputTransformIterator + template OutputIterator> + class FromNavMeshCoordinatesIterator { public: - explicit OutputTransformIterator(OutputIterator& impl, Function&& function) + using iterator_category = std::output_iterator_tag; + using value_type = osg::Vec3f; + using difference_type = std::ptrdiff_t; + using pointer = osg::Vec3f*; + using reference = osg::Vec3f&; + + explicit FromNavMeshCoordinatesIterator(OutputIterator& impl, const RecastSettings& settings) : mImpl(impl) - , mFunction(std::forward(function)) + , mSettings(settings) { } - OutputTransformIterator& operator*() { return *this; } + FromNavMeshCoordinatesIterator& operator*() { return *this; } - OutputTransformIterator& operator++() + FromNavMeshCoordinatesIterator& operator++() { ++mImpl.get(); return *this; } - OutputTransformIterator operator++(int) + FromNavMeshCoordinatesIterator operator++(int) { const auto copy = *this; ++(*this); return copy; } - OutputTransformIterator& operator=(const osg::Vec3f& value) + FromNavMeshCoordinatesIterator& operator=(const osg::Vec3f& value) { - *mImpl.get() = mFunction(value); + *mImpl.get() = fromNavMeshCoordinates(mSettings, value); return *this; } private: std::reference_wrapper mImpl; - Function mFunction; + std::reference_wrapper mSettings; }; - template - auto withFromNavMeshCoordinates(OutputIterator& impl, const RecastSettings& settings) - { - return OutputTransformIterator( - impl, [&settings](const osg::Vec3f& value) { return fromNavMeshCoordinates(settings, value); }); - } - - inline std::optional findPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef, + inline std::optional findPolygonPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef, const dtPolyRef endRef, const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& queryFilter, std::span pathBuffer) { @@ -78,10 +78,9 @@ namespace DetourNavigator return static_cast(pathLen); } - template Status makeSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& start, const osg::Vec3f& end, std::span polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, - OutputIterator& out) + std::output_iterator auto& out) { assert(polygonPathSize <= polygonPath.size()); @@ -102,10 +101,9 @@ namespace DetourNavigator return Status::Success; } - template Status findSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, const DetourSettings& settings, - float endTolerance, OutputIterator out) + float endTolerance, std::output_iterator auto out) { dtQueryFilter queryFilter; queryFilter.setIncludeFlags(includeFlags); @@ -134,7 +132,7 @@ namespace DetourNavigator std::vector polygonPath(settings.mMaxPolygonPathSize); const auto polygonPathSize - = findPath(navMeshQuery, startRef, endRef, startNavMeshPos, endNavMeshPos, queryFilter, polygonPath); + = findPolygonPath(navMeshQuery, startRef, endRef, startNavMeshPos, endNavMeshPos, queryFilter, polygonPath); if (!polygonPathSize.has_value()) return Status::FindPathOverPolygonsFailed; diff --git a/components/detournavigator/navigatorutils.cpp b/components/detournavigator/navigatorutils.cpp index cad201825c..2f9fbbb32e 100644 --- a/components/detournavigator/navigatorutils.cpp +++ b/components/detournavigator/navigatorutils.cpp @@ -1,8 +1,11 @@ #include "navigatorutils.hpp" +#include "debug.hpp" #include "findrandompointaroundcircle.hpp" #include "navigator.hpp" #include "raycast.hpp" +#include + namespace DetourNavigator { std::optional findRandomPointAroundCircle(const Navigator& navigator, const AgentBounds& agentBounds, @@ -37,4 +40,41 @@ namespace DetourNavigator return std::nullopt; return fromNavMeshCoordinates(settings.mRecast, *result); } + + std::optional findNearestNavMeshPosition(const Navigator& navigator, const AgentBounds& agentBounds, + const osg::Vec3f& position, const osg::Vec3f& searchAreaHalfExtents, const Flags includeFlags) + { + const auto navMesh = navigator.getNavMesh(agentBounds); + if (navMesh == nullptr) + return std::nullopt; + + const auto& settings = navigator.getSettings(); + const osg::Vec3f navMeshPosition = toNavMeshCoordinates(settings.mRecast, position); + const auto lockedNavMesh = navMesh->lockConst(); + + dtNavMeshQuery navMeshQuery; + if (const dtStatus status + = navMeshQuery.init(&lockedNavMesh->getImpl(), settings.mDetour.mMaxNavMeshQueryNodes); + dtStatusFailed(status)) + { + Log(Debug::Error) << "Failed to init dtNavMeshQuery for findNearestNavMeshPosition: " + << WriteDtStatus{ status }; + return std::nullopt; + } + + dtQueryFilter queryFilter; + queryFilter.setIncludeFlags(includeFlags); + + osg::Vec3f nearestNavMeshPos; + const osg::Vec3f endPolyHalfExtents = toNavMeshCoordinates(settings.mRecast, searchAreaHalfExtents); + dtPolyRef polyRef; + if (const dtStatus status = navMeshQuery.findNearestPoly( + navMeshPosition.ptr(), endPolyHalfExtents.ptr(), &queryFilter, &polyRef, nearestNavMeshPos.ptr()); + dtStatusFailed(status) || polyRef == 0) + { + return std::nullopt; + } + + return fromNavMeshCoordinates(settings.mRecast, nearestNavMeshPos); + } } diff --git a/components/detournavigator/navigatorutils.hpp b/components/detournavigator/navigatorutils.hpp index 927066b6bb..ca02682ecd 100644 --- a/components/detournavigator/navigatorutils.hpp +++ b/components/detournavigator/navigatorutils.hpp @@ -9,34 +9,31 @@ #include +#include #include namespace DetourNavigator { /** * @brief findPath fills output iterator with points of scene surfaces to be used for actor to walk through. - * @param agentBounds allows to find navmesh for given actor. + * @param agentBounds defines which navmesh to use. * @param start path from given point. * @param end path at given point. - * @param includeFlags setup allowed surfaces for actor to walk. + * @param includeFlags setup allowed navmesh areas. * @param out the beginning of the destination range. * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents - * @return Output iterator to the element in the destination range, one past the last element of found path. + * @return Status. * Equal to out if no path is found. */ - template inline Status findPath(const Navigator& navigator, const AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, float endTolerance, - OutputIterator out) + std::output_iterator auto out) { - static_assert(std::is_same::iterator_category, - std::output_iterator_tag>::value, - "out is not an OutputIterator"); const auto navMesh = navigator.getNavMesh(agentBounds); if (navMesh == nullptr) return Status::NavMeshNotFound; const Settings& settings = navigator.getSettings(); - auto outTransform = withFromNavMeshCoordinates(out, settings.mRecast); + FromNavMeshCoordinatesIterator outTransform(out, settings.mRecast); const auto locked = navMesh->lock(); return findSmoothPath(locked->getQuery(), toNavMeshCoordinates(settings.mRecast, agentBounds.mHalfExtents), toNavMeshCoordinates(settings.mRecast, start), toNavMeshCoordinates(settings.mRecast, end), includeFlags, @@ -45,10 +42,10 @@ namespace DetourNavigator /** * @brief findRandomPointAroundCircle returns random location on navmesh within the reach of specified location. - * @param agentBounds allows to find navmesh for given actor. + * @param agentBounds defines which navmesh to use. * @param start is a position where the search starts. * @param maxRadius limit maximum distance from start. - * @param includeFlags setup allowed surfaces for actor to walk. + * @param includeFlags setup allowed navmesh areas. * @return not empty optional with position if point is found and empty optional if point is not found. */ std::optional findRandomPointAroundCircle(const Navigator& navigator, const AgentBounds& agentBounds, @@ -56,14 +53,25 @@ namespace DetourNavigator /** * @brief raycast finds farest navmesh point from start on a line from start to end that has path from start. - * @param agentBounds allows to find navmesh for given actor. + * @param agentBounds defines which navmesh to use. * @param start of the line * @param end of the line - * @param includeFlags setup allowed surfaces for actor to walk. + * @param includeFlags setup allowed navmesh areas. * @return not empty optional with position if point is found and empty optional if point is not found. */ std::optional raycast(const Navigator& navigator, const AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags); + + /** + * @brief findNearestNavMeshPosition finds nearest position on navmesh within given area having given flags. + * @param agentBounds defines which navmesh to use. + * @param position is a center of the search area. + * @param searchAreaHalfExtents defines AABB like area around given postion. + * @param includeFlags setup allowed navmesh areas. + * @return not empty optional with position if position is found and empty optional if position is not found. + */ + std::optional findNearestNavMeshPosition(const Navigator& navigator, const AgentBounds& agentBounds, + const osg::Vec3f& position, const osg::Vec3f& searchAreaHalfExtents, const Flags includeFlags); } #endif diff --git a/components/detournavigator/navmeshmanager.cpp b/components/detournavigator/navmeshmanager.cpp index 5c93aee5e7..9fda0566d9 100644 --- a/components/detournavigator/navmeshmanager.cpp +++ b/components/detournavigator/navmeshmanager.cpp @@ -177,7 +177,6 @@ namespace DetourNavigator const std::map& changedTiles) { std::map tilesToPost = changedTiles; - std::map tilesToPost1; { const auto locked = cached->lockConst(); const auto& navMesh = locked->getImpl(); 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/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/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/esm/records.hpp b/components/esm/records.hpp index 4473e0290f..ded4b92a92 100644 --- a/components/esm/records.hpp +++ b/components/esm/records.hpp @@ -59,6 +59,7 @@ #include #include #include +#include #include #include #include diff --git a/components/esm3/aipackage.hpp b/components/esm3/aipackage.hpp index 61aea2750a..7346a4af36 100644 --- a/components/esm3/aipackage.hpp +++ b/components/esm3/aipackage.hpp @@ -16,10 +16,10 @@ namespace ESM struct AIData { - unsigned short mHello; // This is the base value for greeting distance [0, 65535] + uint16_t mHello; // This is the base value for greeting distance [0, 65535] unsigned char mFight, mFlee, mAlarm; // These are probabilities [0, 100] char mU1, mU2, mU3; // Unknown values - int mServices; // See the Services enum + int32_t mServices; // See the Services enum void blank(); ///< Set record to default state (does not touch the ID). @@ -27,8 +27,8 @@ namespace ESM struct AIWander { - short mDistance; - short mDuration; + int16_t mDistance; + int16_t mDuration; unsigned char mTimeOfDay; unsigned char mIdle[8]; unsigned char mShouldRepeat; @@ -44,7 +44,7 @@ namespace ESM struct AITarget { float mX, mY, mZ; - short mDuration; + int16_t mDuration; NAME32 mId; unsigned char mShouldRepeat; unsigned char mPadding; diff --git a/components/esm3/aisequence.cpp b/components/esm3/aisequence.cpp index 668b36c871..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"); } @@ -196,7 +191,7 @@ namespace ESM int count = 0; while (esm.isNextSub("AIPK")) { - int type; + int32_t type; esm.getHT(type); mPackages.emplace_back(); @@ -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 10ab4505ce..84b6ae1d18 100644 --- a/components/esm3/cellref.hpp +++ b/components/esm3/cellref.hpp @@ -60,13 +60,12 @@ namespace ESM int32_t mChargeInt; // Used by everything except lights float mChargeFloat; // Used only by lights }; - float mChargeIntRemainder; // Stores amount of charge not subtracted from mChargeInt + float mChargeIntRemainder; // Fractional part of mChargeInt // 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/creaturelevliststate.hpp b/components/esm3/creaturelevliststate.hpp index f8fb7162ff..e7121cf8ac 100644 --- a/components/esm3/creaturelevliststate.hpp +++ b/components/esm3/creaturelevliststate.hpp @@ -9,7 +9,7 @@ namespace ESM struct CreatureLevListState final : public ObjectState { - int mSpawnActorId; + int32_t mSpawnActorId; bool mSpawn; void load(ESMReader& esm) override; 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/creaturestats.hpp b/components/esm3/creaturestats.hpp index 63ba49cdaa..6e65a52354 100644 --- a/components/esm3/creaturestats.hpp +++ b/components/esm3/creaturestats.hpp @@ -47,9 +47,8 @@ namespace ESM std::vector mSummonGraveyard; TimeStamp mTradeTime; - int mGoldPool; - int mActorId; - // int mHitAttemptActorId; + int32_t mGoldPool; + int32_t mActorId; enum Flags { diff --git a/components/esm3/debugprofile.hpp b/components/esm3/debugprofile.hpp index fc48fb23f6..a86e84bfd5 100644 --- a/components/esm3/debugprofile.hpp +++ b/components/esm3/debugprofile.hpp @@ -24,14 +24,14 @@ namespace ESM Flag_Global = 4 // make available from main menu (i.e. not location specific) }; - unsigned int mRecordFlags; + uint32_t mRecordFlags; RefId mId; std::string mDescription; std::string mScriptText; - unsigned int mFlags; + uint32_t mFlags; void load(ESMReader& esm, bool& isDeleted); void save(ESMWriter& esm, bool isDeleted = false) const; 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/doorstate.hpp b/components/esm3/doorstate.hpp index 5298327707..c23ffd5ad2 100644 --- a/components/esm3/doorstate.hpp +++ b/components/esm3/doorstate.hpp @@ -9,7 +9,7 @@ namespace ESM struct DoorState final : public ObjectState { - int mDoorState = 0; + int32_t mDoorState = 0; void load(ESMReader& esm) override; void save(ESMWriter& esm, bool inInventory = false) const override; 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/filter.hpp b/components/esm3/filter.hpp index c4642285af..6a978a2596 100644 --- a/components/esm3/filter.hpp +++ b/components/esm3/filter.hpp @@ -17,7 +17,7 @@ namespace ESM static constexpr std::string_view getRecordType() { return "Filter"; } - unsigned int mRecordFlags; + uint32_t mRecordFlags; RefId mId; std::string mDescription; 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 e58d0335bf..1947be23e9 100644 --- a/components/esm3/inventorystate.cpp +++ b/components/esm3/inventorystate.cpp @@ -7,40 +7,32 @@ namespace ESM { + namespace + { + constexpr uint32_t sInvalidSlot = static_cast(-1); + } void InventoryState::load(ESMReader& esm) { // obsolete - int index = 0; while (esm.isNextSub("IOBJ")) { - int unused; // no longer used - esm.getHT(unused); + esm.skipHT(); ObjectState state; - // obsolete - if (esm.isNextSub("SLOT")) - { - int 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; } - int itemsCount = 0; + uint32_t itemsCount = 0; esm.getHNOT(itemsCount, "ICNT"); - for (int i = 0; i < itemsCount; i++) + for (; itemsCount > 0; --itemsCount) { ObjectState state; @@ -51,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(); - int count; - std::string parentGroup; + int32_t count; // 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")) @@ -91,9 +80,9 @@ namespace ESM while (esm.isNextSub("EQUI")) { esm.getSubHeader(); - int equipIndex; + int32_t equipIndex; esm.getT(equipIndex); - int slot; + int32_t slot; esm.getT(slot); mEquipmentSlots[equipIndex] = slot; } @@ -101,38 +90,42 @@ namespace ESM if (esm.isNextSub("EQIP")) { esm.getSubHeader(); - int slotsCount = 0; + uint32_t slotsCount = 0; esm.getT(slotsCount); - for (int i = 0; i < slotsCount; i++) + for (; slotsCount > 0; --slotsCount) { - int equipIndex; + int32_t equipIndex; esm.getT(equipIndex); - int slot; + int32_t slot; esm.getT(slot); mEquipmentSlots[equipIndex] = slot; } } - mSelectedEnchantItem = -1; - esm.getHNOT(mSelectedEnchantItem, "SELE"); + uint32_t selectedEnchantItem = sInvalidSlot; + esm.getHNOT(selectedEnchantItem, "SELE"); + if (selectedEnchantItem == sInvalidSlot) + mSelectedEnchantItem.reset(); + else + mSelectedEnchantItem = selectedEnchantItem; // 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; } } } void InventoryState::save(ESMWriter& esm) const { - int itemsCount = static_cast(mItems.size()); + uint32_t itemsCount = static_cast(mItems.size()); if (itemsCount > 0) { esm.writeHNT("ICNT", itemsCount); @@ -142,41 +135,32 @@ namespace ESM } } - for (auto it = mLevelledItemMap.begin(); it != mLevelledItemMap.end(); ++it) + for (const auto& [id, params] : mPermanentMagicEffectMagnitudes) { - esm.writeHNRefId("LEVM", it->first.first); - esm.writeHNT("COUN", it->second); - esm.writeHNString("LGRP", it->first.second); - } + esm.writeHNRefId("MAGI", id); - for (TEffectMagnitudes::const_iterator it = mPermanentMagicEffectMagnitudes.begin(); - it != mPermanentMagicEffectMagnitudes.end(); ++it) - { - esm.writeHNRefId("MAGI", it->first); - - const std::vector>& params = it->second; - for (std::vector>::const_iterator pIt = params.begin(); pIt != params.end(); ++pIt) + for (const auto& [rand, mult] : params) { - esm.writeHNT("RAND", pIt->first); - esm.writeHNT("MULT", pIt->second); + esm.writeHNT("RAND", rand); + esm.writeHNT("MULT", mult); } } - int slotsCount = static_cast(mEquipmentSlots.size()); + uint32_t slotsCount = static_cast(mEquipmentSlots.size()); if (slotsCount > 0) { esm.startSubRecord("EQIP"); esm.writeT(slotsCount); - for (std::map::const_iterator it = mEquipmentSlots.begin(); it != mEquipmentSlots.end(); ++it) + for (const auto& [index, slot] : mEquipmentSlots) { - esm.writeT(it->first); - esm.writeT(it->second); + esm.writeT(index); + esm.writeT(slot); } esm.endRecord("EQIP"); } - if (mSelectedEnchantItem != -1) - esm.writeHNT("SELE", mSelectedEnchantItem); + if (mSelectedEnchantItem) + esm.writeHNT("SELE", *mSelectedEnchantItem); } } diff --git a/components/esm3/inventorystate.hpp b/components/esm3/inventorystate.hpp index a0fd948951..814236ce46 100644 --- a/components/esm3/inventorystate.hpp +++ b/components/esm3/inventorystate.hpp @@ -2,6 +2,7 @@ #define OPENMW_ESM_INVENTORYSTATE_H #include +#include #include "objectstate.hpp" #include @@ -19,20 +20,13 @@ namespace ESM std::vector mItems; // - std::map mEquipmentSlots; + std::map mEquipmentSlots; - std::map, int> mLevelledItemMap; + std::map>> mPermanentMagicEffectMagnitudes; - typedef std::map>> TEffectMagnitudes; - TEffectMagnitudes mPermanentMagicEffectMagnitudes; + std::optional mSelectedEnchantItem; // For inventories only - int mSelectedEnchantItem; // For inventories only - - InventoryState() - : mSelectedEnchantItem(-1) - { - } - virtual ~InventoryState() {} + virtual ~InventoryState() = default; virtual void load(ESMReader& esm); virtual void save(ESMWriter& esm) const; diff --git a/components/esm3/loadcell.cpp b/components/esm3/loadcell.cpp index b966338ae5..829cf9e916 100644 --- a/components/esm3/loadcell.cpp +++ b/components/esm3/loadcell.cpp @@ -17,7 +17,7 @@ namespace ESM ///< Translate 8bit/24bit code (stored in refNum.mIndex) into a proper refNum void adjustRefNum(RefNum& refNum, const ESMReader& reader) { - unsigned int local = (refNum.mIndex & 0xff000000) >> 24; + uint32_t local = (refNum.mIndex & 0xff000000) >> 24; // If we have an index value that does not make sense, assume that it was an addition // by the present plugin (but a faulty one) @@ -124,7 +124,7 @@ namespace ESM switch (esm.retSubName().toInt()) { case fourCC("INTV"): - int waterl; + int32_t waterl; esm.getHT(waterl); mWater = static_cast(waterl); mWaterInt = true; @@ -192,7 +192,7 @@ namespace ESM { if (mWaterInt) { - int water = (mWater >= 0) ? (int)(mWater + 0.5) : (int)(mWater - 0.5); + int32_t water = (mWater >= 0) ? static_cast(mWater + 0.5) : static_cast(mWater - 0.5); esm.writeHNT("INTV", water); } else @@ -218,13 +218,13 @@ namespace ESM } } - void Cell::saveTempMarker(ESMWriter& esm, int tempCount) const + void Cell::saveTempMarker(ESMWriter& esm, int32_t tempCount) const { if (tempCount != 0) esm.writeHNT("NAM0", tempCount); } - void Cell::restore(ESMReader& esm, int iCtx) const + void Cell::restore(ESMReader& esm, size_t iCtx) const { esm.restoreContext(mContextList.at(iCtx)); } @@ -321,7 +321,7 @@ namespace ESM void Cell::blank() { - mName = ""; + mName.clear(); mRegion = ESM::RefId(); mWater = 0; mWaterInt = false; diff --git a/components/esm3/loadcell.hpp b/components/esm3/loadcell.hpp index 0ba0777e7c..bfabdd58f9 100644 --- a/components/esm3/loadcell.hpp +++ b/components/esm3/loadcell.hpp @@ -34,7 +34,7 @@ namespace ESM RefNum mRefNum; // Coordinates of target exterior cell - int mTarget[2]; + int32_t mTarget[2]; // The content file format does not support moving objects to an interior cell. // The save game format does support moving to interior cells, but uses a different mechanism @@ -153,13 +153,13 @@ namespace ESM ESMReader& esm, bool saveContext = true); // Load everything, except NAME, DATAstruct and references void save(ESMWriter& esm, bool isDeleted = false) const; - void saveTempMarker(ESMWriter& esm, int tempCount) const; + void saveTempMarker(ESMWriter& esm, int32_t tempCount) const; bool isExterior() const { return !(mData.mFlags & Interior); } - int getGridX() const { return mData.mX; } + int32_t getGridX() const { return mData.mX; } - int getGridY() const { return mData.mY; } + int32_t getGridY() const { return mData.mY; } bool hasWater() const { return ((mData.mFlags & HasWater) != 0) || isExterior(); } @@ -172,7 +172,7 @@ namespace ESM // somewhere other than the file system, you need to pre-open the // ESMReader, and the filename must match the stored filename // exactly. - void restore(ESMReader& esm, int iCtx) const; + void restore(ESMReader& esm, size_t iCtx) const; std::string getDescription() const; ///< Return a short string describing the cell (mostly used for debugging/logging purpose) diff --git a/components/esm3/loadclas.cpp b/components/esm3/loadclas.cpp index 0c58f1d45a..ec4ff680fa 100644 --- a/components/esm3/loadclas.cpp +++ b/components/esm3/loadclas.cpp @@ -10,6 +10,7 @@ namespace ESM { const std::string_view Class::sGmstSpecializationIds[3] = { "sSpecializationCombat", "sSpecializationMagic", "sSpecializationStealth" }; + const std::array Class::specializationIndexToLuaId = { "combat", "magic", "stealth" }; int32_t& Class::CLDTstruct::getSkill(int index, bool major) { diff --git a/components/esm3/loadclas.hpp b/components/esm3/loadclas.hpp index 47b8b73b7e..a804a8da8e 100644 --- a/components/esm3/loadclas.hpp +++ b/components/esm3/loadclas.hpp @@ -32,6 +32,7 @@ namespace ESM }; static const std::string_view sGmstSpecializationIds[3]; + static const std::array specializationIndexToLuaId; struct CLDTstruct { diff --git a/components/esm3/loadcont.hpp b/components/esm3/loadcont.hpp index 3921821da0..3c1fb6468e 100644 --- a/components/esm3/loadcont.hpp +++ b/components/esm3/loadcont.hpp @@ -19,7 +19,7 @@ namespace ESM struct ContItem { - int mCount{ 0 }; + int32_t mCount{ 0 }; ESM::RefId mItem; }; @@ -48,12 +48,12 @@ namespace ESM Unknown = 8 }; - unsigned int mRecordFlags; + uint32_t mRecordFlags; RefId mId, mScript; std::string mName, mModel; float mWeight; // Not sure, might be max total weight allowed? - int mFlags; + int32_t mFlags; InventoryList mInventory; void load(ESMReader& esm, bool& isDeleted); 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/loadglob.hpp b/components/esm3/loadglob.hpp index eed53a5f7b..c78bc83917 100644 --- a/components/esm3/loadglob.hpp +++ b/components/esm3/loadglob.hpp @@ -25,7 +25,7 @@ namespace ESM /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string_view getRecordType() { return "Global"; } - unsigned int mRecordFlags; + uint32_t mRecordFlags; ESM::RefId mId; Variant mValue; diff --git a/components/esm3/loadgmst.hpp b/components/esm3/loadgmst.hpp index 72d30b9ea9..c827be5b6b 100644 --- a/components/esm3/loadgmst.hpp +++ b/components/esm3/loadgmst.hpp @@ -26,7 +26,7 @@ namespace ESM /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string_view getRecordType() { return "GameSetting"; } - unsigned int mRecordFlags; + uint32_t mRecordFlags; RefId mId; Variant mValue; diff --git a/components/esm3/loadlevlist.cpp b/components/esm3/loadlevlist.cpp index 970174ada2..627edbadce 100644 --- a/components/esm3/loadlevlist.cpp +++ b/components/esm3/loadlevlist.cpp @@ -30,7 +30,7 @@ namespace ESM break; case fourCC("INDX"): { - int length = 0; + uint32_t length = 0; esm.getHT(length); mList.resize(length); @@ -87,12 +87,12 @@ namespace ESM esm.writeHNT("DATA", mFlags); esm.writeHNT("NNAM", mChanceNone); - esm.writeHNT("INDX", mList.size()); + esm.writeHNT("INDX", mList.size()); - for (std::vector::const_iterator it = mList.begin(); it != mList.end(); ++it) + for (const auto& item : mList) { - esm.writeHNCRefId(recName, it->mId); - esm.writeHNT("INTV", it->mLevel); + esm.writeHNCRefId(recName, item.mId); + esm.writeHNT("INTV", item.mLevel); } } diff --git a/components/esm3/loadlevlist.hpp b/components/esm3/loadlevlist.hpp index 809c89102e..536b865d90 100644 --- a/components/esm3/loadlevlist.hpp +++ b/components/esm3/loadlevlist.hpp @@ -24,15 +24,15 @@ namespace ESM struct LevelledListBase { - int mFlags; + int32_t mFlags; unsigned char mChanceNone; // Chance that none are selected (0-100) - unsigned int mRecordFlags; + uint32_t mRecordFlags; RefId mId; struct LevelItem { RefId mId; - short mLevel; + uint16_t mLevel; }; std::vector mList; diff --git a/components/esm3/loadltex.hpp b/components/esm3/loadltex.hpp index e6dc2de73e..fb95e8b9ed 100644 --- a/components/esm3/loadltex.hpp +++ b/components/esm3/loadltex.hpp @@ -29,7 +29,7 @@ namespace ESM // mId is merely a user friendly name for the texture in the editor. std::string mTexture; RefId mId; - int mIndex; + int32_t mIndex; void load(ESMReader& esm, bool& isDeleted); void save(ESMWriter& esm, bool isDeleted = false) const; diff --git a/components/esm3/loadmgef.cpp b/components/esm3/loadmgef.cpp index 7d3879a341..686afbc34a 100644 --- a/components/esm3/loadmgef.cpp +++ b/components/esm3/loadmgef.cpp @@ -36,7 +36,7 @@ namespace ESM esm.getSubNameIs("MEDT"); esm.getSubHeader(); - int school; + int32_t school; esm.getT(school); mData.mSchool = MagicSchool::indexToSkillRefId(school); esm.getT(mData.mBaseCost); diff --git a/components/esm3/loadmgef.hpp b/components/esm3/loadmgef.hpp index 9d68bad609..25ec7d0655 100644 --- a/components/esm3/loadmgef.hpp +++ b/components/esm3/loadmgef.hpp @@ -25,7 +25,7 @@ namespace ESM /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string_view getRecordType() { return "MagicEffect"; } - unsigned int mRecordFlags; + uint32_t mRecordFlags; RefId mId; enum Flags @@ -74,9 +74,9 @@ namespace ESM { RefId mSchool; // Skill id float mBaseCost; - int mFlags; + int32_t mFlags; // Glow color for enchanted items with this effect - int mRed, mGreen, mBlue; + int32_t mRed, mGreen, mBlue; float mUnknown1; // Called "Size X" in CS float mSpeed; // Speed of fired projectile @@ -107,7 +107,7 @@ namespace ESM // there. They can be redefined in mods by setting the name in GMST // sEffectSummonCreature04/05 creature id in // sMagicCreature04ID/05ID. - int mIndex; + int32_t mIndex; void load(ESMReader& esm, bool& isDeleted); void save(ESMWriter& esm, bool isDeleted = false) const; diff --git a/components/esm3/loadnpc.cpp b/components/esm3/loadnpc.cpp index 24a79d2e4c..4a30649372 100644 --- a/components/esm3/loadnpc.cpp +++ b/components/esm3/loadnpc.cpp @@ -59,30 +59,38 @@ 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"); break; case fourCC("FLAG"): hasFlags = true; - int flags; + int32_t flags; esm.getHT(flags); mFlags = flags & 0xFF; mBloodType = ((flags >> 8) & 0xFF) >> 2; @@ -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 f0d726434b..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,36 +75,21 @@ namespace ESM NPC_DEFAULT = 52 }; -#pragma pack(push) -#pragma pack(1) - struct NPDTstruct52 { - short mLevel; - unsigned char mStrength, mIntelligence, mWillpower, mAgility, mSpeed, mEndurance, mPersonality, mLuck; + int16_t mLevel; + std::array mAttributes; // mSkill can grow up to 200, it must be unsigned std::array mSkills; char mUnknown1; - unsigned short mHealth, mMana, mFatigue; + uint16_t mHealth, mMana, mFatigue; unsigned char mDisposition, mReputation, mRank; char mUnknown2; - int mGold; + int32_t mGold; }; // 52 bytes - // Structure for autocalculated characters. - // This is only used for load and save operations. - struct NPDTstruct12 - { - short mLevel; - // see above - unsigned char mDisposition, mReputation, mRank; - char mUnknown1, mUnknown2, mUnknown3; - int 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 @@ -111,7 +97,7 @@ namespace ESM int getFactionRank() const; /// wrapper for mNpdt*, -1 = no rank - int mBloodType; + int32_t mBloodType; unsigned char mFlags; InventoryList mInventory; @@ -125,7 +111,7 @@ namespace ESM AIPackageList mAiPackage; - unsigned int mRecordFlags; + uint32_t mRecordFlags; RefId mId, mRace, mClass, mFaction, mScript; std::string mModel, mName; 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 8dd60bdef1..4493240ac8 100644 --- a/components/esm3/loadrace.hpp +++ b/components/esm3/loadrace.hpp @@ -27,22 +27,8 @@ namespace ESM struct SkillBonus { - int mSkill; // SkillEnum - int mBonus; - }; - - struct MaleFemale - { - int mMale, mFemale; - - int getValue(bool male) const; - }; - - struct MaleFemaleF - { - float mMale, mFemale; - - float getValue(bool male) const; + int32_t mSkill; // SkillEnum + int32_t mBonus; }; enum Flags @@ -57,19 +43,25 @@ 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; - int mFlags; // 0x1 - playable, 0x2 - beast race + 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; - unsigned int mRecordFlags; + uint32_t mRecordFlags; std::string mName, mDescription; RefId mId; SpellList mPowers; 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/loadsndg.hpp b/components/esm3/loadsndg.hpp index fff4b98439..3337220d9d 100644 --- a/components/esm3/loadsndg.hpp +++ b/components/esm3/loadsndg.hpp @@ -36,9 +36,9 @@ namespace ESM }; // Type - int mType; + int32_t mType; - unsigned int mRecordFlags; + uint32_t mRecordFlags; RefId mId, mCreature, mSound; void load(ESMReader& esm, bool& isDeleted); diff --git a/components/esm3/loadsscr.hpp b/components/esm3/loadsscr.hpp index 6c9163e4e6..34349d8ea4 100644 --- a/components/esm3/loadsscr.hpp +++ b/components/esm3/loadsscr.hpp @@ -28,7 +28,7 @@ namespace ESM static std::string_view getRecordType() { return "StartScript"; } std::string mData; - unsigned int mRecordFlags; + uint32_t mRecordFlags; RefId mId; // Load a record and add it to the list diff --git a/components/esm3/loadstat.hpp b/components/esm3/loadstat.hpp index abe589563b..4c0341f4ea 100644 --- a/components/esm3/loadstat.hpp +++ b/components/esm3/loadstat.hpp @@ -31,7 +31,7 @@ namespace ESM /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string_view getRecordType() { return "Static"; } - unsigned int mRecordFlags; + uint32_t mRecordFlags; RefId mId; std::string mModel; 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 7927b35cee..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,20 +12,13 @@ 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 - */ - unsigned int version; - int type; // 0=esp, 1=esm, 32=ess (unused) + ESM::ESMVersion version; + int32_t type; // 0=esp, 1=esm, 32=ess (unused) std::string author; // Author's name std::string desc; // File description - int records; // Number of records + int32_t records; // Number of records }; struct GMDT @@ -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/magiceffects.cpp b/components/esm3/magiceffects.cpp index 346cc2c568..a8a759949b 100644 --- a/components/esm3/magiceffects.cpp +++ b/components/esm3/magiceffects.cpp @@ -20,8 +20,8 @@ namespace ESM { while (esm.isNextSub("EFID")) { - int id; - std::pair params; + int32_t id; + std::pair params; esm.getHT(id); esm.getHNT(params.first, "BASE"); if (esm.getFormatVersion() <= MaxClearModifiersFormatVersion) diff --git a/components/esm3/magiceffects.hpp b/components/esm3/magiceffects.hpp index 3f141355c8..74a6e34743 100644 --- a/components/esm3/magiceffects.hpp +++ b/components/esm3/magiceffects.hpp @@ -16,7 +16,7 @@ namespace ESM struct MagicEffects { // - std::map> mEffects; + std::map> mEffects; void load(ESMReader& esm); void save(ESMWriter& esm) const; @@ -24,16 +24,16 @@ namespace ESM struct SummonKey { - SummonKey(int effectId, const ESM::RefId& sourceId, int index) + SummonKey(int32_t effectId, const ESM::RefId& sourceId, int32_t index) : mEffectId(effectId) , mSourceId(sourceId) , mEffectIndex(index) { } - int mEffectId; + int32_t mEffectId; ESM::RefId mSourceId; - int mEffectIndex; + int32_t mEffectIndex; }; inline auto makeTupleRef(const SummonKey& value) noexcept diff --git a/components/esm3/npcstats.cpp b/components/esm3/npcstats.cpp index c34205f6c2..371f506fb4 100644 --- a/components/esm3/npcstats.cpp +++ b/components/esm3/npcstats.cpp @@ -21,7 +21,7 @@ namespace ESM Faction faction; - int expelled = 0; + int32_t expelled = 0; esm.getHNOT(expelled, "FAEX"); if (expelled) @@ -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,40 +71,35 @@ 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"); } void NpcStats::save(ESMWriter& esm) const { - for (auto iter(mFactions.begin()); iter != mFactions.end(); ++iter) + for (const auto& [id, faction] : mFactions) { - esm.writeHNRefId("FACT", iter->first); + esm.writeHNRefId("FACT", id); - if (iter->second.mExpelled) + if (faction.mExpelled) { - int expelled = 1; + int32_t expelled = 1; esm.writeHNT("FAEX", expelled); } - if (iter->second.mRank >= 0) - esm.writeHNT("FARA", iter->second.mRank); + if (faction.mRank >= 0) + esm.writeHNT("FARA", faction.mRank); - if (iter->second.mReputation) - esm.writeHNT("FARE", iter->second.mReputation); + if (faction.mReputation) + esm.writeHNT("FARE", faction.mReputation); } if (mDisposition) esm.writeHNT("DISP", mDisposition); + if (mCrimeDispositionModifier) + esm.writeHNT("DISM", mCrimeDispositionModifier); + for (const auto& skill : mSkills) skill.save(esm); @@ -169,7 +119,7 @@ namespace ESM esm.writeHNT("LPRO", mLevelProgress); bool saveSkillIncreases = false; - for (int increase : mSkillIncrease) + for (int32_t increase : mSkillIncrease) { if (increase != 0) { @@ -183,8 +133,8 @@ namespace ESM if (mSpecIncreases[0] != 0 || mSpecIncreases[1] != 0 || mSpecIncreases[2] != 0) esm.writeHNT("SPEC", mSpecIncreases); - for (auto iter(mUsedIds.begin()); iter != mUsedIds.end(); ++iter) - esm.writeHNRefId("USED", *iter); + for (const RefId& id : mUsedIds) + esm.writeHNRefId("USED", id); if (mTimeToStartDrowning) esm.writeHNT("DRTI", mTimeToStartDrowning); @@ -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 e80ec04c25..b539602e06 100644 --- a/components/esm3/npcstats.hpp +++ b/components/esm3/npcstats.hpp @@ -23,28 +23,27 @@ namespace ESM struct Faction { bool mExpelled; - int mRank; - int mReputation; + int32_t mRank; + int32_t mReputation; Faction(); }; bool mIsWerewolf; - bool mWerewolfDeprecatedData; - - std::map mFactions; // lower case IDs - int mDisposition; + std::map mFactions; + int32_t mDisposition; + int32_t mCrimeDispositionModifier; std::array, ESM::Skill::Length> mSkills; - int mBounty; - int mReputation; - int mWerewolfKills; - int mLevelProgress; - std::array mSkillIncrease; - std::array mSpecIncreases; - std::vector mUsedIds; // lower case IDs + int32_t mBounty; + int32_t mReputation; + int32_t mWerewolfKills; + int32_t mLevelProgress; + std::array mSkillIncrease; + std::array mSpecIncreases; + std::vector mUsedIds; float mTimeToStartDrowning; - int mCrimeId; + int32_t mCrimeId; /// Initialize to default state void blank(); diff --git a/components/esm3/objectstate.cpp b/components/esm3/objectstate.cpp index a56988843a..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 - int 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; @@ -179,6 +164,6 @@ namespace ESM throw std::logic_error(error.str()); } - ObjectState::~ObjectState() {} + ObjectState::~ObjectState() = default; } diff --git a/components/esm3/objectstate.hpp b/components/esm3/objectstate.hpp index 67f4d27706..b3f7bd3d45 100644 --- a/components/esm3/objectstate.hpp +++ b/components/esm3/objectstate.hpp @@ -32,9 +32,8 @@ namespace ESM Locals mLocals; LuaScripts mLuaScripts; unsigned char mEnabled; - int mCount; Position mPosition; - unsigned int mFlags; + uint32_t mFlags; // Is there any class-specific state following the ObjectState bool mHasCustomState; @@ -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 f691f22e86..0f76a3b5eb 100644 --- a/components/esm3/player.hpp +++ b/components/esm3/player.hpp @@ -27,14 +27,13 @@ namespace ESM RefId mMarkedCell; ESM::RefId mBirthsign; - int mCurrentCrimeId; - int mPaidCrimeId; + int32_t mCurrentCrimeId; + int32_t mPaidCrimeId; 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/projectilestate.hpp b/components/esm3/projectilestate.hpp index 7a7651f364..cab550b114 100644 --- a/components/esm3/projectilestate.hpp +++ b/components/esm3/projectilestate.hpp @@ -23,7 +23,7 @@ namespace ESM Vector3 mPosition; Quaternion mOrientation; - int mActorId; + int32_t mActorId; void load(ESMReader& esm); void save(ESMWriter& esm) const; diff --git a/components/esm3/queststate.hpp b/components/esm3/queststate.hpp index 5858714df0..6d9fd6c4fb 100644 --- a/components/esm3/queststate.hpp +++ b/components/esm3/queststate.hpp @@ -14,7 +14,7 @@ namespace ESM struct QuestState { ESM::RefId mTopic; // lower case id - int mState; + int32_t mState; unsigned char mFinished; void load(ESMReader& esm); 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 e84cb27ad8..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"); @@ -61,4 +61,18 @@ namespace ESM esm.writeHNT("MHLT", mMaximumHealth); } + std::vector SavedGame::getMissingContentFiles( + const std::vector& allContentFiles) const + { + std::vector missingFiles; + for (const std::string& contentFile : mContentFiles) + { + if (std::find(allContentFiles.begin(), allContentFiles.end(), contentFile) == allContentFiles.end()) + { + missingFiles.emplace_back(contentFile); + } + } + + return missingFiles; + } } diff --git a/components/esm3/savedgame.hpp b/components/esm3/savedgame.hpp index 2048244ac2..f174340203 100644 --- a/components/esm3/savedgame.hpp +++ b/components/esm3/savedgame.hpp @@ -20,7 +20,7 @@ namespace ESM std::vector mContentFiles; std::string mPlayerName; - int mPlayerLevel; + int32_t mPlayerLevel; // ID of class ESM::RefId mPlayerClassId; @@ -34,12 +34,14 @@ namespace ESM std::string mDescription; std::vector mScreenshot; // raw jpg-encoded data - int mCurrentDay = 0; + int32_t mCurrentDay = 0; float mCurrentHealth = 0; float mMaximumHealth = 0; void load(ESMReader& esm); void save(ESMWriter& esm) const; + + std::vector getMissingContentFiles(const std::vector& allContentFiles) const; }; } 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 b5ddc54985..b46c2e34fd 100644 --- a/components/esm3/statstate.cpp +++ b/components/esm3/statstate.cpp @@ -21,21 +21,17 @@ namespace ESM // We changed stats values from integers to floats; ensure backwards compatibility if (intFallback) { - int base = 0; + int32_t base = 0; esm.getHNT(base, "STBA"); mBase = static_cast(base); - int mod = 0; + int32_t mod = 0; esm.getHNOT(mod, "STMO"); mMod = static_cast(mod); - int current = 0; + int32_t current = 0; esm.getHNOT(current, "STCU"); mCurrent = static_cast(current); - - int oldDamage = 0; - esm.getHNOT(oldDamage, "STDA"); - mDamage = static_cast(oldDamage); } else { diff --git a/components/esm3/variant.cpp b/components/esm3/variant.cpp index 3d5daa2cb3..48621818eb 100644 --- a/components/esm3/variant.cpp +++ b/components/esm3/variant.cpp @@ -17,7 +17,7 @@ namespace ESM template struct GetValue { - constexpr T operator()(int value) const { return static_cast(value); } + constexpr T operator()(int32_t value) const { return static_cast(value); } constexpr T operator()(float value) const { return static_cast(value); } @@ -41,7 +41,7 @@ namespace ESM { } - void operator()(int& value) const { value = static_cast(mValue); } + void operator()(int32_t& value) const { value = static_cast(mValue); } void operator()(float& value) const { value = static_cast(mValue); } @@ -58,9 +58,9 @@ namespace ESM return std::get(mData); } - int Variant::getInteger() const + int32_t Variant::getInteger() const { - return std::visit(GetValue{}, mData); + return std::visit(GetValue{}, mData); } float Variant::getFloat() const @@ -194,17 +194,17 @@ namespace ESM case VT_Short: - stream << "variant short: " << std::get(mData); + stream << "variant short: " << std::get(mData); break; case VT_Int: - stream << "variant int: " << std::get(mData); + stream << "variant int: " << std::get(mData); break; case VT_Long: - stream << "variant long: " << std::get(mData); + stream << "variant long: " << std::get(mData); break; case VT_Float: @@ -259,7 +259,7 @@ namespace ESM std::get(mData) = std::move(value); } - void Variant::setInteger(int value) + void Variant::setInteger(int32_t value) { std::visit(SetValue(value), mData); } diff --git a/components/esm3/variant.hpp b/components/esm3/variant.hpp index d00ccb2746..ed72b1dc05 100644 --- a/components/esm3/variant.hpp +++ b/components/esm3/variant.hpp @@ -25,7 +25,7 @@ namespace ESM class Variant { VarType mType; - std::variant mData; + std::variant mData; public: enum Format @@ -54,7 +54,7 @@ namespace ESM { } - explicit Variant(int value) + explicit Variant(int32_t value) : mType(VT_Long) , mData(value) { @@ -71,7 +71,7 @@ namespace ESM const std::string& getString() const; ///< Will throw an exception, if value can not be represented as a string. - int getInteger() const; + int32_t getInteger() const; ///< Will throw an exception, if value can not be represented as an integer (implicit /// casting of float values is permitted). @@ -93,7 +93,7 @@ namespace ESM void setString(std::string&& value); ///< Will throw an exception, if type is not compatible with string. - void setInteger(int value); + void setInteger(int32_t value); ///< Will throw an exception, if type is not compatible with integer. void setFloat(float value); diff --git a/components/esm3/variantimp.cpp b/components/esm3/variantimp.cpp index 410f4ade8d..31248556ec 100644 --- a/components/esm3/variantimp.cpp +++ b/components/esm3/variantimp.cpp @@ -46,7 +46,7 @@ namespace ESM esm.writeHNString("STRV", in); } - void readESMVariantValue(ESMReader& esm, Variant::Format format, VarType type, int& out) + void readESMVariantValue(ESMReader& esm, Variant::Format format, VarType type, int32_t& out) { if (type != VT_Short && type != VT_Long && type != VT_Int) throw std::logic_error("not an integer type"); @@ -60,9 +60,9 @@ namespace ESM if (std::isnan(value)) out = 0; else - out = static_cast(value); + out = static_cast(value); else if (type == VT_Long) - out = static_cast(value); + out = static_cast(value); else esm.fail("unsupported global variable integer type"); } @@ -82,7 +82,7 @@ namespace ESM { if (type == VT_Short) { - short value; + int16_t value; esm.getHT(value); out = value; } @@ -95,7 +95,7 @@ namespace ESM } } - void writeESMVariantValue(ESMWriter& esm, Variant::Format format, VarType type, int in) + void writeESMVariantValue(ESMWriter& esm, Variant::Format format, VarType type, int32_t in) { if (type != VT_Short && type != VT_Long && type != VT_Int) throw std::logic_error("not an integer type"); @@ -126,7 +126,7 @@ namespace ESM else if (format == Variant::Format_Local) { if (type == VT_Short) - esm.writeHNT("STTV", static_cast(in)); + esm.writeHNT("STTV", static_cast(in)); else if (type == VT_Int) esm.writeHNT("INTV", in); else diff --git a/components/esm3/variantimp.hpp b/components/esm3/variantimp.hpp index 90f45a420f..365cff988a 100644 --- a/components/esm3/variantimp.hpp +++ b/components/esm3/variantimp.hpp @@ -12,13 +12,13 @@ namespace ESM void readESMVariantValue(ESMReader& reader, Variant::Format format, VarType type, float& value); - void readESMVariantValue(ESMReader& reader, Variant::Format format, VarType type, int& value); + void readESMVariantValue(ESMReader& reader, Variant::Format format, VarType type, int32_t& value); void writeESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type, const std::string& value); void writeESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type, float value); - void writeESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type, int value); + void writeESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type, int32_t value); struct ReadESMVariantValue { 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/loadimod.cpp b/components/esm4/loadimod.cpp index 7382946e2c..0359f6d23b 100644 --- a/components/esm4/loadimod.cpp +++ b/components/esm4/loadimod.cpp @@ -46,16 +46,50 @@ void ESM4::ItemMod::load(ESM4::Reader& reader) case ESM4::SUB_EDID: reader.getZString(mEditorId); break; - case ESM4::SUB_OBND: case ESM4::SUB_FULL: + reader.getLocalizedString(mFullName); + break; case ESM4::SUB_MODL: - case ESM4::SUB_ICON: - case ESM4::SUB_MICO: - case ESM4::SUB_SCRI: + reader.getZString(mModel); + break; + case ESM4::SUB_MODB: + reader.get(mBoundRadius); + break; case ESM4::SUB_DESC: + reader.getLocalizedString(mText); + break; + case ESM4::SUB_ICON: + reader.getZString(mIcon); + break; + case ESM4::SUB_MICO: + reader.getZString(mMiniIcon); + break; + case ESM4::SUB_SCRI: + reader.getFormId(mScriptId); + break; case ESM4::SUB_YNAM: + reader.getFormId(mPickUpSound); + break; case ESM4::SUB_ZNAM: + reader.getFormId(mDropSound); + break; case ESM4::SUB_DATA: + reader.get(mData.mValue); + reader.get(mData.mWeight); + break; + case ESM4::SUB_OBND: + case ESM4::SUB_MODT: // Model data + case ESM4::SUB_MODS: + case ESM4::SUB_MODD: // Model data end + case ESM4::SUB_DAMC: // Destructible + case ESM4::SUB_DEST: + case ESM4::SUB_DMDC: + case ESM4::SUB_DMDL: + case ESM4::SUB_DMDT: + case ESM4::SUB_DMDS: + case ESM4::SUB_DSTA: + case ESM4::SUB_DSTD: + case ESM4::SUB_DSTF: // Destructible end reader.skipSubRecordData(); break; default: diff --git a/components/esm4/loadimod.hpp b/components/esm4/loadimod.hpp index f1faed978c..1f6250ae72 100644 --- a/components/esm4/loadimod.hpp +++ b/components/esm4/loadimod.hpp @@ -42,10 +42,29 @@ namespace ESM4 struct ItemMod { + struct Data + { + std::uint32_t mValue{ 0 }; + float mWeight{ 0.f }; + }; + ESM::FormId mId; // from the header std::uint32_t mFlags; // from the header, see enum type RecordFlag for details std::string mEditorId; + std::string mFullName; + std::string mModel; + std::string mText; + std::string mIcon; + std::string mMiniIcon; + + ESM::FormId mScriptId; + ESM::FormId mPickUpSound; + ESM::FormId mDropSound; + + float mBoundRadius; + + Data mData; void load(ESM4::Reader& reader); // void save(ESM4::Writer& writer) const; diff --git a/components/esm4/loadltex.cpp b/components/esm4/loadltex.cpp index 955ac938e3..9b2d12034f 100644 --- a/components/esm4/loadltex.cpp +++ b/components/esm4/loadltex.cpp @@ -76,6 +76,9 @@ void ESM4::LandTexture::load(ESM4::Reader& reader) case ESM4::SUB_MNAM: reader.getFormId(mMaterial); break; // TES5, FO4 + case ESM4::SUB_INAM: + reader.get(mMaterialFlags); + break; // SSE default: throw std::runtime_error("ESM4::LTEX::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); } diff --git a/components/esm4/loadltex.hpp b/components/esm4/loadltex.hpp index bed5887f5c..33c1683ac0 100644 --- a/components/esm4/loadltex.hpp +++ b/components/esm4/loadltex.hpp @@ -63,6 +63,17 @@ namespace ESM4 // ---------------------- + // ------ SSE ----------- + + enum MaterialFlags + { + Flag_IsSnow = 0x1, + }; + + std::uint32_t mMaterialFlags; + + // ---------------------- + void load(ESM4::Reader& reader); // void save(ESM4::Writer& writer) const; diff --git a/components/esm4/loadnpc.cpp b/components/esm4/loadnpc.cpp index 251af13630..885263d67b 100644 --- a/components/esm4/loadnpc.cpp +++ b/components/esm4/loadnpc.cpp @@ -116,9 +116,11 @@ void ESM4::Npc::load(ESM4::Reader& reader) { switch (subHdr.dataSize) { + case 20: // FO4 + mIsFO4 = true; + [[fallthrough]]; case 16: // TES4 case 24: // FO3/FNV, TES5 - case 20: // FO4 reader.get(&mBaseConfig, subHdr.dataSize); break; default: diff --git a/components/esm4/loadnpc.hpp b/components/esm4/loadnpc.hpp index 6f4b3c7e24..04e56b7bd9 100644 --- a/components/esm4/loadnpc.hpp +++ b/components/esm4/loadnpc.hpp @@ -78,6 +78,7 @@ namespace ESM4 FO3_NoRotateHead = 0x40000000 }; + // In FO4 flags seem to be the same. enum ACBS_TES5 { TES5_Female = 0x00000001, @@ -101,27 +102,32 @@ namespace ESM4 TES5_Invulnerable = 0x80000000 }; + // All FO3+ games. enum Template_Flags { - TES5_UseTraits = 0x0001, // Destructible Object; Traits tab, including race, gender, height, weight, - // voice type, death item; Sounds tab; Animation tab; Character Gen tabs - TES5_UseStats = 0x0002, // Stats tab, including level, autocalc, skills, health/magicka/stamina, - // speed, bleedout, class - TES5_UseFactions = 0x0004, // both factions and assigned crime faction - TES5_UseSpellList = 0x0008, // both spells and perks - TES5_UseAIData = 0x0010, // AI Data tab, including aggression/confidence/morality, combat style and - // gift filter - TES5_UseAIPackage = 0x0020, // only the basic Packages listed on the AI Packages tab; - // rest of tab controlled by Def Pack List - TES5_UseBaseData = 0x0080, // including name and short name, and flags for Essential, Protected, - // Respawn, Summonable, Simple Actor, and Doesn't affect stealth meter - TES5_UseInventory = 0x0100, // Inventory tab, including all outfits and geared-up item - // -- but not death item - TES5_UseScript = 0x0200, - TES5_UseDefined = 0x0400, // Def Pack List (the dropdown-selected package lists on the AI Packages tab) - TES5_UseAtkData = 0x0800, // Attack Data tab, including override from behavior graph race, - // events, and data) - TES5_UseKeywords = 0x1000 + Template_UseTraits = 0x0001, // Destructible Object; Traits tab, including race, gender, height, weight, + // voice type, death item; Sounds tab; Animation tab; Character Gen tabs + Template_UseStats = 0x0002, // Stats tab, including level, autocalc, skills, health/magicka/stamina, + // speed, bleedout, class + Template_UseFactions = 0x0004, // both factions and assigned crime faction + Template_UseSpellList = 0x0008, // both spells and perks + Template_UseAIData = 0x0010, // AI Data tab, including aggression/confidence/morality, combat style and + // gift filter + Template_UseAIPackage = 0x0020, // only the basic Packages listed on the AI Packages tab; + // rest of tab controlled by Def Pack List + Template_UseModel = 0x0040, // FO3, FONV; probably not used in TES5+ + Template_UseBaseData = 0x0080, // including name and short name, and flags for Essential, Protected, + // Respawn, Summonable, Simple Actor, and Doesn't affect stealth meter + Template_UseInventory = 0x0100, // Inventory tab, including all outfits and geared-up item, + // but not death item + Template_UseScript = 0x0200, + + // The following flags were added in TES5+: + + Template_UseDefined = 0x0400, // Def Pack List (the dropdown-selected package lists on the AI Packages tab) + Template_UseAtkData = 0x0800, // Attack Data tab, including override from behavior graph race, + // events, and data) + Template_UseKeywords = 0x1000 }; #pragma pack(push, 1) @@ -172,6 +178,7 @@ namespace ESM4 bool mIsTES4; bool mIsFONV; + bool mIsFO4 = false; std::string mEditorId; std::string mFullName; diff --git a/components/esm4/loadrace.hpp b/components/esm4/loadrace.hpp index 9cd4d00891..125fefbd9f 100644 --- a/components/esm4/loadrace.hpp +++ b/components/esm4/loadrace.hpp @@ -110,7 +110,7 @@ namespace ESM4 ESM::FormId mId; // from the header std::uint32_t mFlags; // from the header, see enum type RecordFlag for details - bool mIsTES5; + bool mIsTES5 = false; std::string mEditorId; std::string mFullName; 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/fx/pass.cpp b/components/fx/pass.cpp index 7a7329d755..cf50d20fe2 100644 --- a/components/fx/pass.cpp +++ b/components/fx/pass.cpp @@ -12,10 +12,10 @@ #include #include -#include #include #include #include +#include #include "stateupdater.hpp" #include "technique.hpp" @@ -68,6 +68,7 @@ namespace fx @uboStruct +#define OMW_API_VERSION @apiVersion #define OMW_REVERSE_Z @reverseZ #define OMW_RADIAL_FOG @radialFog #define OMW_EXPONENTIAL_FOG @exponentialFog @@ -90,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; @@ -190,6 +192,11 @@ mat4 omw_InvProjectionMatrix() #endif } + vec3 omw_GetNormalsWorldSpace(vec2 uv) + { + return (vec4(omw_GetNormals(uv), 0.0) * omw.viewMatrix).rgb; + } + vec3 omw_GetWorldPosFromUV(vec2 uv) { float depth = omw_GetDepth(uv); @@ -255,6 +262,7 @@ float omw_EstimateFogCoverageFromUV(vec2 uv) const std::vector> defines = { { "@pointLightCount", std::to_string(SceneUtil::PPLightBuffer::sMaxPPLightsArraySize) }, + { "@apiVersion", std::to_string(Version::getPostprocessingApiRevision()) }, { "@version", std::to_string(technique.getGLSLVersion()) }, { "@multiview", Stereo::getMultiview() ? "1" : "0" }, { "@builtinSampler", Stereo::getMultiview() ? "sampler2DArray" : "sampler2D" }, @@ -321,9 +329,6 @@ float omw_EstimateFogCoverageFromUV(vec2 uv) if (mBlendEq) stateSet->setAttributeAndModes(new osg::BlendEquation(mBlendEq.value())); - - if (mClearColor) - stateSet->setAttributeAndModes(new SceneUtil::ClearColor(mClearColor.value(), GL_COLOR_BUFFER_BIT)); } void Pass::dirty() @@ -339,7 +344,7 @@ float omw_EstimateFogCoverageFromUV(vec2 uv) if (mCompiled) return; - mLegacyGLSL = technique.getGLSLVersion() != 330; + mLegacyGLSL = technique.getGLSLVersion() < 330; if (mType == Type::Pixel) { diff --git a/components/fx/pass.hpp b/components/fx/pass.hpp index 509127a163..e176afc699 100644 --- a/components/fx/pass.hpp +++ b/components/fx/pass.hpp @@ -72,7 +72,6 @@ namespace fx std::array mRenderTargets; std::string mTarget; - std::optional mClearColor; std::optional mBlendSource; std::optional mBlendDest; diff --git a/components/fx/technique.cpp b/components/fx/technique.cpp index 0b5d784ad9..defb581cfc 100644 --- a/components/fx/technique.cpp +++ b/components/fx/technique.cpp @@ -279,6 +279,7 @@ namespace fx rt.mTarget->setSourceType(GL_UNSIGNED_BYTE); rt.mTarget->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); rt.mTarget->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + rt.mTarget->setName(std::string(mBlockName)); while (!isNext() && !isNext()) { @@ -312,6 +313,8 @@ namespace fx rt.mTarget->setSourceFormat(parseSourceFormat()); else if (key == "mipmaps") rt.mMipMap = parseBool(); + else if (key == "clear_color") + rt.mClearColor = parseVec(); else error(Misc::StringUtils::format("unexpected key '%s'", std::string(key))); @@ -797,9 +800,6 @@ namespace fx if (!pass) pass = std::make_shared(); - bool clear = true; - osg::Vec4f clearColor = { 1, 1, 1, 1 }; - while (!isNext()) { expect("invalid key in block header"); @@ -843,10 +843,6 @@ namespace fx if (blendEq != osg::BlendEquation::FUNC_ADD) pass->mBlendEq = blendEq; } - else if (key == "clear") - clear = parseBool(); - else if (key == "clear_color") - clearColor = parseVec(); else error(Misc::StringUtils::format("unrecognized key '%s' in block header", std::string(key))); @@ -864,9 +860,6 @@ namespace fx return; } - if (clear) - pass->mClearColor = clearColor; - error("malformed block header"); } diff --git a/components/fx/technique.hpp b/components/fx/technique.hpp index 844e4b552a..0d17128e56 100644 --- a/components/fx/technique.hpp +++ b/components/fx/technique.hpp @@ -54,10 +54,14 @@ namespace fx osg::ref_ptr mRenderTarget; osg::ref_ptr mRenderTexture; bool mResolve = false; + Types::SizeProxy mSize; + bool mMipMap; SubPass(const SubPass& other, const osg::CopyOp& copyOp = osg::CopyOp::SHALLOW_COPY) : mStateSet(new osg::StateSet(*other.mStateSet, copyOp)) , mResolve(other.mResolve) + , mSize(other.mSize) + , mMipMap(other.mMipMap) { if (other.mRenderTarget) mRenderTarget = new osg::FrameBufferObject(*other.mRenderTarget, copyOp); @@ -171,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); @@ -291,6 +298,7 @@ namespace fx bool mDynamic = false; bool mLocked = false; + bool mInternal = false; }; template <> diff --git a/components/fx/types.hpp b/components/fx/types.hpp index 09f191c61c..0f33d29e1a 100644 --- a/components/fx/types.hpp +++ b/components/fx/types.hpp @@ -29,6 +29,16 @@ namespace fx std::optional mWidth; std::optional mHeight; + SizeProxy() = default; + + SizeProxy(const SizeProxy& other) + : mWidthRatio(other.mWidthRatio) + , mHeightRatio(other.mHeightRatio) + , mWidth(other.mWidth) + , mHeight(other.mHeight) + { + } + std::tuple get(int width, int height) const { int scaledWidth = width; @@ -53,6 +63,17 @@ namespace fx osg::ref_ptr mTarget = new osg::Texture2D; SizeProxy mSize; bool mMipMap = false; + osg::Vec4f mClearColor = osg::Vec4f(0.0, 0.0, 0.0, 1.0); + + RenderTarget() = default; + + RenderTarget(const RenderTarget& other) + : mTarget(other.mTarget) + , mSize(other.mSize) + , mMipMap(other.mMipMap) + , mClearColor(other.mClearColor) + { + } }; 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/utilpackage.cpp b/components/lua/utilpackage.cpp index 85c5dcdbc1..e9b8e886d2 100644 --- a/components/lua/utilpackage.cpp +++ b/components/lua/utilpackage.cpp @@ -133,7 +133,8 @@ namespace LuaUtil // Lua bindings for Box util["box"] = sol::overload([](const Vec3& center, const Vec3& halfSize) { return Box(center, halfSize); }, - [](const TransformM& transform) { return Box(transform.mM); }); + [](const TransformM& transform) { return Box(transform.mM); }, + [](const TransformQ& transform) { return Box(Vec3(), Vec3(1, 1, 1), transform.mQ); }); sol::usertype boxType = lua.new_usertype("Box"); boxType["center"] = sol::readonly_property([](const Box& b) { return b.mCenter; }); boxType["halfSize"] = sol::readonly_property([](const Box& b) { return b.mHalfSize; }); diff --git a/components/lua_ui/adapter.cpp b/components/lua_ui/adapter.cpp index 2fd6365977..44493f6f46 100644 --- a/components/lua_ui/adapter.cpp +++ b/components/lua_ui/adapter.cpp @@ -18,11 +18,16 @@ namespace LuaUi { mContainer = MyGUI::Gui::getInstancePtr()->createWidget( "", MyGUI::IntCoord(), MyGUI::Align::Default, "", ""); - mContainer->initialize(luaState, mContainer); - mContainer->onCoordChange([this](WidgetExtension* ext, MyGUI::IntCoord coord) { setSize(coord.size()); }); + mContainer->initialize(luaState, mContainer, false); + mContainer->widget()->eventChangeCoord += MyGUI::newDelegate(this, &LuaAdapter::containerChangedCoord); mContainer->widget()->attachToWidget(this); } + void LuaAdapter::containerChangedCoord(MyGUI::Widget*) + { + setSize(mContainer->getSize()); + } + void LuaAdapter::attach(const std::shared_ptr& element) { detachElement(); @@ -44,14 +49,20 @@ namespace LuaUi void LuaAdapter::attachElement() { - if (mElement.get()) - mElement->attachToWidget(mContainer); + if (!mElement.get()) + return; + if (!mElement->mRoot) + throw std::logic_error("Attempting to use a destroyed UI Element"); + mContainer->setChildren({ mElement->mRoot }); + mElement->mRoot->updateCoord(); + mContainer->updateCoord(); } void LuaAdapter::detachElement() { - if (mElement.get()) - mElement->detachFromWidget(); + mContainer->setChildren({}); + if (mElement && mElement->mRoot) + mElement->mRoot->widget()->detachFromWidget(); mElement = nullptr; } } diff --git a/components/lua_ui/adapter.hpp b/components/lua_ui/adapter.hpp index d699e4992f..1524a55425 100644 --- a/components/lua_ui/adapter.hpp +++ b/components/lua_ui/adapter.hpp @@ -25,6 +25,8 @@ namespace LuaUi void attachElement(); void detachElement(); + + void containerChangedCoord(MyGUI::Widget*); }; } diff --git a/components/lua_ui/container.hpp b/components/lua_ui/container.hpp index 79d3cd8fa8..16f19d3c12 100644 --- a/components/lua_ui/container.hpp +++ b/components/lua_ui/container.hpp @@ -9,6 +9,7 @@ namespace LuaUi { MYGUI_RTTI_DERIVED(LuaContainer) + public: MyGUI::IntSize calculateSize() override; void updateCoord() override; diff --git a/components/lua_ui/content.cpp b/components/lua_ui/content.cpp index 2e1d4ca0c4..dd169a9291 100644 --- a/components/lua_ui/content.cpp +++ b/components/lua_ui/content.cpp @@ -1,4 +1,5 @@ #include "content.hpp" +#include "element.hpp" namespace LuaUi { @@ -14,6 +15,8 @@ namespace LuaUi bool isValidContent(const sol::object& object) { + if (object.is()) + return true; if (object.get_type() != sol::type::table) return false; sol::table table = object; diff --git a/components/lua_ui/content.hpp b/components/lua_ui/content.hpp index c8bb82ecf3..1a0379b817 100644 --- a/components/lua_ui/content.hpp +++ b/components/lua_ui/content.hpp @@ -22,7 +22,7 @@ namespace LuaUi : mTable(std::move(table)) { if (!isValidContent(mTable)) - throw std::domain_error("Expected a Content table"); + throw std::domain_error("Invalid UI Content"); } size_t size() const { return mTable.size(); } @@ -43,17 +43,17 @@ namespace LuaUi } void insert(size_t index, const sol::table& table) { callMethod("insert", toLua(index), table); } - sol::table at(size_t index) const + sol::object at(size_t index) const { if (index < size()) - return mTable.get(toLua(index)); + return mTable.get(toLua(index)); else throw std::range_error("Invalid Content index"); } - sol::table at(std::string_view name) const + sol::object at(std::string_view name) const { if (indexOf(name).has_value()) - return mTable.get(name); + return mTable.get(name); else throw std::range_error("Invalid Content key"); } diff --git a/components/lua_ui/content.lua b/components/lua_ui/content.lua index fbd39d5f68..99fdb86b70 100644 --- a/components/lua_ui/content.lua +++ b/components/lua_ui/content.lua @@ -1,12 +1,17 @@ local M = {} M.__Content = true + +function validateContentChild(v) + if not (type(v) == 'table' or v.__type and v.__type.name == 'LuaUi::Element') then + error('Content can only contain tables and Elements') + end +end + M.new = function(source) local result = {} result.__nameIndex = {} for i, v in ipairs(source) do - if type(v) ~= 'table' then - error('Content can only contain tables') - end + validateContentChild(v) result[i] = v if type(v.name) == 'string' then result.__nameIndex[v.name] = i @@ -38,9 +43,7 @@ end local methods = { insert = function(self, index, value) validateIndex(self, index) - if type(value) ~= 'table' then - error('Content can only contain tables') - end + validateContentChild(value) for i = #self, index, -1 do rawset(self, i + 1, rawget(self, i)) local name = rawget(self, i + 1) @@ -56,7 +59,7 @@ local methods = { indexOf = function(self, value) if type(value) == 'string' then return self.__nameIndex[value] - elseif type(value) == 'table' then + else for i = 1, #self do if rawget(self, i) == value then return i @@ -113,10 +116,9 @@ M.__newindex = function(self, key, value) local index = getIndexFromKey(self, key) if value == nil then remove(self, index) - elseif type(value) == 'table' then - assign(self, index, value) else - error('Content can only contain tables') + validateContentChild(value) + assign(self, index, value) end end M.__tostring = function(self) diff --git a/components/lua_ui/element.cpp b/components/lua_ui/element.cpp index 4fe9349b9e..baa3438982 100644 --- a/components/lua_ui/element.cpp +++ b/components/lua_ui/element.cpp @@ -43,12 +43,70 @@ namespace LuaUi return type; } - void destroyWidget(LuaUi::WidgetExtension* ext) + void destroyWidget(WidgetExtension* ext) { ext->deinitialize(); MyGUI::Gui::getInstancePtr()->destroyWidget(ext->widget()); } + void destroyChild(WidgetExtension* ext) + { + if (!ext->isRoot()) + destroyWidget(ext); + else + ext->widget()->detachFromWidget(); + } + + void detachElements(WidgetExtension* ext) + { + for (auto* child : ext->children()) + { + if (child->isRoot()) + child->widget()->detachFromWidget(); + else + detachElements(child); + } + for (auto* child : ext->templateChildren()) + { + if (child->isRoot()) + child->widget()->detachFromWidget(); + else + detachElements(child); + } + } + + void destroyRoot(WidgetExtension* ext) + { + detachElements(ext); + destroyWidget(ext); + } + + void updateRootCoord(WidgetExtension* ext) + { + WidgetExtension* root = ext; + while (root->getParent()) + root = root->getParent(); + root->updateCoord(); + } + + WidgetExtension* pluckElementRoot(const sol::object& child) + { + std::shared_ptr element = child.as>(); + WidgetExtension* root = element->mRoot; + if (!root) + throw std::logic_error("Using a destroyed element as a layout child"); + WidgetExtension* parent = root->getParent(); + if (parent) + { + auto children = parent->children(); + std::erase(children, root); + parent->setChildren(children); + root->widget()->detachFromWidget(); + } + root->updateCoord(); + return root; + } + WidgetExtension* createWidget(const sol::table& layout, uint64_t depth); void updateWidget(WidgetExtension* ext, const sol::table& layout, uint64_t depth); @@ -60,7 +118,7 @@ namespace LuaUi if (contentObj == sol::nil) { for (WidgetExtension* w : children) - destroyWidget(w); + destroyChild(w); return result; } ContentView content(LuaUtil::cast(contentObj)); @@ -69,22 +127,39 @@ namespace LuaUi for (size_t i = 0; i < minSize; i++) { WidgetExtension* ext = children[i]; - sol::table newLayout = content.at(i); - if (ext->widget()->getTypeName() == widgetType(newLayout)) + sol::object child = content.at(i); + if (child.is()) { - updateWidget(ext, newLayout, depth); + WidgetExtension* root = pluckElementRoot(child); + if (ext != root) + destroyChild(ext); + result[i] = root; } else { - destroyWidget(ext); - ext = createWidget(newLayout, depth); + sol::table newLayout = child.as(); + if (ext->widget()->getTypeName() == widgetType(newLayout)) + { + updateWidget(ext, newLayout, depth); + } + else + { + destroyChild(ext); + ext = createWidget(newLayout, depth); + } + result[i] = ext; } - result[i] = ext; } for (size_t i = minSize; i < children.size(); i++) - destroyWidget(children[i]); + destroyChild(children[i]); for (size_t i = minSize; i < content.size(); i++) - result[i] = createWidget(content.at(i), depth); + { + sol::object child = content.at(i); + if (child.is()) + result[i] = pluckElementRoot(child); + else + result[i] = createWidget(child.as(), depth); + } return result; } @@ -97,7 +172,7 @@ namespace LuaUi ext->setTemplateChildren(updateContent(ext->templateChildren(), content, depth)); } - void setEventCallbacks(LuaUi::WidgetExtension* ext, const sol::object& eventsObj) + void setEventCallbacks(WidgetExtension* ext, const sol::object& eventsObj) { ext->clearCallbacks(); if (eventsObj == sol::nil) @@ -130,7 +205,7 @@ namespace LuaUi WidgetExtension* ext = dynamic_cast(widget); if (!ext) throw std::runtime_error("Invalid widget!"); - ext->initialize(layout.lua_state(), widget); + ext->initialize(layout.lua_state(), widget, depth == 0); updateWidget(ext, layout, depth); return ext; @@ -169,7 +244,6 @@ namespace LuaUi Element::Element(sol::table layout) : mRoot(nullptr) - , mAttachedTo(nullptr) , mLayout(std::move(layout)) , mLayer() , mUpdate(false) @@ -191,7 +265,7 @@ namespace LuaUi { mRoot = createWidget(layout(), 0); mLayer = setLayer(mRoot, layout()); - updateAttachment(); + updateRootCoord(mRoot); } } @@ -201,15 +275,21 @@ namespace LuaUi { if (mRoot->widget()->getTypeName() != widgetType(layout())) { - destroyWidget(mRoot); + destroyRoot(mRoot); + WidgetExtension* parent = mRoot->getParent(); + auto children = parent->children(); + auto it = std::find(children.begin(), children.end(), mRoot); mRoot = createWidget(layout(), 0); + *it = mRoot; + parent->setChildren(children); + mRoot->updateCoord(); } else { updateWidget(mRoot, layout(), 0); } mLayer = setLayer(mRoot, layout()); - updateAttachment(); + updateRootCoord(mRoot); } mUpdate = false; } @@ -218,40 +298,10 @@ namespace LuaUi { if (mRoot) { - destroyWidget(mRoot); + destroyRoot(mRoot); mRoot = nullptr; mLayout = sol::make_object(mLayout.lua_state(), sol::nil); } sAllElements.erase(this); } - - void Element::attachToWidget(WidgetExtension* w) - { - if (mAttachedTo) - throw std::logic_error("A UI element can't be attached to two widgets at once"); - mAttachedTo = w; - updateAttachment(); - } - - void Element::detachFromWidget() - { - if (mRoot) - mRoot->widget()->detachFromWidget(); - if (mAttachedTo) - mAttachedTo->setChildren({}); - mAttachedTo = nullptr; - } - - void Element::updateAttachment() - { - if (!mRoot) - return; - if (mAttachedTo) - { - if (!mLayer.empty()) - Log(Debug::Warning) << "Ignoring element's layer " << mLayer << " because it's attached to a widget"; - mAttachedTo->setChildren({ mRoot }); - mAttachedTo->updateCoord(); - } - } } diff --git a/components/lua_ui/element.hpp b/components/lua_ui/element.hpp index b57af92fee..5aadb1beab 100644 --- a/components/lua_ui/element.hpp +++ b/components/lua_ui/element.hpp @@ -17,7 +17,6 @@ namespace LuaUi } WidgetExtension* mRoot; - WidgetExtension* mAttachedTo; sol::object mLayout; std::string mLayer; bool mUpdate; @@ -31,14 +30,10 @@ namespace LuaUi friend void clearUserInterface(); - void attachToWidget(WidgetExtension* w); - void detachFromWidget(); - private: Element(sol::table layout); sol::table layout() { return LuaUtil::cast(mLayout); } static std::map> sAllElements; - void updateAttachment(); }; } diff --git a/components/lua_ui/image.cpp b/components/lua_ui/image.cpp index 0454dd19b4..ffe93a8d2d 100644 --- a/components/lua_ui/image.cpp +++ b/components/lua_ui/image.cpp @@ -8,9 +8,7 @@ namespace LuaUi { void LuaTileRect::_setAlign(const MyGUI::IntSize& _oldsize) { - mCurrentCoord.set(0, 0, mCroppedParent->getWidth(), mCroppedParent->getHeight()); - mAlign = MyGUI::Align::Stretch; - MyGUI::TileRect::_setAlign(_oldsize); + mCoord.set(0, 0, mCroppedParent->getWidth(), mCroppedParent->getHeight()); mTileSize = mSetTileSize; // zero tilesize stands for not tiling @@ -25,6 +23,8 @@ namespace LuaUi mTileSize.width = 1e7; if (mTileSize.height <= 0) mTileSize.height = 1e7; + + MyGUI::TileRect::_updateView(); } void LuaImage::initialize() @@ -55,13 +55,13 @@ namespace LuaUi if (texture != nullptr) textureSize = MyGUI::IntSize(texture->getWidth(), texture->getHeight()); - mTileRect->updateSize(MyGUI::IntSize(tileH ? textureSize.width : 0, tileV ? textureSize.height : 0)); - setImageTile(textureSize); - if (atlasCoord.width == 0) atlasCoord.width = textureSize.width; if (atlasCoord.height == 0) atlasCoord.height = textureSize.height; + + mTileRect->updateSize(MyGUI::IntSize(tileH ? atlasCoord.width : 0, tileV ? atlasCoord.height : 0)); + setImageTile(atlasCoord.size()); setImageCoord(atlasCoord); setColour(propertyValue("color", MyGUI::Colour(1, 1, 1, 1))); diff --git a/components/lua_ui/textedit.cpp b/components/lua_ui/textedit.cpp index a8c19fa8fd..e12bd20c35 100644 --- a/components/lua_ui/textedit.cpp +++ b/components/lua_ui/textedit.cpp @@ -8,6 +8,7 @@ namespace LuaUi { mEditBox = createWidget("LuaTextEdit", MyGUI::IntCoord(0, 0, 0, 0), MyGUI::Align::Default); mEditBox->eventEditTextChange += MyGUI::newDelegate(this, &LuaTextEdit::textChange); + mEditBox->setMaxTextLength(std::numeric_limits::max()); registerEvents(mEditBox); WidgetExtension::initialize(); } diff --git a/components/lua_ui/widget.cpp b/components/lua_ui/widget.cpp index e3188f6136..9550c9de73 100644 --- a/components/lua_ui/widget.cpp +++ b/components/lua_ui/widget.cpp @@ -9,6 +9,7 @@ namespace LuaUi : mForcePosition(false) , mForceSize(false) , mPropagateEvents(true) + , mVisible(true) , mLua(nullptr) , mWidget(nullptr) , mSlot(this) @@ -18,13 +19,15 @@ namespace LuaUi , mExternal(sol::nil) , mParent(nullptr) , mTemplateChild(false) + , mElementRoot(false) { } - void WidgetExtension::initialize(lua_State* lua, MyGUI::Widget* self) + void WidgetExtension::initialize(lua_State* lua, MyGUI::Widget* self, bool isRoot) { mLua = lua; mWidget = self; + mElementRoot = isRoot; initialize(); updateTemplate(); } @@ -40,8 +43,6 @@ namespace LuaUi clearCallbacks(); clearEvents(mWidget); - mOnCoordChange.reset(); - for (WidgetExtension* w : mChildren) w->deinitialize(); for (WidgetExtension* w : mTemplateChildren) @@ -92,10 +93,9 @@ namespace LuaUi { // workaround for MyGUI bug // parent visibility doesn't affect added children - MyGUI::Widget* widget = this->widget(); - MyGUI::Widget* parent = widget->getParent(); - bool inheritedVisible = widget->getVisible() && (parent == nullptr || parent->getInheritedVisible()); - widget->setVisible(inheritedVisible); + MyGUI::Widget* parent = widget()->getParent(); + bool inheritedVisible = mVisible && (parent == nullptr || parent->getInheritedVisible()); + widget()->setVisible(inheritedVisible); } void WidgetExtension::attach(WidgetExtension* ext) @@ -262,8 +262,6 @@ namespace LuaUi if (oldCoord != newCoord) mWidget->setCoord(newCoord); updateChildrenCoord(); - if (oldCoord != newCoord && mOnCoordChange.has_value()) - mOnCoordChange.value()(this, newCoord); } void WidgetExtension::setProperties(const sol::object& props) @@ -280,7 +278,8 @@ namespace LuaUi mRelativeCoord = propertyValue("relativePosition", MyGUI::FloatPoint()); mRelativeCoord = propertyValue("relativeSize", MyGUI::FloatSize()); mAnchor = propertyValue("anchor", MyGUI::FloatSize()); - mWidget->setVisible(propertyValue("visible", true)); + mVisible = propertyValue("visible", true); + mWidget->setVisible(mVisible); mWidget->setPointer(propertyValue("pointer", std::string("arrow"))); mWidget->setAlpha(propertyValue("alpha", 1.f)); mWidget->setInheritsAlpha(propertyValue("inheritAlpha", true)); diff --git a/components/lua_ui/widget.hpp b/components/lua_ui/widget.hpp index 81698b0479..c72b64ae3b 100644 --- a/components/lua_ui/widget.hpp +++ b/components/lua_ui/widget.hpp @@ -26,13 +26,16 @@ namespace LuaUi virtual ~WidgetExtension() = default; // must be called after creating the underlying MyGUI::Widget - void initialize(lua_State* lua, MyGUI::Widget* self); + void initialize(lua_State* lua, MyGUI::Widget* self, bool isRoot); // must be called after before destroying the underlying MyGUI::Widget virtual void deinitialize(); MyGUI::Widget* widget() const { return mWidget; } WidgetExtension* slot() const { return mSlot; } + bool isRoot() const { return mElementRoot; } + WidgetExtension* getParent() const { return mParent; } + void reset(); const std::vector& children() { return mChildren; } @@ -66,11 +69,6 @@ namespace LuaUi return parseExternal(mExternal, name, defaultValue); } - void onCoordChange(const std::optional>& callback) - { - mOnCoordChange = callback; - } - virtual MyGUI::IntSize calculateSize(); virtual MyGUI::IntPoint calculatePosition(const MyGUI::IntSize& size); MyGUI::IntCoord calculateCoord(); @@ -137,6 +135,7 @@ namespace LuaUi MyGUI::FloatSize mAnchor; bool mPropagateEvents; + bool mVisible; // used to implement updateVisible private: // use lua_State* instead of sol::state_view because MyGUI requires a default constructor @@ -152,6 +151,7 @@ namespace LuaUi sol::object mExternal; WidgetExtension* mParent; bool mTemplateChild; + bool mElementRoot; void attach(WidgetExtension* ext); void attachTemplate(WidgetExtension* ext); @@ -172,8 +172,6 @@ namespace LuaUi void focusGain(MyGUI::Widget*, MyGUI::Widget*); void focusLoss(MyGUI::Widget*, MyGUI::Widget*); - std::optional> mOnCoordChange; - void updateVisible(); }; diff --git a/components/misc/display.cpp b/components/misc/display.cpp new file mode 100644 index 0000000000..ee78b2a0c9 --- /dev/null +++ b/components/misc/display.cpp @@ -0,0 +1,82 @@ +#include "display.hpp" + +#include +#include + +#include + +namespace Misc +{ + std::string getResolutionText(int x, int y, const std::string& format) + { + int gcd = std::gcd(x, y); + if (gcd == 0) + return std::string(); + + int xaspect = x / gcd; + int yaspect = y / gcd; + + // It is unclear how to handle 90-degree screen rotation properly. + // So far only swap aspects, apply custom formatting logic and then swap back. + // As result, 1920 x 1200 is displayed as "1200 x 1920 (10:16)" + bool flipped = false; + if (yaspect > xaspect) + { + flipped = true; + std::swap(xaspect, yaspect); + } + + // 683:384 (used in 1366 x 768) is usually referred as 16:9 + if (xaspect == 683 && yaspect == 384) + { + xaspect = 16; + yaspect = 9; + } + // 85:48 (used in 1360 x 768) is usually referred as 16:9 + else if (xaspect == 85 && yaspect == 48) + { + xaspect = 16; + yaspect = 9; + } + // 49:36 (used in 1176 x 864) is usually referred as 4:3 + else if (xaspect == 49 && yaspect == 36) + { + xaspect = 4; + yaspect = 3; + } + // 39:29 (used in 624 x 484) is usually referred as 4:3 + else if (xaspect == 39 && yaspect == 29) + { + xaspect = 4; + yaspect = 3; + } + // 8:5 (used in 1440 x 900) is usually referred as 16:10 + else if (xaspect == 8 && yaspect == 5) + { + xaspect = 16; + yaspect = 10; + } + // 5:3 (used in 1280 x 768) is usually referred as 15:9 + else if (xaspect == 5 && yaspect == 3) + { + xaspect = 15; + yaspect = 9; + } + else + { + // everything between 21:9 and 22:9 + // is usually referred as 21:9 + float ratio = static_cast(xaspect) / yaspect; + if (ratio >= 21 / 9.f && ratio < 22 / 9.f) + { + xaspect = 21; + yaspect = 9; + } + } + + if (flipped) + std::swap(xaspect, yaspect); + + return Misc::StringUtils::format(format, x, y, xaspect, yaspect); + } +} diff --git a/components/misc/display.hpp b/components/misc/display.hpp new file mode 100644 index 0000000000..82037661c8 --- /dev/null +++ b/components/misc/display.hpp @@ -0,0 +1,11 @@ +#ifndef OPENMW_COMPONENTS_MISC_DISPLAY_H +#define OPENMW_COMPONENTS_MISC_DISPLAY_H + +#include + +namespace Misc +{ + std::string getResolutionText(int x, int y, const std::string& format); +} + +#endif 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/controller.hpp b/components/nif/controller.hpp index 33c04a6d35..947fae1ab2 100644 --- a/components/nif/controller.hpp +++ b/components/nif/controller.hpp @@ -171,7 +171,7 @@ namespace Nif }; NiPosDataPtr mData; - TargetColor mTargetColor; + TargetColor mTargetColor = TargetColor::Ambient; void read(NIFStream* nif) override; void post(Reader& nif) override; diff --git a/components/nif/data.cpp b/components/nif/data.cpp index 8b459d015a..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); @@ -413,7 +424,7 @@ namespace Nif if (!hasPresenceFlags || nif->get()) nif->readVector(mVertexMap, numVertices); if (!hasPresenceFlags || nif->get()) - nif->readVector(mWeights, numVertices * bonesPerVertex); + nif->readVector(mWeights, static_cast(numVertices) * bonesPerVertex); std::vector stripLengths; nif->readVector(stripLengths, numStrips); if (!hasPresenceFlags || nif->get()) @@ -428,7 +439,7 @@ namespace Nif nif->readVector(mTriangles, numTriangles * 3); } if (nif->get() != 0) - nif->readVector(mBoneIndices, numVertices * bonesPerVertex); + nif->readVector(mBoneIndices, static_cast(numVertices) * bonesPerVertex); if (nif->getBethVersion() > NIFFile::BethVersion::BETHVER_FO3) { nif->read(mLODLevel); 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/effect.cpp b/components/nif/effect.cpp index ae4c7947cb..47d1863352 100644 --- a/components/nif/effect.cpp +++ b/components/nif/effect.cpp @@ -45,7 +45,9 @@ namespace Nif { NiPointLight::read(nif); - nif->read(mCutoff); + nif->read(mOuterSpotAngle); + if (nif->getVersion() >= NIFStream::generateVersion(20, 2, 0, 5)) + nif->read(mInnerSpotAngle); nif->read(mExponent); } diff --git a/components/nif/effect.hpp b/components/nif/effect.hpp index 906a7fdedf..2dd18d5304 100644 --- a/components/nif/effect.hpp +++ b/components/nif/effect.hpp @@ -58,7 +58,8 @@ namespace Nif struct NiSpotLight : public NiPointLight { - float mCutoff; + float mOuterSpotAngle; + float mInnerSpotAngle{ 0.f }; float mExponent; void read(NIFStream* nif) override; }; diff --git a/components/nif/extra.cpp b/components/nif/extra.cpp index 2d222f5a54..4ebd0bf517 100644 --- a/components/nif/extra.cpp +++ b/components/nif/extra.cpp @@ -136,6 +136,11 @@ namespace Nif nif->readVector(mData, nif->get()); } + void BSCollisionQueryProxyExtraData::read(NIFStream* nif) + { + nif->readVector(mData, nif->get()); + } + void BSConnectPoint::Point::read(NIFStream* nif) { mParent = nif->getSizedString(); diff --git a/components/nif/extra.hpp b/components/nif/extra.hpp index 1efa4ae7bb..2b46c81e26 100644 --- a/components/nif/extra.hpp +++ b/components/nif/extra.hpp @@ -173,6 +173,13 @@ namespace Nif void read(NIFStream* nif) override; }; + struct BSCollisionQueryProxyExtraData : BSExtraData + { + std::vector mData; + + void read(NIFStream* nif) override; + }; + struct BSConnectPoint { struct Point diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index 81a223e095..d6d063a254 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -248,6 +248,8 @@ namespace Nif { "BSBehaviorGraphExtraData", &construct }, { "BSBoneLODExtraData", &construct }, { "BSClothExtraData", &construct }, + { "BSCollisionQueryProxyExtraData", + &construct }, { "BSConnectPoint::Children", &construct }, { "BSConnectPoint::Parents", &construct }, { "BSDecalPlacementVectorExtraData", @@ -510,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)); @@ -536,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); @@ -601,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 @@ -656,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 28cd2cdbfe..b91f143d00 100644 --- a/components/nif/node.cpp +++ b/components/nif/node.cpp @@ -2,13 +2,64 @@ #include +#include + +#include #include +#include #include "data.hpp" #include "exception.hpp" #include "physics.hpp" #include "property.hpp" +namespace +{ + + void triBasedGeomToBtTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriBasedGeomData& data) + { + // FIXME: copying vertices/indices individually is unreasonable + const std::vector& vertices = data.mVertices; + mesh.preallocateVertices(static_cast(vertices.size())); + for (const osg::Vec3f& vertex : vertices) + mesh.findOrAddVertex(Misc::Convert::toBullet(vertex), false); + + 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 { @@ -218,6 +269,99 @@ namespace Nif } } + std::unique_ptr NiTriShape::getCollisionShape() const + { + if (mData.empty() || mData->mVertices.empty()) + return nullptr; + + std::vector*> triangleLists; + std::vector>*> stripsLists; + auto data = static_cast(mData.getPtr()); + 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); + for (const auto triangles : triangleLists) + trianglesToBtTriangleMesh(*mesh, *triangles); + for (const auto strips : stripsLists) + stripsToBtTriangleMesh(*mesh, *strips); + + if (mesh->getNumTriangles() == 0) + return nullptr; + + auto shape = std::make_unique(mesh.get(), true); + std::ignore = mesh.release(); + + return shape; + } + + std::unique_ptr NiTriStrips::getCollisionShape() const + { + if (mData.empty() || mData->mVertices.empty()) + return nullptr; + + std::vector*> triangleLists; + std::vector>*> stripsLists; + auto data = static_cast(mData.getPtr()); + 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 auto triangles : triangleLists) + trianglesToBtTriangleMesh(*mesh, *triangles); + for (const auto strips : stripsLists) + stripsToBtTriangleMesh(*mesh, *strips); + + if (mesh->getNumTriangles() == 0) + return nullptr; + + auto shape = std::make_unique(mesh.get(), true); + std::ignore = mesh.release(); + + return shape; + } + + std::unique_ptr NiLines::getCollisionShape() const + { + return nullptr; + } + + std::unique_ptr NiParticles::getCollisionShape() const + { + return nullptr; + } + void BSSegmentedTriShape::SegmentData::read(NIFStream* nif) { nif->read(mFlags); diff --git a/components/nif/node.hpp b/components/nif/node.hpp index 0aaad40ed4..32746f7a9f 100644 --- a/components/nif/node.hpp +++ b/components/nif/node.hpp @@ -8,6 +8,8 @@ #include "base.hpp" +class btCollisionShape; + namespace Nif { @@ -146,6 +148,11 @@ namespace Nif void read(NIFStream* nif) override; void post(Reader& nif) override; + + virtual std::unique_ptr getCollisionShape() const + { + throw std::runtime_error("NiGeometry::getCollisionShape() called on base class"); + } }; // Abstract triangle-based geometry @@ -155,6 +162,7 @@ namespace Nif struct NiTriShape : NiTriBasedGeom { + std::unique_ptr getCollisionShape() const override; }; struct BSSegmentedTriShape : NiTriShape @@ -175,17 +183,20 @@ namespace Nif struct NiTriStrips : NiTriBasedGeom { + std::unique_ptr getCollisionShape() const override; }; struct NiLines : NiTriBasedGeom { + std::unique_ptr getCollisionShape() const override; }; struct NiParticles : NiGeometry { + std::unique_ptr getCollisionShape() const override; }; - struct BSLODTriShape : NiTriBasedGeom + struct BSLODTriShape : NiTriShape { std::array mLOD; void read(NIFStream* nif) override; diff --git a/components/nif/particle.cpp b/components/nif/particle.cpp index 0581c5a1d1..d81d423fb6 100644 --- a/components/nif/particle.cpp +++ b/components/nif/particle.cpp @@ -231,7 +231,7 @@ namespace Nif info.read(nif); } - if (nif->getBethVersion() >= NIFFile::BethVersion::BETHVER_F76) + if (nif->getBethVersion() > NIFFile::BethVersion::BETHVER_FO4) nif->skip(12); // Unknown if (nif->getVersion() >= NIFStream::generateVersion(20, 0, 0, 2) && nif->get() && hasData) @@ -420,8 +420,8 @@ namespace Nif { nif->read(mRotationSpeedVariation); - if (nif->getBethVersion() >= NIFFile::BethVersion::BETHVER_F76) - nif->skip(5); // Unknown + if (nif->getBethVersion() > NIFFile::BethVersion::BETHVER_FO4) + nif->skip(17); // Unknown nif->read(mRotationAngle); nif->read(mRotationAngleVariation); diff --git a/components/nif/property.cpp b/components/nif/property.cpp index bcc70540c8..2a5f91385d 100644 --- a/components/nif/property.cpp +++ b/components/nif/property.cpp @@ -148,12 +148,6 @@ namespace Nif } else { - uint32_t numShaderFlags1 = 0, numShaderFlags2 = 0; - nif->read(numShaderFlags1); - if (nif->getBethVersion() >= 152) - nif->read(numShaderFlags2); - nif->readVector(mShaderFlags1Hashes, numShaderFlags1); - nif->readVector(mShaderFlags2Hashes, numShaderFlags2); if (nif->getBethVersion() >= NIFFile::BethVersion::BETHVER_F76 && recType == RC_BSLightingShaderProperty) { nif->read(mType); @@ -181,6 +175,13 @@ namespace Nif break; } } + + uint32_t numShaderFlags1 = 0, numShaderFlags2 = 0; + nif->read(numShaderFlags1); + if (nif->getBethVersion() >= 152) + nif->read(numShaderFlags2); + nif->readVector(mShaderFlags1Hashes, numShaderFlags1); + nif->readVector(mShaderFlags2Hashes, numShaderFlags2); } nif->read(mUVOffset); @@ -324,7 +325,7 @@ namespace Nif { nif->read(mSubsurfaceRolloff); nif->read(mRimlightPower); - if (mRimlightPower == std::numeric_limits::max()) + if (nif->getBethVersion() == 130 && mRimlightPower == std::numeric_limits::max()) nif->read(mBacklightPower); } @@ -335,27 +336,27 @@ namespace Nif mWetness.read(nif); } - if (nif->getBethVersion() >= NIFFile::BethVersion::BETHVER_STF) + if (nif->getBethVersion() >= NIFFile::BethVersion::BETHVER_F76) + { mLuminance.read(nif); - - if (nif->getBethVersion() == NIFFile::BethVersion::BETHVER_F76) - { - nif->read(mDoTranslucency); - if (mDoTranslucency) - mTranslucency.read(nif); - if (nif->get() != 0) + if (nif->getBethVersion() == NIFFile::BethVersion::BETHVER_F76) { - mTextureArrays.resize(nif->get()); - for (std::vector& textureArray : mTextureArrays) - nif->getSizedStrings(textureArray, nif->get()); + nif->read(mDoTranslucency); + if (mDoTranslucency) + mTranslucency.read(nif); + if (nif->get() != 0) + { + mTextureArrays.resize(nif->get()); + for (std::vector& textureArray : mTextureArrays) + nif->getSizedStrings(textureArray, nif->get()); + } + } + if (nif->getBethVersion() >= NIFFile::BethVersion::BETHVER_STF) + { + nif->skip(4); // Unknown + nif->skip(4); // Unknown + nif->skip(2); // Unknown } - } - - if (nif->getBethVersion() >= NIFFile::BethVersion::BETHVER_STF) - { - nif->skip(4); // Unknown - nif->skip(4); // Unknown - nif->skip(2); // Unknown } switch (static_cast(mType)) @@ -439,7 +440,6 @@ namespace Nif if (nif->getBethVersion() >= NIFFile::BethVersion::BETHVER_F76) { - nif->read(mRefractionPower); mReflectanceTexture = nif->getSizedString(); mLightingTexture = nif->getSizedString(); nif->read(mEmittanceColor); diff --git a/components/nif/property.hpp b/components/nif/property.hpp index da908f2eab..fbc7e8294c 100644 --- a/components/nif/property.hpp +++ b/components/nif/property.hpp @@ -108,7 +108,15 @@ namespace Nif enum BSShaderFlags1 { BSSFlag1_Specular = 0x00000001, + BSSFlag1_Refraction = 0x00008000, + BSSFlag1_FireRefraction = 0x00010000, BSSFlag1_Decal = 0x04000000, + BSSFlag1_DepthTest = 0x80000000, + }; + + enum BSShaderFlags2 + { + BSSFlag2_DepthWrite = 0x00000001, }; struct BSSPParallaxParams @@ -140,6 +148,10 @@ namespace Nif // Shader-specific flag methods must be handled on per-record basis bool specular() const { return mShaderFlags1 & BSSFlag1_Specular; } 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 @@ -287,15 +299,15 @@ namespace Nif { BSShaderTextureSetPtr mTextureSet; osg::Vec3f mEmissive; - float mEmissiveMult; + float mEmissiveMult{ 1.f }; std::string mRootMaterial; - uint32_t mClamp; - float mAlpha; + uint32_t mClamp{ 3 }; + float mAlpha{ 1.f }; float mRefractionStrength; float mGlossiness{ 80.f }; float mSmoothness{ 1.f }; osg::Vec3f mSpecular; - float mSpecStrength; + float mSpecStrength{ 1.f }; std::array mLightingEffects; float mSubsurfaceRolloff; float mRimlightPower; @@ -453,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/nif/record.hpp b/components/nif/record.hpp index d2a30b1317..699522d24c 100644 --- a/components/nif/record.hpp +++ b/components/nif/record.hpp @@ -77,6 +77,7 @@ namespace Nif RC_BSBound, RC_BSBoneLODExtraData, RC_BSClothExtraData, + RC_BSCollisionQueryProxyExtraData, RC_BSConnectPointChildren, RC_BSConnectPointParents, RC_BSDecalPlacementVectorExtraData, diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index 95744a8cfe..0737d0a165 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -6,23 +6,15 @@ #include #include -#include -#include - #include - +#include #include - #include - -#include #include #include #include #include -#include - namespace { @@ -33,111 +25,6 @@ namespace return letterPos < path.size() && (path[letterPos] == 'x' || path[letterPos] == 'X'); } - bool isTypeNiGeometry(int type) - { - switch (type) - { - case Nif::RC_NiTriShape: - case Nif::RC_NiTriStrips: - case Nif::RC_BSLODTriShape: - case Nif::RC_BSSegmentedTriShape: - return true; - } - return false; - } - - bool isTypeTriShape(int type) - { - switch (type) - { - case Nif::RC_NiTriShape: - case Nif::RC_BSLODTriShape: - case Nif::RC_BSSegmentedTriShape: - return true; - } - - return false; - } - - void prepareTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriBasedGeomData& data) - { - // FIXME: copying vertices/indices individually is unreasonable - const std::vector& vertices = data.mVertices; - mesh.preallocateVertices(static_cast(vertices.size())); - for (const osg::Vec3f& vertex : vertices) - mesh.findOrAddVertex(Misc::Convert::toBullet(vertex), false); - - mesh.preallocateIndices(static_cast(data.mNumTriangles) * 3); - } - - void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriShapeData& data) - { - prepareTriangleMesh(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]); - } - - void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriStripsData& data) - { - prepareTriangleMesh(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); - } - } - } - - template - auto handleNiGeometry(const Nif::NiGeometry& geometry, Function&& function) - -> decltype(function(static_cast(geometry.mData.get()))) - { - if (isTypeTriShape(geometry.recType)) - { - auto data = static_cast(geometry.mData.getPtr()); - if (data->mTriangles.empty()) - return {}; - - return function(static_cast(*data)); - } - - if (geometry.recType == Nif::RC_NiTriStrips) - { - auto data = static_cast(geometry.mData.getPtr()); - if (data->mStrips.empty()) - return {}; - - return function(static_cast(*data)); - } - - return {}; - } - - std::unique_ptr makeChildMesh(const Nif::NiGeometry& geometry) - { - return handleNiGeometry(geometry, [&](const auto& data) { - auto mesh = std::make_unique(); - fillTriangleMesh(*mesh, data); - return mesh; - }); - } - } namespace NifBullet @@ -171,23 +58,8 @@ namespace NifBullet } for (const Nif::NiAVObject* node : roots) - { - // Try to find a valid bounding box first. If one's found for any root node, use that. if (findBoundingBox(*node)) - { - const btVector3 extents = Misc::Convert::toBullet(mShape->mCollisionBox.mExtents); - const btVector3 center = Misc::Convert::toBullet(mShape->mCollisionBox.mCenter); - auto compound = std::make_unique(); - auto boxShape = std::make_unique(extents); - btTransform transform = btTransform::getIdentity(); - transform.setOrigin(center); - compound->addChildShape(transform, boxShape.get()); - std::ignore = boxShape.release(); - - mShape->mCollisionShape.reset(compound.release()); - return mShape; - } - } + break; HandleNodeArgs args; @@ -196,8 +68,6 @@ namespace NifBullet // TODO: investigate whether this should and could be optimized. args.mAnimated = pathFileNameStartsWithX(mShape->mFileName); - // If there's no bounding box, we'll have to generate a Bullet collision shape - // from the collision data present in every root node. for (const Nif::NiAVObject* node : roots) handleRoot(nif, *node, args); @@ -210,8 +80,7 @@ namespace NifBullet return mShape; } - // Find a boundingBox in the node hierarchy. - // Return: use bounding box for collision? + // Find a bounding box in the node hierarchy to use for actor collision bool BulletNifLoader::findBoundingBox(const Nif::NiAVObject& node) { if (Misc::StringUtils::ciEqual(node.mName, "Bounding Box")) @@ -282,6 +151,32 @@ namespace NifBullet args.mAutogenerated = colNode == nullptr; + // Check for extra data + for (const auto& e : node.getExtraList()) + { + if (e->recType == Nif::RC_NiStringExtraData) + { + // String markers may contain important information + // affecting the entire subtree of this node + auto sd = static_cast(e.getPtr()); + + // Editor marker flag + if (sd->mData == "MRK") + args.mHasTriMarkers = true; + else if (Misc::StringUtils::ciStartsWith(sd->mData, "NC")) + { + // NC prefix is case-insensitive but the second C in NCC flag needs be uppercase. + + // Collide only with camera. + if (sd->mData.length() > 2 && sd->mData[2] == 'C') + mShape->mVisualCollisionType = Resource::VisualCollisionType::Camera; + // No collision. + else + mShape->mVisualCollisionType = Resource::VisualCollisionType::Default; + } + } + } + // FIXME: BulletNifLoader should never have to provide rendered geometry for camera collision if (colNode && colNode->mChildren.empty()) { @@ -329,8 +224,8 @@ namespace NifBullet return; // Otherwise we'll want to notify the user. - Log(Debug::Info) << "RootCollisionNode is not attached to the root node in " << mShape->mFileName - << ". Treating it as a common NiTriShape."; + Log(Debug::Info) << "BulletNifLoader: RootCollisionNode is not attached to the root node in " + << mShape->mFileName << ". Treating it as a NiNode."; } else { @@ -342,79 +237,68 @@ namespace NifBullet if (node.recType == Nif::RC_AvoidNode) args.mAvoid = true; - // Check for extra data - for (const auto& e : node.getExtraList()) + if (args.mAutogenerated || args.mIsCollisionNode) { - if (e->recType == Nif::RC_NiStringExtraData) - { - // String markers may contain important information - // affecting the entire subtree of this node - auto sd = static_cast(e.getPtr()); - - if (Misc::StringUtils::ciStartsWith(sd->mData, "NC")) - { - // NCC flag in vanilla is partly case sensitive: prefix NC is case insensitive but second C needs be - // uppercase - if (sd->mData.length() > 2 && sd->mData[2] == 'C') - // Collide only with camera. - mShape->mVisualCollisionType = Resource::VisualCollisionType::Camera; - else - // No collision. - mShape->mVisualCollisionType = Resource::VisualCollisionType::Default; - } - // Don't autogenerate collision if MRK is set. - // FIXME: verify if this covers the entire subtree - else if (sd->mData == "MRK" && args.mAutogenerated) - { - return; - } - } + auto geometry = dynamic_cast(&node); + if (geometry) + handleGeometry(*geometry, parent, args); } - if ((args.mAutogenerated || args.mIsCollisionNode) && isTypeNiGeometry(node.recType)) - handleNiTriShape(static_cast(node), parent, args); - // For NiNodes, loop through children if (const Nif::NiNode* ninode = dynamic_cast(&node)) { 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; } } } - void BulletNifLoader::handleNiTriShape( + void BulletNifLoader::handleGeometry( const Nif::NiGeometry& niGeometry, const Nif::Parent* nodeParent, HandleNodeArgs args) { - // mHasMarkers is specifically BSXFlags editor marker flag. - // If this changes, the check must be corrected. + // This flag comes from BSXFlags if (args.mHasMarkers && Misc::StringUtils::ciStartsWith(niGeometry.mName, "EditorMarker")) return; - if (niGeometry.mData.empty() || niGeometry.mData->mVertices.empty()) + // This flag comes from Morrowind + if (args.mHasTriMarkers && Misc::StringUtils::ciStartsWith(niGeometry.mName, "Tri EditorMarker")) return; if (!niGeometry.mSkin.empty()) args.mAnimated = false; - // TODO: handle NiSkinPartition - std::unique_ptr childMesh = makeChildMesh(niGeometry); - if (childMesh == nullptr || childMesh->getNumTriangles() == 0) + std::unique_ptr childShape = niGeometry.getCollisionShape(); + if (childShape == nullptr) return; - auto childShape = std::make_unique(childMesh.get(), true); - std::ignore = childMesh.release(); - 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/nifbullet/bulletnifloader.hpp b/components/nifbullet/bulletnifloader.hpp index 521bbe91dd..a80e6fdc3d 100644 --- a/components/nifbullet/bulletnifloader.hpp +++ b/components/nifbullet/bulletnifloader.hpp @@ -53,6 +53,7 @@ namespace NifBullet struct HandleNodeArgs { bool mHasMarkers{ false }; + bool mHasTriMarkers{ false }; bool mAnimated{ false }; bool mIsCollisionNode{ false }; bool mAutogenerated{ false }; @@ -61,7 +62,7 @@ namespace NifBullet void handleRoot(Nif::FileView nif, const Nif::NiAVObject& node, HandleNodeArgs args); void handleNode(const Nif::NiAVObject& node, const Nif::Parent* parent, HandleNodeArgs args); - void handleNiTriShape(const Nif::NiGeometry& nifNode, const Nif::Parent* parent, HandleNodeArgs args); + void handleGeometry(const Nif::NiGeometry& nifNode, const Nif::Parent* parent, HandleNodeArgs args); std::unique_ptr mCompoundShape; std::unique_ptr mAvoidCompoundShape; 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 9945a8c40e..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" @@ -679,10 +680,11 @@ namespace NifOsg // String markers may contain important information // affecting the entire subtree of this obj - if (sd->mData == "MRK" && !Loader::getShowMarkers()) + if (sd->mData == "MRK") { // Marker objects. These meshes are only visible in the editor. - args.mHasMarkers = true; + if (!Loader::getShowMarkers() && args.mRootNode == node) + args.mHasMarkers = true; } else if (sd->mData == "BONE") { @@ -696,8 +698,12 @@ namespace NifOsg } else if (e->recType == Nif::RC_BSXFlags) { + if (args.mRootNode != node) + continue; + auto bsxFlags = static_cast(e.getPtr()); - if (bsxFlags->mData & 32) // Editor marker flag + // Marker objects. + if (!Loader::getShowMarkers() && (bsxFlags->mData & 32)) args.mHasMarkers = true; } } @@ -1376,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) { @@ -1510,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; } @@ -1666,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; } @@ -1964,6 +1960,21 @@ namespace NifOsg return texEnv; } + void handleDepthFlags(osg::StateSet* stateset, bool depthTest, bool depthWrite) + { + if (!depthWrite && !depthTest) + { + stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + return; + } + osg::ref_ptr depth = new osg::Depth; + depth->setWriteMask(depthWrite); + if (!depthTest) + depth->setFunction(osg::Depth::ALWAYS); + depth = shareAttribute(depth); + stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); + } + void handleTextureProperty(const Nif::NiTexturingProperty* texprop, const std::string& nodeName, osg::StateSet* stateset, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags) @@ -2319,16 +2330,8 @@ namespace NifOsg { const Nif::NiZBufferProperty* zprop = static_cast(property); osg::StateSet* stateset = node->getOrCreateStateSet(); - stateset->setMode( - GL_DEPTH_TEST, zprop->depthTest() ? osg::StateAttribute::ON : osg::StateAttribute::OFF); - osg::ref_ptr depth = new osg::Depth; - depth->setWriteMask(zprop->depthWrite()); - // Morrowind ignores depth test function, unless a NiStencilProperty is present, in which case it - // uses a fixed depth function of GL_ALWAYS. - if (hasStencilProperty) - depth->setFunction(osg::Depth::ALWAYS); - depth = shareAttribute(depth); - stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); + // The test function from this property seems to be ignored. + handleDepthFlags(stateset, zprop->depthTest(), zprop->depthWrite()); break; } // OSG groups the material properties that NIFs have separate, so we have to parse them all again when @@ -2351,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: @@ -2367,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: @@ -2405,6 +2411,7 @@ namespace NifOsg } stateset->addUniform(new osg::Uniform("useFalloff", useFalloff)); handleTextureControllers(texprop, composite, imageManager, stateset, animflags); + handleDepthFlags(stateset, texprop->depthTest(), texprop->depthWrite()); break; } case Nif::RC_BSLightingShaderProperty: @@ -2422,6 +2429,9 @@ namespace NifOsg stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); 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: @@ -2477,12 +2487,38 @@ namespace NifOsg handleTextureControllers(texprop, composite, imageManager, stateset, animflags); if (texprop->doubleSided()) stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + 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; } @@ -2563,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()) { @@ -2678,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/lightcontroller.cpp b/components/sceneutil/lightcontroller.cpp index 98af5d21c8..caff6826f5 100644 --- a/components/sceneutil/lightcontroller.cpp +++ b/components/sceneutil/lightcontroller.cpp @@ -36,9 +36,11 @@ namespace SceneUtil // if (time == mLastTime) // return; + osg::Light* light = node->getLight(nv->getTraversalNumber()); + if (mType == LT_Normal) { - node->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor); + light->setDiffuse(mDiffuseColor); traverse(node, nv); return; } @@ -63,7 +65,10 @@ namespace SceneUtil mPhase = mPhase <= 0.5f ? 1.f : 0.25f; } - node->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor * mBrightness * node->getActorFade()); + osg::Vec4f result = mDiffuseColor * mBrightness * node->getActorFade(); + + light->setDiffuse(result); + light->setSpecular(result); traverse(node, nv); } diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp index 6bca92fdb4..8f7304416b 100644 --- a/components/sceneutil/lightmanager.cpp +++ b/components/sceneutil/lightmanager.cpp @@ -536,6 +536,7 @@ namespace SceneUtil configurePosition(lightMat, light->getPosition() * mViewMatrix); configureAmbient(lightMat, light->getAmbient()); configureDiffuse(lightMat, light->getDiffuse()); + configureSpecular(lightMat, light->getSpecular()); configureAttenuation(lightMat, light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), lightList[i]->mLightSource->getRadius()); @@ -1214,6 +1215,7 @@ namespace SceneUtil auto& buf = getUBOManager()->getLightBuffer(frameNum); buf->setDiffuse(index, light->getDiffuse()); buf->setAmbient(index, light->getAmbient()); + buf->setSpecular(index, light->getSpecular()); buf->setAttenuationRadius(index, osg::Vec4(light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), lightSource->getRadius())); diff --git a/components/sceneutil/lightutil.cpp b/components/sceneutil/lightutil.cpp index 9c0ebdf5c2..f69461fa3c 100644 --- a/components/sceneutil/lightutil.cpp +++ b/components/sceneutil/lightutil.cpp @@ -124,7 +124,7 @@ namespace SceneUtil } light->setDiffuse(diffuse); light->setAmbient(ambient); - light->setSpecular(osg::Vec4f(0, 0, 0, 0)); + light->setSpecular(diffuse); // ESM format doesn't provide specular lightSource->setLight(light); 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/sceneutil/shadow.cpp b/components/sceneutil/shadow.cpp index f2748d70f1..04f3b65edd 100644 --- a/components/sceneutil/shadow.cpp +++ b/components/sceneutil/shadow.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include #include "mwshadowtechnique.hpp" @@ -13,9 +13,10 @@ namespace SceneUtil { using namespace osgShadow; - void ShadowManager::setupShadowSettings(Shader::ShaderManager& shaderManager) + void ShadowManager::setupShadowSettings( + const Settings::ShadowsCategory& settings, Shader::ShaderManager& shaderManager) { - mEnableShadows = Settings::Manager::getBool("enable shadows", "Shadows"); + mEnableShadows = settings.mEnableShadows; if (!mEnableShadows) { @@ -28,64 +29,58 @@ namespace SceneUtil mShadowSettings->setLightNum(0); mShadowSettings->setReceivesShadowTraversalMask(~0u); - const int numberOfShadowMapsPerLight - = std::clamp(Settings::Manager::getInt("number of shadow maps", "Shadows"), 1, 8); + const int numberOfShadowMapsPerLight = settings.mNumberOfShadowMaps; mShadowSettings->setNumShadowMapsPerLight(numberOfShadowMapsPerLight); mShadowSettings->setBaseShadowTextureUnit(shaderManager.reserveGlobalTextureUnits( Shader::ShaderManager::Slot::ShadowMaps, numberOfShadowMapsPerLight)); - const float maximumShadowMapDistance = Settings::Manager::getFloat("maximum shadow map distance", "Shadows"); + const float maximumShadowMapDistance = settings.mMaximumShadowMapDistance; if (maximumShadowMapDistance > 0) { - const float shadowFadeStart - = std::clamp(Settings::Manager::getFloat("shadow fade start", "Shadows"), 0.f, 1.f); + const float shadowFadeStart = settings.mShadowFadeStart; mShadowSettings->setMaximumShadowMapDistance(maximumShadowMapDistance); mShadowTechnique->setShadowFadeStart(maximumShadowMapDistance * shadowFadeStart); } - mShadowSettings->setMinimumShadowMapNearFarRatio( - Settings::Manager::getFloat("minimum lispsm near far ratio", "Shadows")); + mShadowSettings->setMinimumShadowMapNearFarRatio(settings.mMinimumLispsmNearFarRatio); - const std::string& computeSceneBounds = Settings::Manager::getString("compute scene bounds", "Shadows"); + const std::string& computeSceneBounds = settings.mComputeSceneBounds; if (Misc::StringUtils::ciEqual(computeSceneBounds, "primitives")) mShadowSettings->setComputeNearFarModeOverride(osg::CullSettings::COMPUTE_NEAR_FAR_USING_PRIMITIVES); else if (Misc::StringUtils::ciEqual(computeSceneBounds, "bounds")) mShadowSettings->setComputeNearFarModeOverride(osg::CullSettings::COMPUTE_NEAR_FAR_USING_BOUNDING_VOLUMES); - int mapres = Settings::Manager::getInt("shadow map resolution", "Shadows"); + const int mapres = settings.mShadowMapResolution; mShadowSettings->setTextureSize(osg::Vec2s(mapres, mapres)); - mShadowTechnique->setSplitPointUniformLogarithmicRatio( - Settings::Manager::getFloat("split point uniform logarithmic ratio", "Shadows")); - mShadowTechnique->setSplitPointDeltaBias(Settings::Manager::getFloat("split point bias", "Shadows")); + mShadowTechnique->setSplitPointUniformLogarithmicRatio(settings.mSplitPointUniformLogarithmicRatio); + mShadowTechnique->setSplitPointDeltaBias(settings.mSplitPointBias); - mShadowTechnique->setPolygonOffset(Settings::Manager::getFloat("polygon offset factor", "Shadows"), - Settings::Manager::getFloat("polygon offset units", "Shadows")); + mShadowTechnique->setPolygonOffset(settings.mPolygonOffsetFactor, settings.mPolygonOffsetUnits); - if (Settings::Manager::getBool("use front face culling", "Shadows")) + if (settings.mUseFrontFaceCulling) mShadowTechnique->enableFrontFaceCulling(); else mShadowTechnique->disableFrontFaceCulling(); - if (Settings::Manager::getBool("allow shadow map overlap", "Shadows")) + if (settings.mAllowShadowMapOverlap) mShadowSettings->setMultipleShadowMapHint(osgShadow::ShadowSettings::CASCADED); else mShadowSettings->setMultipleShadowMapHint(osgShadow::ShadowSettings::PARALLEL_SPLIT); - if (Settings::Manager::getBool("enable debug hud", "Shadows")) + if (settings.mEnableDebugHud) mShadowTechnique->enableDebugHUD(); else mShadowTechnique->disableDebugHUD(); } - void ShadowManager::disableShadowsForStateSet(osg::ref_ptr stateset) + void ShadowManager::disableShadowsForStateSet(const Settings::ShadowsCategory& settings, osg::StateSet& stateset) { - if (!Settings::Manager::getBool("enable shadows", "Shadows")) + if (!settings.mEnableShadows) return; - const int numberOfShadowMapsPerLight - = std::clamp(Settings::Manager::getInt("number of shadow maps", "Shadows"), 1, 8); + const int numberOfShadowMapsPerLight = settings.mNumberOfShadowMaps; int baseShadowTextureUnit = 8 - numberOfShadowMapsPerLight; @@ -99,18 +94,18 @@ namespace SceneUtil fakeShadowMapTexture->setShadowCompareFunc(osg::Texture::ShadowCompareFunc::ALWAYS); for (int i = baseShadowTextureUnit; i < baseShadowTextureUnit + numberOfShadowMapsPerLight; ++i) { - stateset->setTextureAttributeAndModes(i, fakeShadowMapTexture, + stateset.setTextureAttributeAndModes(i, fakeShadowMapTexture, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED); - stateset->addUniform( + stateset.addUniform( new osg::Uniform(("shadowTexture" + std::to_string(i - baseShadowTextureUnit)).c_str(), i)); - stateset->addUniform( + stateset.addUniform( new osg::Uniform(("shadowTextureUnit" + std::to_string(i - baseShadowTextureUnit)).c_str(), i)); } } ShadowManager::ShadowManager(osg::ref_ptr sceneRoot, osg::ref_ptr rootNode, unsigned int outdoorShadowCastingMask, unsigned int indoorShadowCastingMask, unsigned int worldMask, - Shader::ShaderManager& shaderManager) + const Settings::ShadowsCategory& settings, Shader::ShaderManager& shaderManager) : mShadowedScene(new osgShadow::ShadowedScene) , mShadowTechnique(new MWShadowTechnique) , mOutdoorShadowCastingMask(outdoorShadowCastingMask) @@ -126,7 +121,7 @@ namespace SceneUtil mShadowedScene->setNodeMask(sceneRoot->getNodeMask()); mShadowSettings = mShadowedScene->getShadowSettings(); - setupShadowSettings(shaderManager); + setupShadowSettings(settings, shaderManager); mShadowTechnique->setupCastingShader(shaderManager); mShadowTechnique->setWorldMask(worldMask); @@ -140,7 +135,7 @@ namespace SceneUtil Stereo::Manager::instance().setShadowTechnique(nullptr); } - Shader::ShaderManager::DefineMap ShadowManager::getShadowDefines() + Shader::ShaderManager::DefineMap ShadowManager::getShadowDefines(const Settings::ShadowsCategory& settings) { if (!mEnableShadows) return getShadowsDisabledDefines(); @@ -155,24 +150,19 @@ namespace SceneUtil definesWithShadows["shadow_texture_unit_list"] = definesWithShadows["shadow_texture_unit_list"].substr( 0, definesWithShadows["shadow_texture_unit_list"].length() - 1); - definesWithShadows["shadowMapsOverlap"] - = Settings::Manager::getBool("allow shadow map overlap", "Shadows") ? "1" : "0"; + definesWithShadows["shadowMapsOverlap"] = settings.mAllowShadowMapOverlap ? "1" : "0"; - definesWithShadows["useShadowDebugOverlay"] - = Settings::Manager::getBool("enable debug overlay", "Shadows") ? "1" : "0"; + definesWithShadows["useShadowDebugOverlay"] = settings.mEnableDebugOverlay ? "1" : "0"; // switch this to reading settings if it's ever exposed to the user definesWithShadows["perspectiveShadowMaps"] = mShadowSettings->getShadowMapProjectionHint() == ShadowSettings::PERSPECTIVE_SHADOW_MAP ? "1" : "0"; - definesWithShadows["disableNormalOffsetShadows"] - = Settings::Manager::getFloat("normal offset distance", "Shadows") == 0.0 ? "1" : "0"; + definesWithShadows["disableNormalOffsetShadows"] = settings.mNormalOffsetDistance == 0.0 ? "1" : "0"; - definesWithShadows["shadowNormalOffset"] - = std::to_string(Settings::Manager::getFloat("normal offset distance", "Shadows")); + definesWithShadows["shadowNormalOffset"] = std::to_string(settings.mNormalOffsetDistance); - definesWithShadows["limitShadowMapDistance"] - = Settings::Manager::getFloat("maximum shadow map distance", "Shadows") > 0 ? "1" : "0"; + definesWithShadows["limitShadowMapDistance"] = settings.mMaximumShadowMapDistance > 0 ? "1" : "0"; return definesWithShadows; } @@ -200,9 +190,9 @@ namespace SceneUtil return definesWithoutShadows; } - void ShadowManager::enableIndoorMode() + void ShadowManager::enableIndoorMode(const Settings::ShadowsCategory& settings) { - if (Settings::Manager::getBool("enable indoor shadows", "Shadows")) + if (settings.mEnableIndoorShadows) mShadowSettings->setCastsShadowTraversalMask(mIndoorShadowCastingMask); else mShadowTechnique->disableShadows(true); diff --git a/components/sceneutil/shadow.hpp b/components/sceneutil/shadow.hpp index 76fd2b7fdd..fd82e828b6 100644 --- a/components/sceneutil/shadow.hpp +++ b/components/sceneutil/shadow.hpp @@ -8,32 +8,38 @@ namespace osg class StateSet; class Group; } + namespace osgShadow { class ShadowSettings; class ShadowedScene; } +namespace Settings +{ + struct ShadowsCategory; +} + namespace SceneUtil { class MWShadowTechnique; class ShadowManager { public: - static void disableShadowsForStateSet(osg::ref_ptr stateSet); + static void disableShadowsForStateSet(const Settings::ShadowsCategory& settings, osg::StateSet& stateset); static Shader::ShaderManager::DefineMap getShadowsDisabledDefines(); - ShadowManager(osg::ref_ptr sceneRoot, osg::ref_ptr rootNode, + explicit ShadowManager(osg::ref_ptr sceneRoot, osg::ref_ptr rootNode, unsigned int outdoorShadowCastingMask, unsigned int indoorShadowCastingMask, unsigned int worldMask, - Shader::ShaderManager& shaderManager); + const Settings::ShadowsCategory& settings, Shader::ShaderManager& shaderManager); ~ShadowManager(); - void setupShadowSettings(Shader::ShaderManager& shaderManager); + void setupShadowSettings(const Settings::ShadowsCategory& settings, Shader::ShaderManager& shaderManager); - Shader::ShaderManager::DefineMap getShadowDefines(); + Shader::ShaderManager::DefineMap getShadowDefines(const Settings::ShadowsCategory& settings); - void enableIndoorMode(); + void enableIndoorMode(const Settings::ShadowsCategory& settings); void enableOutdoorMode(); diff --git a/components/sdlutil/sdlgraphicswindow.cpp b/components/sdlutil/sdlgraphicswindow.cpp index 3c2efc3728..36947460df 100644 --- a/components/sdlutil/sdlgraphicswindow.cpp +++ b/components/sdlutil/sdlgraphicswindow.cpp @@ -14,22 +14,16 @@ namespace SDLUtil close(true); } - GraphicsWindowSDL2::GraphicsWindowSDL2(osg::GraphicsContext::Traits* traits, int vsync) + GraphicsWindowSDL2::GraphicsWindowSDL2(osg::GraphicsContext::Traits* traits, VSyncMode vsyncMode) : mWindow(nullptr) , mContext(nullptr) , mValid(false) , mRealized(false) , mOwnsWindow(false) + , mVSyncMode(vsyncMode) { _traits = traits; - if (vsync == 2) - mVSyncMode = VSyncMode::Adaptive; - else if (vsync == 1) - mVSyncMode = VSyncMode::Enabled; - else - mVSyncMode = VSyncMode::Disabled; - init(); if (GraphicsWindowSDL2::valid()) { diff --git a/components/sdlutil/sdlgraphicswindow.hpp b/components/sdlutil/sdlgraphicswindow.hpp index 3af6ef9276..238c872fb9 100644 --- a/components/sdlutil/sdlgraphicswindow.hpp +++ b/components/sdlutil/sdlgraphicswindow.hpp @@ -5,14 +5,10 @@ #include +#include "vsyncmode.hpp" + namespace SDLUtil { - enum VSyncMode - { - Disabled = 0, - Enabled = 1, - Adaptive = 2 - }; class GraphicsWindowSDL2 : public osgViewer::GraphicsWindow { @@ -29,7 +25,7 @@ namespace SDLUtil virtual ~GraphicsWindowSDL2(); public: - GraphicsWindowSDL2(osg::GraphicsContext::Traits* traits, int vsync); + GraphicsWindowSDL2(osg::GraphicsContext::Traits* traits, VSyncMode vsyncMode); bool isSameKindAs(const Object* object) const override { diff --git a/components/sdlutil/sdlinputwrapper.cpp b/components/sdlutil/sdlinputwrapper.cpp index 07cab33ad3..cc9706732e 100644 --- a/components/sdlutil/sdlinputwrapper.cpp +++ b/components/sdlutil/sdlinputwrapper.cpp @@ -1,7 +1,7 @@ #include "sdlinputwrapper.hpp" #include -#include +#include #include @@ -187,8 +187,10 @@ namespace SDLUtil { case SDL_DISPLAYEVENT_ORIENTATION: if (mSensorListener - && evt.display.display == (unsigned int)Settings::Manager::getInt("screen", "Video")) + && evt.display.display == static_cast(Settings::video().mScreen)) + { mSensorListener->displayOrientationChanged(); + } break; default: break; diff --git a/components/sdlutil/sdlvideowrapper.cpp b/components/sdlutil/sdlvideowrapper.cpp index 3b612214b8..d93c16aace 100644 --- a/components/sdlutil/sdlvideowrapper.cpp +++ b/components/sdlutil/sdlvideowrapper.cpp @@ -30,14 +30,8 @@ namespace SDLUtil SDL_SetWindowGammaRamp(mWindow, mOldSystemGammaRamp, &mOldSystemGammaRamp[256], &mOldSystemGammaRamp[512]); } - void VideoWrapper::setSyncToVBlank(int mode) + void VideoWrapper::setSyncToVBlank(VSyncMode vsyncMode) { - VSyncMode vsyncMode = VSyncMode::Disabled; - if (mode == 1) - vsyncMode = VSyncMode::Enabled; - else if (mode == 2) - vsyncMode = VSyncMode::Adaptive; - osgViewer::Viewer::Windows windows; mViewer->getWindows(windows); mViewer->stopThreading(); @@ -47,7 +41,7 @@ namespace SDLUtil if (GraphicsWindowSDL2* sdl2win = dynamic_cast(win)) sdl2win->setSyncToVBlank(vsyncMode); else - win->setSyncToVBlank(static_cast(mode)); + win->setSyncToVBlank(vsyncMode != VSyncMode::Disabled); } mViewer->startThreading(); } diff --git a/components/sdlutil/sdlvideowrapper.hpp b/components/sdlutil/sdlvideowrapper.hpp index 9ed6ff1252..7977de40a7 100644 --- a/components/sdlutil/sdlvideowrapper.hpp +++ b/components/sdlutil/sdlvideowrapper.hpp @@ -5,6 +5,8 @@ #include +#include "vsyncmode.hpp" + struct SDL_Window; namespace osgViewer @@ -26,7 +28,7 @@ namespace SDLUtil VideoWrapper(SDL_Window* window, osg::ref_ptr viewer); ~VideoWrapper(); - void setSyncToVBlank(int mode); + void setSyncToVBlank(VSyncMode vsyncMode); void setGammaContrast(float gamma, float contrast); diff --git a/components/sdlutil/vsyncmode.hpp b/components/sdlutil/vsyncmode.hpp new file mode 100644 index 0000000000..5156addc40 --- /dev/null +++ b/components/sdlutil/vsyncmode.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_COMPONENTS_SDLUTIL_VSYNCMODE_H +#define OPENMW_COMPONENTS_SDLUTIL_VSYNCMODE_H + +namespace SDLUtil +{ + enum VSyncMode + { + Disabled = 0, + Enabled = 1, + Adaptive = 2 + }; +} + +#endif diff --git a/components/settings/categories/camera.hpp b/components/settings/categories/camera.hpp index 4712f88b45..5b3a9b742b 100644 --- a/components/settings/categories/camera.hpp +++ b/components/settings/categories/camera.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_CAMERA_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_CAMERA_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include diff --git a/components/settings/categories/cells.hpp b/components/settings/categories/cells.hpp index 723004d674..86fe944acf 100644 --- a/components/settings/categories/cells.hpp +++ b/components/settings/categories/cells.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_CELLS_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_CELLS_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include diff --git a/components/settings/categories/fog.hpp b/components/settings/categories/fog.hpp index 5acf3d20c6..3bc75587f1 100644 --- a/components/settings/categories/fog.hpp +++ b/components/settings/categories/fog.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_FOG_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_FOG_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include diff --git a/components/settings/categories/game.hpp b/components/settings/categories/game.hpp index ded367a54c..4aec92d0b8 100644 --- a/components/settings/categories/game.hpp +++ b/components/settings/categories/game.hpp @@ -1,9 +1,9 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_GAME_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_GAME_H -#include "components/detournavigator/collisionshapetype.hpp" -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include +#include #include #include @@ -74,6 +74,7 @@ namespace Settings "unarmed creature attacks damage armor" }; SettingValue mActorCollisionShapeType{ mIndex, "Game", "actor collision shape type" }; + SettingValue mPlayerMovementIgnoresAnimation{ mIndex, "Game", "player movement ignores animation" }; }; } diff --git a/components/settings/categories/general.hpp b/components/settings/categories/general.hpp index 7bbb651ee8..a79ef83ea0 100644 --- a/components/settings/categories/general.hpp +++ b/components/settings/categories/general.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_GENERAL_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_GENERAL_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include diff --git a/components/settings/categories/groundcover.hpp b/components/settings/categories/groundcover.hpp index 48f97037ae..78615a4e76 100644 --- a/components/settings/categories/groundcover.hpp +++ b/components/settings/categories/groundcover.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_GROUNDCOVER_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_GROUNDCOVER_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include diff --git a/components/settings/categories/gui.hpp b/components/settings/categories/gui.hpp index 1d3a56af39..4a5e50fd8a 100644 --- a/components/settings/categories/gui.hpp +++ b/components/settings/categories/gui.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_GUI_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_GUI_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include @@ -28,10 +28,8 @@ namespace Settings SettingValue mSubtitles{ mIndex, "GUI", "subtitles" }; SettingValue mHitFader{ mIndex, "GUI", "hit fader" }; SettingValue mWerewolfOverlay{ mIndex, "GUI", "werewolf overlay" }; - SettingValue mColorBackgroundOwned{ mIndex, "GUI", "color background owned", - makeClampSanitizerFloat(0, 1) }; - SettingValue mColorCrosshairOwned{ mIndex, "GUI", "color crosshair owned", - makeClampSanitizerFloat(0, 1) }; + SettingValue mColorBackgroundOwned{ mIndex, "GUI", "color background owned" }; + SettingValue mColorCrosshairOwned{ mIndex, "GUI", "color crosshair owned" }; SettingValue mKeyboardNavigation{ mIndex, "GUI", "keyboard navigation" }; SettingValue mColorTopicEnable{ mIndex, "GUI", "color topic enable" }; SettingValue mColorTopicSpecific{ mIndex, "GUI", "color topic specific" }; diff --git a/components/settings/categories/hud.hpp b/components/settings/categories/hud.hpp index e97a81501d..fe05e39eda 100644 --- a/components/settings/categories/hud.hpp +++ b/components/settings/categories/hud.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_HUD_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_HUD_H -#include "components/settings/settingvalue.hpp" +#include #include #include diff --git a/components/settings/categories/input.hpp b/components/settings/categories/input.hpp index 1e59b45334..0a450f1dcd 100644 --- a/components/settings/categories/input.hpp +++ b/components/settings/categories/input.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_INPUT_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_INPUT_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include diff --git a/components/settings/categories/lua.hpp b/components/settings/categories/lua.hpp index da11a605eb..88d74b2d1f 100644 --- a/components/settings/categories/lua.hpp +++ b/components/settings/categories/lua.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_LUA_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_LUA_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include diff --git a/components/settings/categories/map.hpp b/components/settings/categories/map.hpp index bdccd6f205..8a80d5aa63 100644 --- a/components/settings/categories/map.hpp +++ b/components/settings/categories/map.hpp @@ -1,9 +1,9 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_MAP_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_MAP_H -#include "components/misc/constants.hpp" -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include +#include #include #include diff --git a/components/settings/categories/models.hpp b/components/settings/categories/models.hpp index fec8da7775..0d26eeba5f 100644 --- a/components/settings/categories/models.hpp +++ b/components/settings/categories/models.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_MODELS_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_MODELS_H -#include "components/settings/settingvalue.hpp" +#include #include #include diff --git a/components/settings/categories/navigator.hpp b/components/settings/categories/navigator.hpp index b820b2c950..d6d7adcd56 100644 --- a/components/settings/categories/navigator.hpp +++ b/components/settings/categories/navigator.hpp @@ -1,9 +1,9 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_NAVIGATOR_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_NAVIGATOR_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" #include +#include +#include #include #include diff --git a/components/settings/categories/physics.hpp b/components/settings/categories/physics.hpp index 41005a06cf..4720708db2 100644 --- a/components/settings/categories/physics.hpp +++ b/components/settings/categories/physics.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_PHYSICS_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_PHYSICS_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include diff --git a/components/settings/categories/postprocessing.hpp b/components/settings/categories/postprocessing.hpp index 04810b847c..7f5ddfba3e 100644 --- a/components/settings/categories/postprocessing.hpp +++ b/components/settings/categories/postprocessing.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_POSTPROCESSING_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_POSTPROCESSING_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include diff --git a/components/settings/categories/saves.hpp b/components/settings/categories/saves.hpp index e565d98564..3a64785412 100644 --- a/components/settings/categories/saves.hpp +++ b/components/settings/categories/saves.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_SAVES_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_SAVES_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include diff --git a/components/settings/categories/shaders.hpp b/components/settings/categories/shaders.hpp index 7efb891822..dce2531c1e 100644 --- a/components/settings/categories/shaders.hpp +++ b/components/settings/categories/shaders.hpp @@ -1,9 +1,9 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_SHADERS_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_SHADERS_H -#include "components/sceneutil/lightingmethod.hpp" -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include +#include #include #include diff --git a/components/settings/categories/shadows.hpp b/components/settings/categories/shadows.hpp index d716272bce..0da6f649c4 100644 --- a/components/settings/categories/shadows.hpp +++ b/components/settings/categories/shadows.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_SHADOWS_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_SHADOWS_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include diff --git a/components/settings/categories/sound.hpp b/components/settings/categories/sound.hpp index 43313e622d..995bce2a58 100644 --- a/components/settings/categories/sound.hpp +++ b/components/settings/categories/sound.hpp @@ -1,9 +1,9 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_SOUND_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_SOUND_H -#include "components/settings/hrtfmode.hpp" -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include +#include #include diff --git a/components/settings/categories/stereo.hpp b/components/settings/categories/stereo.hpp index 98fae8693f..aa903c5b53 100644 --- a/components/settings/categories/stereo.hpp +++ b/components/settings/categories/stereo.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_STEREO_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_STEREO_H -#include "components/settings/settingvalue.hpp" +#include #include #include diff --git a/components/settings/categories/stereoview.hpp b/components/settings/categories/stereoview.hpp index 1e9d35ace8..7f08d9bc35 100644 --- a/components/settings/categories/stereoview.hpp +++ b/components/settings/categories/stereoview.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_STEREOVIEW_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_STEREOVIEW_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include @@ -31,14 +31,14 @@ namespace Settings makeClampSanitizerDouble(-1, 1) }; SettingValue mLeftEyeOrientationW{ mIndex, "Stereo View", "left eye orientation w", makeClampSanitizerDouble(-1, 1) }; - SettingValue mLeftEyeFovLeft{ mIndex, "Stereo View", "left eye fov left", - makeClampSanitizerDouble(-osg::PI, osg::PI) }; - SettingValue mLeftEyeFovRight{ mIndex, "Stereo View", "left eye fov right", - makeClampSanitizerDouble(-osg::PI, osg::PI) }; - SettingValue mLeftEyeFovUp{ mIndex, "Stereo View", "left eye fov up", - makeClampSanitizerDouble(-osg::PI, osg::PI) }; - SettingValue mLeftEyeFovDown{ mIndex, "Stereo View", "left eye fov down", - makeClampSanitizerDouble(-osg::PI, osg::PI) }; + SettingValue mLeftEyeFovLeft{ mIndex, "Stereo View", "left eye fov left", + makeClampSanitizerFloat(-osg::PIf, osg::PIf) }; + SettingValue mLeftEyeFovRight{ mIndex, "Stereo View", "left eye fov right", + makeClampSanitizerFloat(-osg::PIf, osg::PIf) }; + SettingValue mLeftEyeFovUp{ mIndex, "Stereo View", "left eye fov up", + makeClampSanitizerFloat(-osg::PIf, osg::PIf) }; + SettingValue mLeftEyeFovDown{ mIndex, "Stereo View", "left eye fov down", + makeClampSanitizerFloat(-osg::PIf, osg::PIf) }; SettingValue mRightEyeOffsetX{ mIndex, "Stereo View", "right eye offset x" }; SettingValue mRightEyeOffsetY{ mIndex, "Stereo View", "right eye offset y" }; SettingValue mRightEyeOffsetZ{ mIndex, "Stereo View", "right eye offset z" }; @@ -50,14 +50,14 @@ namespace Settings makeClampSanitizerDouble(-1, 1) }; SettingValue mRightEyeOrientationW{ mIndex, "Stereo View", "right eye orientation w", makeClampSanitizerDouble(-1, 1) }; - SettingValue mRightEyeFovLeft{ mIndex, "Stereo View", "right eye fov left", - makeClampSanitizerDouble(-osg::PI, osg::PI) }; - SettingValue mRightEyeFovRight{ mIndex, "Stereo View", "right eye fov right", - makeClampSanitizerDouble(-osg::PI, osg::PI) }; - SettingValue mRightEyeFovUp{ mIndex, "Stereo View", "right eye fov up", - makeClampSanitizerDouble(-osg::PI, osg::PI) }; - SettingValue mRightEyeFovDown{ mIndex, "Stereo View", "right eye fov down", - makeClampSanitizerDouble(-osg::PI, osg::PI) }; + SettingValue mRightEyeFovLeft{ mIndex, "Stereo View", "right eye fov left", + makeClampSanitizerFloat(-osg::PIf, osg::PIf) }; + SettingValue mRightEyeFovRight{ mIndex, "Stereo View", "right eye fov right", + makeClampSanitizerFloat(-osg::PIf, osg::PIf) }; + SettingValue mRightEyeFovUp{ mIndex, "Stereo View", "right eye fov up", + makeClampSanitizerFloat(-osg::PIf, osg::PIf) }; + SettingValue mRightEyeFovDown{ mIndex, "Stereo View", "right eye fov down", + makeClampSanitizerFloat(-osg::PIf, osg::PIf) }; }; } diff --git a/components/settings/categories/terrain.hpp b/components/settings/categories/terrain.hpp index c2eef9dc20..f26cc264b8 100644 --- a/components/settings/categories/terrain.hpp +++ b/components/settings/categories/terrain.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_TERRAIN_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_TERRAIN_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include diff --git a/components/settings/categories/video.hpp b/components/settings/categories/video.hpp index fd6bb0018a..cb12ea079c 100644 --- a/components/settings/categories/video.hpp +++ b/components/settings/categories/video.hpp @@ -1,8 +1,11 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_VIDEO_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_VIDEO_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include +#include +#include +#include #include #include @@ -20,16 +23,16 @@ namespace Settings SettingValue mResolutionX{ mIndex, "Video", "resolution x", makeMaxSanitizerInt(1) }; SettingValue mResolutionY{ mIndex, "Video", "resolution y", makeMaxSanitizerInt(1) }; - SettingValue mWindowMode{ mIndex, "Video", "window mode", makeEnumSanitizerInt({ 0, 1, 2 }) }; + SettingValue mWindowMode{ mIndex, "Video", "window mode" }; SettingValue mScreen{ mIndex, "Video", "screen", makeMaxSanitizerInt(0) }; SettingValue mMinimizeOnFocusLoss{ mIndex, "Video", "minimize on focus loss" }; SettingValue mWindowBorder{ mIndex, "Video", "window border" }; SettingValue mAntialiasing{ mIndex, "Video", "antialiasing", makeMaxSanitizerInt(0) }; - SettingValue mVsyncMode{ mIndex, "Video", "vsync mode", makeEnumSanitizerInt({ 0, 1, 2 }) }; + SettingValue mVsyncMode{ mIndex, "Video", "vsync mode" }; SettingValue mFramerateLimit{ mIndex, "Video", "framerate limit", makeMaxSanitizerFloat(0) }; SettingValue mContrast{ mIndex, "Video", "contrast", makeMaxStrictSanitizerFloat(0) }; SettingValue mGamma{ mIndex, "Video", "gamma", makeMaxStrictSanitizerFloat(0) }; - SettingValue mScreenshotType{ mIndex, "Video", "screenshot type" }; + SettingValue mScreenshotType{ mIndex, "Video", "screenshot type" }; }; } diff --git a/components/settings/categories/water.hpp b/components/settings/categories/water.hpp index b88d38b023..2e04114244 100644 --- a/components/settings/categories/water.hpp +++ b/components/settings/categories/water.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_WATER_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_WATER_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include +#include #include #include diff --git a/components/settings/categories/windows.hpp b/components/settings/categories/windows.hpp index 1fc249cc97..2f22e751e6 100644 --- a/components/settings/categories/windows.hpp +++ b/components/settings/categories/windows.hpp @@ -1,8 +1,7 @@ #ifndef OPENMW_COMPONENTS_SETTINGS_CATEGORIES_WINDOWS_H #define OPENMW_COMPONENTS_SETTINGS_CATEGORIES_WINDOWS_H -#include "components/settings/sanitizerimpl.hpp" -#include "components/settings/settingvalue.hpp" +#include #include #include diff --git a/components/settings/screenshotsettings.hpp b/components/settings/screenshotsettings.hpp new file mode 100644 index 0000000000..6475ace005 --- /dev/null +++ b/components/settings/screenshotsettings.hpp @@ -0,0 +1,29 @@ +#ifndef OPENMW_COMPONENTS_SETTINGS_SCREENSHOTSETTINGS_H +#define OPENMW_COMPONENTS_SETTINGS_SCREENSHOTSETTINGS_H + +#include +#include + +namespace Settings +{ + enum class ScreenshotType + { + Regular, + Cylindrical, + Spherical, + Planet, + Cubemap, + }; + + struct ScreenshotSettings + { + ScreenshotType mType; + std::optional mWidth; + std::optional mHeight; + std::optional mCubeSize; + + auto operator<=>(const ScreenshotSettings& value) const = default; + }; +} + +#endif diff --git a/components/settings/settings.cpp b/components/settings/settings.cpp index 7b31bf6aad..bdbd0f7353 100644 --- a/components/settings/settings.cpp +++ b/components/settings/settings.cpp @@ -119,6 +119,23 @@ namespace Settings Log(Debug::Warning) << "Invalid HRTF mode value: " << static_cast(value) << ", fallback to auto (-1)"; return -1; } + + ScreenshotType parseScreenshotType(std::string_view value) + { + if (value == "regular") + return ScreenshotType::Regular; + if (value == "spherical") + return ScreenshotType::Spherical; + if (value == "cylindrical") + return ScreenshotType::Cylindrical; + if (value == "planet") + return ScreenshotType::Planet; + if (value == "cubemap") + return ScreenshotType::Cubemap; + + Log(Debug::Warning) << "Invalid screenshot type: " << value << ", fallback to regular"; + return ScreenshotType::Regular; + } } CategorySettingValueMap Manager::mDefaultSettings = CategorySettingValueMap(); @@ -128,6 +145,8 @@ namespace Settings void Manager::clear() { + sInitialized.clear(); + StaticValues::clear(); mDefaultSettings.clear(); mUserSettings.clear(); mChangedSettings.clear(); @@ -501,6 +520,16 @@ namespace Settings setInt(setting, category, toInt(value)); } + void Manager::set(std::string_view setting, std::string_view category, WindowMode value) + { + setInt(setting, category, static_cast(value)); + } + + void Manager::set(std::string_view setting, std::string_view category, SDLUtil::VSyncMode value) + { + setInt(setting, category, static_cast(value)); + } + void Manager::recordInit(std::string_view setting, std::string_view category) { sInitialized.emplace(category, setting); @@ -547,4 +576,28 @@ namespace Settings Log(Debug::Warning) << "Unknown lighting method '" << value << "', returning fallback '" << fallback << "'"; return SceneUtil::LightingMethod::PerObjectUniform; } + + ScreenshotSettings parseScreenshotSettings(std::string_view value) + { + std::vector settingArgs; + Misc::StringUtils::split(value, settingArgs); + + ScreenshotSettings result; + + if (settingArgs.size() > 0) + result.mType = parseScreenshotType(settingArgs[0]); + else + result.mType = ScreenshotType::Regular; + + if (settingArgs.size() > 1) + result.mWidth = std::min(10000, Misc::StringUtils::toNumeric(settingArgs[1], 0)); + + if (settingArgs.size() > 2) + result.mHeight = std::min(10000, Misc::StringUtils::toNumeric(settingArgs[2], 0)); + + if (settingArgs.size() > 3) + result.mCubeSize = std::min(5000, Misc::StringUtils::toNumeric(settingArgs[3], 0)); + + return result; + } } diff --git a/components/settings/settings.hpp b/components/settings/settings.hpp index a5b75fd445..bcdcbb16d1 100644 --- a/components/settings/settings.hpp +++ b/components/settings/settings.hpp @@ -5,9 +5,12 @@ #include "gyroscopeaxis.hpp" #include "hrtfmode.hpp" #include "navmeshrendermode.hpp" +#include "screenshotsettings.hpp" +#include "windowmode.hpp" #include #include +#include #include #include @@ -27,13 +30,6 @@ namespace Files namespace Settings { - enum class WindowMode - { - Fullscreen = 0, - WindowedFullscreen, - Windowed - }; - /// /// \brief Settings management (can change during runtime) /// @@ -81,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) { @@ -114,6 +119,8 @@ namespace Settings static void set(std::string_view setting, std::string_view category, const MyGUI::Colour& value); static void set(std::string_view setting, std::string_view category, SceneUtil::LightingMethod value); static void set(std::string_view setting, std::string_view category, HrtfMode value); + static void set(std::string_view setting, std::string_view category, WindowMode value); + static void set(std::string_view setting, std::string_view category, SDLUtil::VSyncMode value); private: static std::set> sInitialized; @@ -239,6 +246,32 @@ namespace Settings return HrtfMode::Enable; return HrtfMode::Disable; } + + template <> + inline WindowMode Manager::getImpl(std::string_view setting, std::string_view category) + { + const int value = getInt(setting, category); + if (value < 0 || 2 < value) + return WindowMode::Fullscreen; + return static_cast(value); + } + + template <> + inline SDLUtil::VSyncMode Manager::getImpl(std::string_view setting, std::string_view category) + { + const int value = getInt(setting, category); + if (value < 0 || 2 < value) + return SDLUtil::VSyncMode::Disabled; + return static_cast(value); + } + + ScreenshotSettings parseScreenshotSettings(std::string_view value); + + template <> + inline ScreenshotSettings Manager::getImpl(std::string_view setting, std::string_view category) + { + return parseScreenshotSettings(getString(setting, category)); + } } #endif // COMPONENTS_SETTINGS_H diff --git a/components/settings/settingvalue.hpp b/components/settings/settingvalue.hpp index 7b49c9bda2..8183e8c1ac 100644 --- a/components/settings/settingvalue.hpp +++ b/components/settings/settingvalue.hpp @@ -42,6 +42,9 @@ namespace Settings NavMeshRenderMode, LightingMethod, HrtfMode, + WindowMode, + VSyncMode, + ScreenshotSettings, }; template @@ -161,6 +164,24 @@ namespace Settings return SettingValueType::HrtfMode; } + template <> + inline constexpr SettingValueType getSettingValueType() + { + return SettingValueType::WindowMode; + } + + template <> + inline constexpr SettingValueType getSettingValueType() + { + return SettingValueType::VSyncMode; + } + + template <> + inline constexpr SettingValueType getSettingValueType() + { + return SettingValueType::ScreenshotSettings; + } + inline constexpr std::string_view getSettingValueTypeName(SettingValueType type) { switch (type) @@ -203,6 +224,12 @@ namespace Settings return "lighting method"; case SettingValueType::HrtfMode: return "hrtf mode"; + case SettingValueType::WindowMode: + return "window mode"; + case SettingValueType::VSyncMode: + return "vsync mode"; + case SettingValueType::ScreenshotSettings: + return "screenshot settings"; } return "unsupported"; } @@ -308,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)) @@ -361,6 +397,17 @@ namespace Settings } return stream; } + else if constexpr (std::is_same_v) + { + stream << "ScreenshotSettings{ .mType = " << static_cast(value.mValue.mType); + if (value.mValue.mWidth.has_value()) + stream << ", .mWidth = " << *value.mValue.mWidth; + if (value.mValue.mHeight.has_value()) + stream << ", .mHeight = " << *value.mValue.mHeight; + if (value.mValue.mCubeSize.has_value()) + stream << ", .mCubeSize = " << *value.mValue.mCubeSize; + return stream << " }"; + } else return stream << value.mValue; } diff --git a/components/settings/values.cpp b/components/settings/values.cpp index 66460beb9a..a6bbec3df7 100644 --- a/components/settings/values.cpp +++ b/components/settings/values.cpp @@ -4,24 +4,29 @@ namespace Settings { - Index* StaticValues::sIndex = nullptr; - Values* StaticValues::sValues = nullptr; + std::unique_ptr StaticValues::sIndex; + std::unique_ptr StaticValues::sDefaultValues; + std::unique_ptr StaticValues::sValues; void StaticValues::initDefaults() { - if (sValues != nullptr) - throw std::logic_error("Default settings already initialized"); - static Index index; - static Values values(index); - sIndex = &index; - sValues = &values; + if (sDefaultValues != nullptr) + throw std::logic_error("Default settings are already initialized"); + sIndex = std::make_unique(); + sDefaultValues = std::make_unique(*sIndex); } void StaticValues::init() { - if (sValues == nullptr) + if (sDefaultValues == nullptr) throw std::logic_error("Default settings are not initialized"); - static Values values(std::move(*sValues)); - sValues = &values; + sValues = std::make_unique(std::move(*sDefaultValues)); + } + + void StaticValues::clear() + { + sValues = nullptr; + sDefaultValues = nullptr; + sIndex = nullptr; } } diff --git a/components/settings/values.hpp b/components/settings/values.hpp index 323be2e866..af28dc16f8 100644 --- a/components/settings/values.hpp +++ b/components/settings/values.hpp @@ -29,6 +29,7 @@ #include "settingvalue.hpp" #include +#include #include namespace Settings @@ -71,9 +72,12 @@ namespace Settings static void init(); + static void clear(); + private: - static Index* sIndex; - static Values* sValues; + static std::unique_ptr sIndex; + static std::unique_ptr sDefaultValues; + static std::unique_ptr sValues; friend Values& values(); diff --git a/components/settings/windowmode.hpp b/components/settings/windowmode.hpp new file mode 100644 index 0000000000..96a81059f4 --- /dev/null +++ b/components/settings/windowmode.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_COMPONENTS_SETTINGS_WINDOWMODE_H +#define OPENMW_COMPONENTS_SETTINGS_WINDOWMODE_H + +namespace Settings +{ + enum class WindowMode + { + Fullscreen = 0, + WindowedFullscreen = 1, + Windowed = 2, + }; +} + +#endif 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/frustum.cpp b/components/stereo/frustum.cpp index cf8a7e8c30..a35beba7dd 100644 --- a/components/stereo/frustum.cpp +++ b/components/stereo/frustum.cpp @@ -85,7 +85,7 @@ namespace Stereo } } - StereoFrustumManager::StereoFrustumManager(osg::Camera* camera) + StereoFrustumManager::StereoFrustumManager(bool sharedShadowMaps, osg::Camera* camera) : mCamera(camera) , mShadowTechnique(nullptr) , mShadowFrustumCallback(nullptr) @@ -95,7 +95,7 @@ namespace Stereo mMultiviewFrustumCallback = std::make_unique(this, camera); } - if (Settings::Manager::getBool("shared shadow maps", "Stereo")) + if (sharedShadowMaps) { mShadowFrustumCallback = new ShadowFrustumCallback(this); auto* renderer = static_cast(mCamera->getRenderer()); diff --git a/components/stereo/frustum.hpp b/components/stereo/frustum.hpp index c3abdd87d2..35e3adf95a 100644 --- a/components/stereo/frustum.hpp +++ b/components/stereo/frustum.hpp @@ -45,7 +45,7 @@ namespace Stereo class StereoFrustumManager { public: - StereoFrustumManager(osg::Camera* camera); + StereoFrustumManager(bool sharedShadowMaps, osg::Camera* camera); ~StereoFrustumManager(); void update(std::array projections); diff --git a/components/stereo/multiview.cpp b/components/stereo/multiview.cpp index 047a52747b..a2f6cfd626 100644 --- a/components/stereo/multiview.cpp +++ b/components/stereo/multiview.cpp @@ -124,12 +124,12 @@ namespace Stereo } } - void setVertexBufferHint(bool enableMultiview) + void setVertexBufferHint(bool enableMultiview, bool allowDisplayListsForMultiview) { if (getStereo() && enableMultiview) { auto* ds = osg::DisplaySettings::instance().get(); - if (!Settings::Manager::getBool("allow display lists for multiview", "Stereo") + if (!allowDisplayListsForMultiview && ds->getVertexBufferHint() == osg::DisplaySettings::VertexBufferHint::NO_PREFERENCE) { // Note that this only works if this code is executed before realize() is called on the viewer. diff --git a/components/stereo/multiview.hpp b/components/stereo/multiview.hpp index fa69afc7a1..a9d84eae85 100644 --- a/components/stereo/multiview.hpp +++ b/components/stereo/multiview.hpp @@ -37,7 +37,7 @@ namespace Stereo void configureExtensions(unsigned int contextID, bool enableMultiview); //! Sets the appropriate vertex buffer hint on OSG's display settings if needed - void setVertexBufferHint(bool enableMultiview); + void setVertexBufferHint(bool enableMultiview, bool allowDisplayListsForMultiview); //! Creates a Texture2D as a texture view into a Texture2DArray osg::ref_ptr createTextureView_Texture2DFromTexture2DArray( diff --git a/components/stereo/stereomanager.cpp b/components/stereo/stereomanager.cpp index bf82d52078..a2eea0fda2 100644 --- a/components/stereo/stereomanager.cpp +++ b/components/stereo/stereomanager.cpp @@ -114,13 +114,15 @@ namespace Stereo return *sInstance; } - Manager::Manager(osgViewer::Viewer* viewer, bool enableStereo) + Manager::Manager(osgViewer::Viewer* viewer, bool enableStereo, double near, double far) : mViewer(viewer) , mMainCamera(mViewer->getCamera()) , mUpdateCallback(new StereoUpdateCallback(this)) , mMasterProjectionMatrix(osg::Matrixd::identity()) , mEyeResolutionOverriden(false) , mEyeResolutionOverride(0, 0) + , mNear(near) + , mFar(far) , mFrustumManager(nullptr) , mUpdateViewCallback(nullptr) { @@ -132,13 +134,13 @@ namespace Stereo Manager::~Manager() {} - void Manager::initializeStereo(osg::GraphicsContext* gc, bool enableMultiview) + void Manager::initializeStereo(osg::GraphicsContext* gc, bool enableMultiview, bool sharedShadowMaps) { auto ci = gc->getState()->getContextID(); configureExtensions(ci, enableMultiview); mMainCamera->addUpdateCallback(mUpdateCallback); - mFrustumManager = std::make_unique(mViewer->getCamera()); + mFrustumManager = std::make_unique(sharedShadowMaps, mViewer->getCamera()); if (getMultiview()) setupOVRMultiView2Technique(); @@ -273,7 +275,7 @@ namespace Stereo void Manager::updateStereoFramebuffer() { // VR-TODO: in VR, still need to have this framebuffer attached before the postprocessor is created - // auto samples = Settings::Manager::getInt("antialiasing", "Video"); + // auto samples = /*do not use Settings here*/; // auto eyeRes = eyeResolution(); // if (mMultiviewFramebuffer) @@ -289,20 +291,17 @@ namespace Stereo void Manager::update() { - const double near_ = Settings::camera().mNearClip; - const double far_ = Settings::camera().mViewingDistance; - if (mUpdateViewCallback) { mUpdateViewCallback->updateView(mView[0], mView[1]); mViewOffsetMatrix[0] = mView[0].viewMatrix(true); mViewOffsetMatrix[1] = mView[1].viewMatrix(true); - mProjectionMatrix[0] = mView[0].perspectiveMatrix(near_, far_, false); - mProjectionMatrix[1] = mView[1].perspectiveMatrix(near_, far_, false); + mProjectionMatrix[0] = mView[0].perspectiveMatrix(mNear, mFar, false); + mProjectionMatrix[1] = mView[1].perspectiveMatrix(mNear, mFar, false); if (SceneUtil::AutoDepth::isReversed()) { - mProjectionMatrixReverseZ[0] = mView[0].perspectiveMatrix(near_, far_, true); - mProjectionMatrixReverseZ[1] = mView[1].perspectiveMatrix(near_, far_, true); + mProjectionMatrixReverseZ[0] = mView[0].perspectiveMatrix(mNear, mFar, true); + mProjectionMatrixReverseZ[1] = mView[1].perspectiveMatrix(mNear, mFar, true); } View masterView; @@ -310,7 +309,7 @@ namespace Stereo masterView.fov.angleUp = std::max(mView[0].fov.angleUp, mView[1].fov.angleUp); masterView.fov.angleLeft = std::min(mView[0].fov.angleLeft, mView[1].fov.angleLeft); masterView.fov.angleRight = std::max(mView[0].fov.angleRight, mView[1].fov.angleRight); - auto projectionMatrix = masterView.perspectiveMatrix(near_, far_, false); + auto projectionMatrix = masterView.perspectiveMatrix(mNear, mFar, false); mMainCamera->setProjectionMatrix(projectionMatrix); } else @@ -394,60 +393,30 @@ namespace Stereo right = mRight; } - InitializeStereoOperation::InitializeStereoOperation() + InitializeStereoOperation::InitializeStereoOperation(const Settings& settings) : GraphicsOperation("InitializeStereoOperation", false) + , mMultiview(settings.mMultiview) + , mSharedShadowMaps(settings.mSharedShadowMaps) + , mCustomView(settings.mCustomView) + , mEyeResolution(settings.mEyeResolution) { // Ideally, this would have belonged to the operator(). But the vertex buffer // hint has to be set before realize is called on the osg viewer, and so has to // be done here instead. - Stereo::setVertexBufferHint(Settings::Manager::getBool("multiview", "Stereo")); + Stereo::setVertexBufferHint(settings.mMultiview, settings.mAllowDisplayListsForMultiview); } void InitializeStereoOperation::operator()(osg::GraphicsContext* graphicsContext) { auto& sm = Stereo::Manager::instance(); - if (Settings::Manager::getBool("use custom view", "Stereo")) - { - Stereo::View left; - Stereo::View right; + if (mCustomView.has_value()) + sm.setUpdateViewCallback( + std::make_shared(mCustomView->mLeft, mCustomView->mRight)); - left.pose.position.x() = Settings::Manager::getDouble("left eye offset x", "Stereo View"); - left.pose.position.y() = Settings::Manager::getDouble("left eye offset y", "Stereo View"); - left.pose.position.z() = Settings::Manager::getDouble("left eye offset z", "Stereo View"); - left.pose.orientation.x() = Settings::Manager::getDouble("left eye orientation x", "Stereo View"); - left.pose.orientation.y() = Settings::Manager::getDouble("left eye orientation y", "Stereo View"); - left.pose.orientation.z() = Settings::Manager::getDouble("left eye orientation z", "Stereo View"); - left.pose.orientation.w() = Settings::Manager::getDouble("left eye orientation w", "Stereo View"); - left.fov.angleLeft = Settings::Manager::getDouble("left eye fov left", "Stereo View"); - left.fov.angleRight = Settings::Manager::getDouble("left eye fov right", "Stereo View"); - left.fov.angleUp = Settings::Manager::getDouble("left eye fov up", "Stereo View"); - left.fov.angleDown = Settings::Manager::getDouble("left eye fov down", "Stereo View"); + if (mEyeResolution.has_value()) + sm.overrideEyeResolution(*mEyeResolution); - right.pose.position.x() = Settings::Manager::getDouble("right eye offset x", "Stereo View"); - right.pose.position.y() = Settings::Manager::getDouble("right eye offset y", "Stereo View"); - right.pose.position.z() = Settings::Manager::getDouble("right eye offset z", "Stereo View"); - right.pose.orientation.x() = Settings::Manager::getDouble("right eye orientation x", "Stereo View"); - right.pose.orientation.y() = Settings::Manager::getDouble("right eye orientation y", "Stereo View"); - right.pose.orientation.z() = Settings::Manager::getDouble("right eye orientation z", "Stereo View"); - right.pose.orientation.w() = Settings::Manager::getDouble("right eye orientation w", "Stereo View"); - right.fov.angleLeft = Settings::Manager::getDouble("right eye fov left", "Stereo View"); - right.fov.angleRight = Settings::Manager::getDouble("right eye fov right", "Stereo View"); - right.fov.angleUp = Settings::Manager::getDouble("right eye fov up", "Stereo View"); - right.fov.angleDown = Settings::Manager::getDouble("right eye fov down", "Stereo View"); - - auto customViewCallback = std::make_shared(left, right); - sm.setUpdateViewCallback(customViewCallback); - } - - if (Settings::Manager::getBool("use custom eye resolution", "Stereo")) - { - osg::Vec2i eyeResolution = osg::Vec2i(); - eyeResolution.x() = Settings::Manager::getInt("eye resolution x", "Stereo View"); - eyeResolution.y() = Settings::Manager::getInt("eye resolution y", "Stereo View"); - sm.overrideEyeResolution(eyeResolution); - } - - sm.initializeStereo(graphicsContext, Settings::Manager::getBool("multiview", "Stereo")); + sm.initializeStereo(graphicsContext, mMultiview, mSharedShadowMaps); } } diff --git a/components/stereo/stereomanager.hpp b/components/stereo/stereomanager.hpp index 6f4e971718..8ed88da550 100644 --- a/components/stereo/stereomanager.hpp +++ b/components/stereo/stereomanager.hpp @@ -76,15 +76,23 @@ namespace Stereo //! @Param viewer the osg viewer whose stereo should be managed. //! @Param enableStereo whether or not stereo should be enabled. //! @Param enableMultiview whether or not to make use of the GL_OVR_Multiview extension, if supported. - Manager(osgViewer::Viewer* viewer, bool enableStereo); + //! @Param near defines distance to near camera clipping plane from view point. + //! @Param far defines distance to far camera clipping plane from view point. + explicit Manager(osgViewer::Viewer* viewer, bool enableStereo, double near, double far); ~Manager(); //! Called during update traversal void update(); + void updateSettings(double near, double far) + { + mNear = near; + mFar = far; + } + //! Initializes all details of stereo if applicable. If the constructor was called with enableMultiview=true, //! and the GL_OVR_Multiview extension is supported, Stereo::getMultiview() will return true after this call. - void initializeStereo(osg::GraphicsContext* gc, bool enableMultiview); + void initializeStereo(osg::GraphicsContext* gc, bool enableMultiview, bool sharedShadowMaps); //! Callback that updates stereo configuration during the update pass void setUpdateViewCallback(std::shared_ptr cb); @@ -138,6 +146,8 @@ namespace Stereo std::shared_ptr mMultiviewFramebuffer; bool mEyeResolutionOverriden; osg::Vec2i mEyeResolutionOverride; + double mNear; + double mFar; std::array mView; std::array mViewOffsetMatrix; @@ -153,13 +163,34 @@ namespace Stereo osg::ref_ptr mIdentifierRight = new Identifier(); }; + struct CustomView + { + Stereo::View mLeft; + Stereo::View mRight; + }; + + struct Settings + { + bool mMultiview; + bool mAllowDisplayListsForMultiview; + bool mSharedShadowMaps; + std::optional mCustomView; + std::optional mEyeResolution; + }; + //! Performs stereo-specific initialization operations. class InitializeStereoOperation final : public osg::GraphicsOperation { public: - InitializeStereoOperation(); + explicit InitializeStereoOperation(const Settings& settings); void operator()(osg::GraphicsContext* graphicsContext) override; + + private: + bool mMultiview; + bool mSharedShadowMaps; + std::optional mCustomView; + std::optional mEyeResolution; }; } diff --git a/components/terrain/cellborder.cpp b/components/terrain/cellborder.cpp index 06767531d3..b78691cd8d 100644 --- a/components/terrain/cellborder.cpp +++ b/components/terrain/cellborder.cpp @@ -6,6 +6,7 @@ #include "world.hpp" +#include #include #include #include @@ -25,7 +26,7 @@ namespace Terrain osg::ref_ptr CellBorder::createBorderGeometry(float x, float y, float size, Terrain::Storage* terrain, Resource::SceneManager* sceneManager, int mask, ESM::RefId worldspace, float offset, osg::Vec4f color) { - const int cellSize = ESM::Land::REAL_SIZE; + const int cellSize = ESM::getCellSize(worldspace); const int borderSegments = 40; osg::Vec3 cellCorner = osg::Vec3(x * cellSize, y * cellSize, 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/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp index 52edd96b9f..bdf9f56790 100644 --- a/components/terrain/quadtreeworld.cpp +++ b/components/terrain/quadtreeworld.cpp @@ -280,8 +280,9 @@ namespace Terrain QuadTreeWorld::QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask, int compMapResolution, float compMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize, - bool debugChunks, ESM::RefId worldspace) - : TerrainGrid(parent, compileRoot, resourceSystem, storage, nodeMask, worldspace, preCompileMask, borderMask) + bool debugChunks, ESM::RefId worldspace, double expiryDelay) + : TerrainGrid( + parent, compileRoot, resourceSystem, storage, nodeMask, worldspace, expiryDelay, preCompileMask, borderMask) , mViewDataMap(new ViewDataMap) , mQuadTreeBuilt(false) , mLodFactor(lodFactor) diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp index 2524dc046f..fa800d2655 100644 --- a/components/terrain/quadtreeworld.hpp +++ b/components/terrain/quadtreeworld.hpp @@ -33,7 +33,7 @@ namespace Terrain QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask, int compMapResolution, float comMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize, - bool debugChunks, ESM::RefId worldspace); + bool debugChunks, ESM::RefId worldspace, double expiryDelay); ~QuadTreeWorld(); diff --git a/components/version/version.cpp.in b/components/version/version.cpp.in index d95e47cbd6..312520acbb 100644 --- a/components/version/version.cpp.in +++ b/components/version/version.cpp.in @@ -25,6 +25,11 @@ namespace Version return @OPENMW_LUA_API_REVISION@; } + int getPostprocessingApiRevision() + { + return @OPENMW_POSTPROCESSING_API_REVISION@; + } + std::string getOpenmwVersionDescription() { std::string str = "OpenMW version "; diff --git a/components/version/version.hpp b/components/version/version.hpp index 64b8cd05e0..c05cf8a594 100644 --- a/components/version/version.hpp +++ b/components/version/version.hpp @@ -11,6 +11,7 @@ namespace Version std::string_view getCommitHash(); std::string_view getTagHash(); int getLuaApiRevision(); + int getPostprocessingApiRevision(); // Prepares string that contains version and commit hash. std::string getOpenmwVersionDescription(); 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/components/widgets/box.cpp b/components/widgets/box.cpp index 89f92b7bf1..d711925656 100644 --- a/components/widgets/box.cpp +++ b/components/widgets/box.cpp @@ -48,7 +48,7 @@ namespace Gui } else { - TextBox::setPropertyOverride(_key, _value); + Gui::TextBox::setPropertyOverride(_key, _value); } } @@ -115,7 +115,7 @@ namespace Gui } else { - EditBox::setPropertyOverride(_key, _value); + Gui::EditBox::setPropertyOverride(_key, _value); } } @@ -144,7 +144,7 @@ namespace Gui } else { - Button::setPropertyOverride(_key, _value); + Gui::Button::setPropertyOverride(_key, _value); } } diff --git a/components/widgets/box.hpp b/components/widgets/box.hpp index b7543f1f05..83cbddb3b0 100644 --- a/components/widgets/box.hpp +++ b/components/widgets/box.hpp @@ -7,8 +7,25 @@ #include #include +#include "fontwrapper.hpp" + namespace Gui { + class Button : public FontWrapper + { + MYGUI_RTTI_DERIVED(Button) + }; + + class TextBox : public FontWrapper + { + MYGUI_RTTI_DERIVED(TextBox) + }; + + class EditBox : public FontWrapper + { + MYGUI_RTTI_DERIVED(EditBox) + }; + class AutoSizedWidget { public: @@ -27,7 +44,7 @@ namespace Gui MyGUI::Align mExpandDirection; }; - class AutoSizedTextBox : public AutoSizedWidget, public MyGUI::TextBox + class AutoSizedTextBox : public AutoSizedWidget, public TextBox { MYGUI_RTTI_DERIVED(AutoSizedTextBox) @@ -40,7 +57,7 @@ namespace Gui std::string mFontSize; }; - class AutoSizedEditBox : public AutoSizedWidget, public MyGUI::EditBox + class AutoSizedEditBox : public AutoSizedWidget, public EditBox { MYGUI_RTTI_DERIVED(AutoSizedEditBox) @@ -59,7 +76,7 @@ namespace Gui int mMaxWidth = 0; }; - class AutoSizedButton : public AutoSizedWidget, public MyGUI::Button + class AutoSizedButton : public AutoSizedWidget, public Button { MYGUI_RTTI_DERIVED(AutoSizedButton) diff --git a/components/widgets/fontwrapper.hpp b/components/widgets/fontwrapper.hpp new file mode 100644 index 0000000000..813d323f99 --- /dev/null +++ b/components/widgets/fontwrapper.hpp @@ -0,0 +1,25 @@ +#ifndef OPENMW_WIDGETS_WRAPPER_H +#define OPENMW_WIDGETS_WRAPPER_H + +#include + +#include "components/settings/values.hpp" + +#include + +namespace Gui +{ + /// Wrapper to tell UI element to use font size from settings.cfg + template + class FontWrapper : public T + { + public: + void setFontName(std::string_view name) override + { + T::setFontName(name); + T::setPropertyOverride("FontHeight", std::to_string(Settings::gui().mFontSize)); + } + }; +} + +#endif diff --git a/components/widgets/numericeditbox.hpp b/components/widgets/numericeditbox.hpp index 39605b67d9..ee8ef39a59 100644 --- a/components/widgets/numericeditbox.hpp +++ b/components/widgets/numericeditbox.hpp @@ -3,13 +3,15 @@ #include +#include "fontwrapper.hpp" + namespace Gui { /** * @brief A variant of the EditBox that only allows integer inputs */ - class NumericEditBox final : public MyGUI::EditBox + class NumericEditBox final : public FontWrapper { MYGUI_RTTI_DERIVED(NumericEditBox) diff --git a/components/widgets/sharedstatebutton.hpp b/components/widgets/sharedstatebutton.hpp index 99f597360c..688d949f6e 100644 --- a/components/widgets/sharedstatebutton.hpp +++ b/components/widgets/sharedstatebutton.hpp @@ -3,6 +3,8 @@ #include +#include "fontwrapper.hpp" + namespace Gui { @@ -12,7 +14,7 @@ namespace Gui /// @brief A button that applies its own state changes to other widgets, to do this you define it as part of a /// ButtonGroup. - class SharedStateButton final : public MyGUI::Button + class SharedStateButton final : public FontWrapper { MYGUI_RTTI_DERIVED(SharedStateButton) diff --git a/components/widgets/widgets.cpp b/components/widgets/widgets.cpp index 097f84c62f..d27d7e5fc9 100644 --- a/components/widgets/widgets.cpp +++ b/components/widgets/widgets.cpp @@ -18,9 +18,12 @@ namespace Gui MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); diff --git a/docs/source/conf.py b/docs/source/conf.py index 096dec6ae0..902e84c393 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -67,6 +67,7 @@ copyright = u'2023, OpenMW Team' release = version = "UNRELEASED" luaApiRevision = "UNKNOWN" +ppApiRevision = "UNDEFINED" try: cmake_raw = open(project_root+'/CMakeLists.txt', 'r').read() @@ -80,6 +81,9 @@ try: luaApiRevisionMatch = re.search('set\(OPENMW_LUA_API_REVISION (\d+)\)', cmake_raw) if luaApiRevisionMatch: luaApiRevision = luaApiRevisionMatch.group(1) + ppApiRevisionMatch = re.search('set\(OPENMW_POSTPROCESSING_API_REVISION (\d+)\)', cmake_raw) + if ppApiRevisionMatch: + ppApiRevision = ppApiRevisionMatch.group(1) except Exception as ex: print("WARNING: Version will be set to '{0}' because: '{1}'.".format(release, str(ex))) @@ -87,6 +91,7 @@ except Exception as ex: rst_prolog = f""" .. |luaApiRevision| replace:: {luaApiRevision} +.. |ppApiRevision| replace:: {ppApiRevision} """ # The language for content autogenerated by Sphinx. Refer to documentation 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/game.rst b/docs/source/reference/modding/settings/game.rst index c88ae4e28f..31cc2703f2 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -517,3 +517,15 @@ will not be useful with another. * 0: Axis-aligned bounding box * 1: Rotating box * 2: Cylinder + +player movement ignores animation +--------------------------------- + +:Type: boolean +:Range: True/False +:Default: False + +In third person, the camera will sway along with the movement animations of the player. +Enabling this option disables this swaying by having the player character move independently of its animation. + +This setting can be controlled in the Settings tab of the launcher, under Visuals. 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/docs/source/reference/modding/settings/navigator.rst b/docs/source/reference/modding/settings/navigator.rst index 0de061d8f0..55b9e19b19 100644 --- a/docs/source/reference/modding/settings/navigator.rst +++ b/docs/source/reference/modding/settings/navigator.rst @@ -13,16 +13,15 @@ enable :Range: True/False :Default: True -Enable navigator. -When enabled background threads are started to build nav mesh for world geometry. -Pathfinding system uses nav mesh to build paths. -When disabled only pathgrid is used to build paths. -Single-core CPU systems may have big performance impact on exiting interior location and moving across exterior world. +Enable navigator to make all settings in this category take effect. +When enabled, a navigation mesh (navmesh) is built in the background for world geometry to be used for pathfinding. +When disabled only the path grid is used to build paths. +Single-core CPU systems may have a big performance impact on existing interior location and moving across the exterior world. May slightly affect performance on multi-core CPU systems. -Multi-core CPU systems may have different latency for nav mesh update depending on other settings and system performance. -Moving across external world, entering/exiting location produce nav mesh update. -NPC and creatures may not be able to find path before nav mesh is built around them. -Try to disable this if you want to have old fashioned AI which doesn't know where to go when you stand behind that stone and casting a firebolt. +Multi-core CPU systems may have different latency for navigation mesh update depending on other settings and system performance. +Moving across external world, entering/exiting location produce navigation mesh update. +NPC and creatures may not be able to find path before navigation mesh is built around them. +Try to disable this if you want to have old fashioned AI which doesn't know where to go when you stand behind that stone and cast a firebolt. max tiles number ---------------- @@ -31,14 +30,14 @@ max tiles number :Range: >= 0 :Default: 512 -Number of tiles at nav mesh. +Number of tiles at navigation mesh. Nav mesh covers circle area around player. -This option allows to set an explicit limit for nav mesh size, how many tiles should fit into circle. -If actor is inside this area it able to find path over nav mesh. +This option allows to set an explicit limit for navigation mesh size, how many tiles should fit into circle. +If actor is inside this area it able to find path over navigation mesh. Increasing this value may decrease performance. .. note:: - Don't expect infinite nav mesh size increasing. + Don't expect infinite navigation mesh size increasing. This condition is always true: ``max tiles number * max polygons per tile <= 4194304``. It's a limitation of `Recastnavigation `_ library. @@ -49,9 +48,9 @@ wait until min distance to player :Range: >= 0 :Default: 5 -Distance in navmesh tiles around the player to keep loading screen until navigation mesh is generated. +Distance in navigation mesh tiles around the player to keep loading screen until navigation mesh is generated. Allows to complete cell loading only when minimal navigation mesh area is generated to correctly find path for actors -nearby the player. Increasing this value will keep loading screen longer but will slightly increase nav mesh generation +nearby the player. Increasing this value will keep loading screen longer but will slightly increase navigation mesh generation speed on systems bound by CPU. Zero means no waiting. enable nav mesh disk cache @@ -61,8 +60,8 @@ enable nav mesh disk cache :Range: True/False :Default: True -If true navmesh cache stored on disk will be used in addition to memory cache. -If navmesh tile is not present in memory cache, it will be looked up in the disk cache. +If true navigation mesh cache stored on disk will be used in addition to memory cache. +If navigation mesh tile is not present in memory cache, it will be looked up in the disk cache. If it's not found there it will be generated. write to navmeshdb @@ -72,7 +71,7 @@ write to navmeshdb :Range: True/False :Default: True -If true generated navmesh tiles will be stored into disk cache while game is running. +If true generated navigation mesh tiles will be stored into disk cache while game is running. max navmeshdb file size ----------------------- @@ -95,8 +94,8 @@ async nav mesh updater threads :Range: >= 1 :Default: 1 -Number of background threads to update nav mesh. -Increasing this value may decrease performance, but also may decrease or increase nav mesh update latency depending on number of CPU cores. +Number of background threads to update navigation mesh. +Increasing this value may decrease performance, but also may decrease or increase navigation mesh update latency depending on number of CPU cores. On systems with not less than 4 CPU cores latency dependens approximately like 1/log(n) from number of threads. Don't expect twice better latency by doubling this value. @@ -107,12 +106,12 @@ max nav mesh tiles cache size :Range: >= 0 :Default: 268435456 -Maximum total cached size of all nav mesh tiles in bytes. -Setting greater than zero value will reduce nav mesh update latency for previously visited locations. +Maximum total cached size of all navigation mesh tiles in bytes. +Setting greater than zero value will reduce navigation mesh update latency for previously visited locations. Increasing this value may increase total memory consumption, but potentially will reduce latency for recently visited locations. Limit this value by total available physical memory minus base game memory consumption and other applications. Game will not eat all memory at once. -Memory will be consumed in approximately linear dependency from number of nav mesh updates. +Memory will be consumed in approximately linear dependency from number of navigation mesh updates. But only for new locations or already dropped from cache. min update interval ms @@ -122,17 +121,17 @@ min update interval ms :Range: >= 0 :Default: 250 -Minimum time duration required to pass before next navmesh update for the same tile in milliseconds. +Minimum time duration required to pass before next navigation mesh update for the same tile in milliseconds. Only tiles affected where objects are transformed. Next update for tile with added or removed object will not be delayed. -Visible ingame effect is navmesh update around opening or closing door. +Visible ingame effect is navigation mesh update around opening or closing door. Primary usage is for rotating signs like in Seyda Neen at Arrille's Tradehouse entrance. Decreasing this value may increase CPU usage by background threads. Developer's settings ******************** -This section is for developers or anyone who wants to investigate how nav mesh system works in OpenMW. +This section is for developers or anyone who wants to learn how navigation mesh system works in OpenMW. enable write recast mesh to file -------------------------------- @@ -141,8 +140,8 @@ enable write recast mesh to file :Range: True/False :Default: False -Write recast mesh to file in .obj format for each use to update nav mesh. -Option is used to find out what world geometry is used to build nav mesh. +Write recast mesh to file in .obj format for each use to update navigation mesh. +Option is used to find out what world geometry is used to build navigation mesh. Potentially decreases performance. enable write nav mesh to file @@ -152,7 +151,7 @@ enable write nav mesh to file :Range: True/False :Default: False -Write nav mesh to file to be able to open by RecastDemo application. +Write navigation mesh to file to be able to open by RecastDemo application. Usually it is more useful to have both enable write recast mesh to file and this options enabled. RecastDemo supports .obj files. Potentially decreases performance. @@ -175,9 +174,9 @@ enable nav mesh file name revision :Range: True/False :Default: False -Write each nav mesh file with revision in name. +Write each navigation mesh file with revision in name. Otherwise will rewrite same file. -If it is unclear when nav mesh is changed use this option to dump multiple files for each state. +If it is unclear when navigation mesh is changed use this option to dump multiple files for each state. recast mesh path prefix ----------------------- @@ -195,7 +194,7 @@ nav mesh path prefix :Range: file system path :Default: "" -Write nav mesh file at path with this prefix. +Write navigation mesh file at path with this prefix. enable nav mesh render ---------------------- @@ -206,7 +205,7 @@ enable nav mesh render Render navigation mesh. Allows to do in-game debug. -Every nav mesh is visible and every update is noticeable. +Every navigation mesh is visible and every update is noticeable. Potentially decreases performance. nav mesh render mode @@ -246,17 +245,6 @@ Absent pieces usually mean a bug in recast mesh tiles building. Allows to do in-game debug. Potentially decreases performance. -nav mesh version ----------------- - -:Type: integer -:Range: > 0 -:Default: 1 - -Version of navigation mesh generation algorithm. -Should be increased each time there is a difference between output of makeNavMeshTileData function for the same input. -Changing the value will invalidate navmesh disk cache. - Expert settings *************** @@ -269,12 +257,12 @@ recast scale factor :Range: > 0.0 :Default: 0.029411764705882353 -Scale of nav mesh coordinates to world coordinates. Recastnavigation builds voxels for world geometry. +Scale of navigation mesh coordinates to world coordinates. Recastnavigation builds voxels for world geometry. Basically voxel size is 1 / "cell size". To reduce amount of voxels we apply scale factor, to make voxel size "recast scale factor" / "cell size". Default value calculates by this equation: sStepSizeUp * "recast scale factor" / "cell size" = 5 (max climb height should be equal to 4 voxels). -Changing this value will change generated nav mesh. Some locations may become unavailable for NPC and creatures. -Pay attention to slopes and roofs when change it. Increasing this value will reduce nav mesh update latency. +Changing this value will change generated navigation mesh. Some locations may become unavailable for NPC and creatures. +Pay attention to slopes and roofs when change it. Increasing this value will reduce navigation mesh update latency. max polygon path size --------------------- @@ -397,13 +385,13 @@ max polygons per tile :Range: 2^n, 0 < n < 22 :Default: 4096 -Maximum number of polygons per nav mesh tile. Maximum number of nav mesh tiles depends on +Maximum number of polygons per navigation mesh tile. Maximum number of navigation mesh tiles depends on this value. 22 bits is a limit to store both tile identifier and polygon identifier (tiles = 2^(22 - log2(polygons))). See `recastnavigation `_ for more details. .. Warning:: - Lower value may lead to ignored world geometry on nav mesh. - Greater value will reduce number of nav mesh tiles. + Lower value may lead to ignored world geometry on navigation mesh. + Greater value will reduce number of navigation mesh tiles. This condition is always true: ``max tiles number * max polygons per tile <= 4194304``. It's a limitation of `Recastnavigation `_ library. diff --git a/docs/source/reference/modding/settings/video.rst b/docs/source/reference/modding/settings/video.rst index 4cb5ba1842..801cf63d5b 100644 --- a/docs/source/reference/modding/settings/video.rst +++ b/docs/source/reference/modding/settings/video.rst @@ -194,3 +194,12 @@ Gamma is an exponent that makes colors brighter if greater than 1.0 and darker i This setting can be changed in the Detail tab of the Video panel of the Options menu. It has been reported to not work on some Linux systems, and therefore the in-game setting in the Options menu has been disabled on Linux systems. + +screenshot type +--------------- + +:Type: screenshot settings +:Default: regular + +Type of screenshot to take (regular, cylindrical, spherical, planet or cubemap), optionally followed by +screenshot width, height and cubemap resolution in pixels (e.g. spherical 1600 1000 1200). diff --git a/docs/source/reference/postprocessing/omwfx.rst b/docs/source/reference/postprocessing/omwfx.rst index a4028c5040..7a7cdc198b 100644 --- a/docs/source/reference/postprocessing/omwfx.rst +++ b/docs/source/reference/postprocessing/omwfx.rst @@ -126,45 +126,49 @@ Builtin Uniforms Builtin Macros ############## -+-----------------------+----------------+----------------------------------------------------------------------+ -| Macro | Definition | Description | -+=======================+================+======================================================================+ -|``OMW_REVERSE_Z`` | ``0`` or ``1`` | Whether a reversed depth buffer is in use. | -| | | | -| | | ``0`` Depth sampler will be in range [1, 0] | -| | | | -| | | ``1`` Depth sampler will be in range [0, 1] | -+-----------------------+----------------+----------------------------------------------------------------------+ -|``OMW_RADIAL_FOG`` | ``0`` or ``1`` | Whether radial fog is in use. | -| | | | -| | | ``0`` Fog is linear | -| | | | -| | | ``1`` Fog is radial | -+-----------------------+----------------+----------------------------------------------------------------------+ -|``OMW_EXPONENTIAL_FOG``| ``0`` or ``1`` | Whether exponential fog is in use. | -| | | | -| | | ``0`` Fog is linear | -| | | | -| | | ``1`` Fog is exponential | -+-----------------------+----------------+----------------------------------------------------------------------+ -| ``OMW_HDR`` | ``0`` or ``1`` | Whether average scene luminance is computed every frame. | -| | | | -| | | ``0`` Average scene luminance is not computed | -| | | | -| | | ``1`` Average scene luminance is computed | -+-----------------------+----------------+----------------------------------------------------------------------+ -| ``OMW_NORMALS`` | ``0`` or ``1`` | Whether normals are available as a sampler in the technique. | -| | | | -| | | ``0`` Normals are not available | -| | | | -| | | ``1`` Normals are available. | -+-----------------------+----------------+----------------------------------------------------------------------+ -| ``OMW_MULTIVIEW`` | ``0`` or ``1`` | Whether multiview rendering is in use. | -| | | | -| | | ``0`` Multiview not in use | -| | | | -| | | ``1`` Multiview in use. | -+-----------------------+----------------+----------------------------------------------------------------------+ ++-----------------------+-----------------+----------------------------------------------------------------------+ +| Macro | Definition | Description | ++=======================+=================+======================================================================+ +|``OMW_REVERSE_Z`` | ``0`` or ``1`` | Whether a reversed depth buffer is in use. | +| | | | +| | | ``0`` Depth sampler will be in range [1, 0] | +| | | | +| | | ``1`` Depth sampler will be in range [0, 1] | ++-----------------------+-----------------+----------------------------------------------------------------------+ +|``OMW_RADIAL_FOG`` | ``0`` or ``1`` | Whether radial fog is in use. | +| | | | +| | | ``0`` Fog is linear | +| | | | +| | | ``1`` Fog is radial | ++-----------------------+-----------------+----------------------------------------------------------------------+ +|``OMW_EXPONENTIAL_FOG``| ``0`` or ``1`` | Whether exponential fog is in use. | +| | | | +| | | ``0`` Fog is linear | +| | | | +| | | ``1`` Fog is exponential | ++-----------------------+-----------------+----------------------------------------------------------------------+ +| ``OMW_HDR`` | ``0`` or ``1`` | Whether average scene luminance is computed every frame. | +| | | | +| | | ``0`` Average scene luminance is not computed | +| | | | +| | | ``1`` Average scene luminance is computed | ++-----------------------+-----------------+----------------------------------------------------------------------+ +| ``OMW_NORMALS`` | ``0`` or ``1`` | Whether normals are available as a sampler in the technique. | +| | | | +| | | ``0`` Normals are not available | +| | | | +| | | ``1`` Normals are available. | ++-----------------------+-----------------+----------------------------------------------------------------------+ +| ``OMW_MULTIVIEW`` | ``0`` or ``1`` | Whether multiview rendering is in use. | +| | | | +| | | ``0`` Multiview not in use | +| | | | +| | | ``1`` Multiview in use. | ++-----------------------+-----------------+----------------------------------------------------------------------+ +| ``OMW_API_VERSION`` | |ppApiRevision| | The revision of OpenMW postprocessing API. | +| | | It is an integer that is incremented every time the API is changed. | +| | | This was added in 0.49, so it will be undefined in 0.48. | ++-----------------------+-----------------+----------------------------------------------------------------------+ Builtin Functions @@ -189,19 +193,30 @@ The following functions can be accessed in any fragment or vertex shader. +--------------------------------------------------+-------------------------------------------------------------------------------+ | ``vec4 omw_GetLastPass(vec2 uv)`` | Returns RGBA color output of the last pass | +--------------------------------------------------+-------------------------------------------------------------------------------+ -| ``vec3 omw_GetNormals(vec2 uv)`` | Returns normalized worldspace normals [-1, 1] | -| | | -| | The values in sampler are in [0, 1] but are transformed to [-1, 1] | -+--------------------------------------------------+-----------------------+-------------------------------------------------------+ +| ``vec3 omw_GetNormals(vec2 uv)`` | Returns normalized view-space normals [-1, 1] | ++--------------------------------------------------+-------------------------------------------------------------------------------+ +| ``vec3 omw_GetNormalsWorldSpace(vec2 uv)`` | Returns normalized world-space normals [-1, 1] | ++--------------------------------------------------+-------------------------------------------------------------------------------+ | ``vec3 omw_GetWorldPosFromUV(vec2 uv)`` | Returns world position for given uv coordinate. | -+--------------------------------------------------+-----------------------+-------------------------------------------------------+ ++--------------------------------------------------+-------------------------------------------------------------------------------+ | ``float omw_GetLinearDepth(vec2 uv)`` | Returns the depth in game units for given uv coordinate. | -+--------------------------------------------------+-----------------------+-------------------------------------------------------+ ++--------------------------------------------------+-------------------------------------------------------------------------------+ | ``float omw_EstimateFogCoverageFromUV(vec2 uv)`` | Returns a fog coverage in the range from 0.0 (no fog) and 1.0 (full fog) | | | | | | Calculates an estimated fog coverage for given uv coordinate. | -+--------------------------------------------------+-----------------------+-------------------------------------------------------+ - ++--------------------------------------------------+-------------------------------------------------------------------------------+ +| ``int omw_GetPointLightCount()`` | Returns the number of point lights available to sample from in the scene. | ++--------------------------------------------------+-------------------------------------------------------------------------------+ +| ``vec3 omw_GetPointLightWorldPos(int index)`` | Returns the world space position of a point light. | ++--------------------------------------------------+-------------------------------------------------------------------------------+ +| ``vec3 omw_GetPointLightDiffuse(int index)`` | Returns the diffuse color of the point light. | ++--------------------------------------------------+-------------------------------------------------------------------------------+ +| ``int omw_GetPointLightAttenuation(int index)`` | Returns the attenuation values of the point light. | +| | | +| | The XYZ channels hold the constant, linear, and quadratic components. | ++--------------------------------------------------+-------------------------------------------------------------------------------+ +| ``float omw_GetPointLightRadius(int index)`` | Returns the radius of the point light, in game units. | ++--------------------------------------------------+-------------------------------------------------------------------------------+ Special Attributes ################## @@ -318,7 +333,7 @@ Exactly one ``technique`` block is required for every shader file. In this we de +------------------+--------------------+---------------------------------------------------+ | author | string | Shader authors that shows in HUD | +------------------+--------------------+---------------------------------------------------+ -| glsl_Version | integer | GLSL version | +| glsl_version | integer | GLSL version | +------------------+--------------------+---------------------------------------------------+ | glsl_profile | string | GLSL profile, like ``compatibility`` | +------------------+--------------------+---------------------------------------------------+ @@ -528,49 +543,138 @@ is not wanted and you want a custom render target. +------------------+---------------------+-----------------------------------------------------------------------------+ | mipmaps | boolean | Whether mipmaps should be generated every frame | +------------------+---------------------+-----------------------------------------------------------------------------+ +| clear_color | vec4 | The color the texture will be cleared to when it's first created | ++------------------+---------------------+-----------------------------------------------------------------------------+ -To use the render target a pass must be assigned to it, along with any optional clear or blend modes. +To use the render target a pass must be assigned to it, along with any optional blend modes. +As a restriction, only three render targets can be bound per pass with ``rt1``, ``rt2``, ``rt3``, respectively. -In the code snippet below a rendertarget is used to draw the red channel of a scene at half resolution, then a quarter. As a restriction, -only three render targets can be bound per pass with ``rt1``, ``rt2``, ``rt3``, respectively. +Blending modes can be useful at times. Below is a simple shader which, when activated, will slowly turn the screen pure red. +Notice how we only ever write the value `.01` to the `RT_Red` buffer. Since we're using appropriate blending modes the +color buffer will accumulate. .. code-block:: none - render_target RT_Downsample { - width_ratio = 0.5; - height_ratio = 0.5; - internal_format = r16f; + render_target RT_Red { + width = 4; + height = 4; + source_format = rgb; + internal_format = rgb16f; source_type = float; - source_format = red; + clear_color = vec4(1,0,0,1); } - render_target RT_Downsample4 { - width_ratio = 0.25; - height_ratio = 0.25; - } - - fragment downsample2x(target=RT_Downsample) { - + fragment red(target=RT_Red,blend=(add, src_color, one), rt1=RT_Red) { omw_In vec2 omw_TexCoord; void main() { - omw_FragColor.r = omw_GetLastShader(omw_TexCoord).r; + omw_FragColor.rgb = vec3(0.01,0,0); } } - fragment downsample4x(target=RT_Downsample4, rt1=RT_Downsample) { - + fragment view(rt1=RT_Red) { omw_In vec2 omw_TexCoord; void main() { - omw_FragColor = omw_Texture2D(RT_Downsample, omw_TexCoord); + omw_FragColor = omw_Texture2D(RT_Red, omw_TexCoord); } } -Now, when the `downsample2x` pass runs it will write to the target buffer instead of the default -one assigned by the engine. + technique { + author = "OpenMW"; + passes = red, view; + } + + +These custom render targets are persistent and ownership is given to the shader which defines them. +This gives potential to implement temporal effects by storing previous frame data in these buffers. +Below is an example which calculates a naive average scene luminance and transitions between values smoothly. + +.. code-block:: none + + render_target RT_Lum { + width = 256; + height = 256; + mipmaps = true; + source_format = rgb; + internal_format = rgb16f; + source_type = float; + min_filter = linear_mipmap_linear; + mag_filter = linear; + } + + render_target RT_LumAvg { + source_type = float; + source_format = rgb; + internal_format = rgb16f; + min_filter = nearest; + mag_filter = nearest; + } + + render_target RT_LumAvgLastFrame { + source_type = float; + source_format = rgb; + internal_format = rgb16f; + min_filter = nearest; + mag_filter = nearest; + } + + fragment calculateLum(target=RT_Lum) { + omw_In vec2 omw_TexCoord; + + void main() + { + vec3 orgi = pow(omw_GetLastShader(omw_TexCoord), vec4(2.2)).rgb; + omw_FragColor.rgb = orgi; + } + } + + fragment fetchLumAvg(target=RT_LumAvg, rt1=RT_Lum, rt2=RT_LumAvgLastFrame) { + omw_In vec2 omw_TexCoord; + + void main() + { + vec3 avgLumaCurrFrame = textureLod(RT_Lum, vec2(0.5, 0.5), 6).rgb; + vec3 avgLumaLastFrame = omw_Texture2D(RT_LumAvgLastFrame, vec2(0.5, 0.5)).rgb; + + const float speed = 0.9; + + vec3 avgLuma = avgLumaLastFrame + (avgLumaCurrFrame - avgLumaLastFrame) * (1.0 - exp(-omw.deltaSimulationTime * speed)); + + omw_FragColor.rgb = avgLuma; + } + } + + fragment adaptation(rt1=RT_LumAvg) { + omw_In vec2 omw_TexCoord; + + void main() + { + vec3 avgLuma = omw_Texture2D(RT_LumAvg, vec2(0.5, 0.5)).rgb; + + if (omw_TexCoord.y < 0.2) + omw_FragColor = vec4(avgLuma, 1.0); + else + omw_FragColor = omw_GetLastShader(omw_TexCoord); + } + } + + fragment store(target=RT_LumAvgLastFrame, rt1=RT_LumAvg) { + void main() + { + vec3 avgLuma = omw_Texture2D(RT_LumAvg, vec2(0.5, 0.5)).rgb; + omw_FragColor.rgb = avgLuma; + } + } + + technique { + author = "OpenMW"; + passes = calculateLum, fetchLumAvg, store, adaptation; + glsl_version = 330; + } + Simple Example ############## @@ -625,7 +729,6 @@ together passes, setting up metadata, and setting up various flags. passes = desaturate; version = "1.0"; author = "Fargoth"; - passes = desaturate; } 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/extern/oics/CMakeLists.txt b/extern/oics/CMakeLists.txt index 1b9fea9217..4bd3bc51ad 100644 --- a/extern/oics/CMakeLists.txt +++ b/extern/oics/CMakeLists.txt @@ -20,6 +20,8 @@ else() target_link_libraries(oics local_tinyxml) endif() +target_link_libraries(oics SDL2::SDL2) + if (MSVC) target_precompile_headers(oics PUBLIC ) endif() diff --git a/extern/osg-ffmpeg-videoplayer/CMakeLists.txt b/extern/osg-ffmpeg-videoplayer/CMakeLists.txt index 7e2712f19b..10c8d356a0 100644 --- a/extern/osg-ffmpeg-videoplayer/CMakeLists.txt +++ b/extern/osg-ffmpeg-videoplayer/CMakeLists.txt @@ -14,6 +14,7 @@ include_directories(${FFmpeg_INCLUDE_DIRS}) add_library(${OSG_FFMPEG_VIDEOPLAYER_LIBRARY} STATIC ${OSG_FFMPEG_VIDEOPLAYER_SOURCE_FILES}) target_link_libraries(${OSG_FFMPEG_VIDEOPLAYER_LIBRARY} ${FFmpeg_LIBRARIES}) target_link_libraries(${OSG_FFMPEG_VIDEOPLAYER_LIBRARY} ${OSG_LIBRARIES}) +target_link_libraries(${OSG_FFMPEG_VIDEOPLAYER_LIBRARY} SDL2::SDL2) link_directories(${CMAKE_CURRENT_BINARY_DIR}) diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index 14728be732..4b36254183 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -93,10 +93,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 c717a13f02..30f6941059 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/Interface/de.yaml b/files/data/l10n/Interface/de.yaml index 1cabad01a9..ac1a95a0ea 100644 --- a/files/data/l10n/Interface/de.yaml +++ b/files/data/l10n/Interface/de.yaml @@ -25,3 +25,4 @@ Yes: "Ja" #OK: "OK" #Off: "Off" #On: "On" +#Copy: "Copy" diff --git a/files/data/l10n/Interface/en.yaml b/files/data/l10n/Interface/en.yaml index df450b5c38..82c1aeba1a 100644 --- a/files/data/l10n/Interface/en.yaml +++ b/files/data/l10n/Interface/en.yaml @@ -22,3 +22,4 @@ None: "None" OK: "OK" Cancel: "Cancel" Close: "Close" +Copy: "Copy" diff --git a/files/data/l10n/Interface/fr.yaml b/files/data/l10n/Interface/fr.yaml index 5aa0260680..bac4346364 100644 --- a/files/data/l10n/Interface/fr.yaml +++ b/files/data/l10n/Interface/fr.yaml @@ -22,3 +22,4 @@ None: "Aucun" OK: "Valider" Cancel: "Annuler" Close: "Fermer" +#Copy: "Copy" diff --git a/files/data/l10n/Interface/ru.yaml b/files/data/l10n/Interface/ru.yaml index 6d81dd7797..44b38a77b8 100644 --- a/files/data/l10n/Interface/ru.yaml +++ b/files/data/l10n/Interface/ru.yaml @@ -1,5 +1,6 @@ Cancel: "Отмена" Close: "Закрыть" +Copy: "Скопировать" DurationDay: "{days} д " DurationHour: "{hours} ч " DurationMinute: "{minutes} мин " diff --git a/files/data/l10n/Interface/sv.yaml b/files/data/l10n/Interface/sv.yaml index 5e9260cf97..aae63a1941 100644 --- a/files/data/l10n/Interface/sv.yaml +++ b/files/data/l10n/Interface/sv.yaml @@ -14,3 +14,4 @@ Off: "Av" On: "På" Reset: "Återställ" Yes: "Ja" +#Copy: "Copy" 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 f2b4ee7e5a..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..." @@ -41,10 +44,22 @@ TimePlayed: "Spielzeit" #DeleteGameConfirmation: "Are you sure you want to delete this saved game?" #EmptySaveNameError: "Game can not be saved without a name!" #LoadGameConfirmation: "Do you want to load a saved game and lose the current one?" -#MissingContentFilesConfirmation: | +#MissingContentFilesConfirmation: |- # The currently selected content files do not match the ones used by this save game. # Errors may occur during load or game play. # Do you wish to continue? +#MissingContentFilesList: |- +# {files, plural, +# one{\n\nFound missing file: } +# few{\n\nFound {files} missing files:\n} +# other{\n\nFound {files} missing files:\n} +# } +#MissingContentFilesListCopy: |- +# {files, plural, +# one{\n\nPress Copy to place its name to the clipboard.} +# few{\n\nPress Copy to place their names to the clipboard.} +# other{\n\nPress Copy to place their names to the clipboard.} +# } #OverwriteGameConfirmation: "Are you sure you want to overwrite this saved game?" diff --git a/files/data/l10n/OMWEngine/en.yaml b/files/data/l10n/OMWEngine/en.yaml index 08df886f18..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..." @@ -34,10 +37,22 @@ DeleteGame: "Delete Game" DeleteGameConfirmation: "Are you sure you want to delete this saved game?" EmptySaveNameError: "Game can not be saved without a name!" LoadGameConfirmation: "Do you want to load a saved game and lose the current one?" -MissingContentFilesConfirmation: | +MissingContentFilesConfirmation: |- The currently selected content files do not match the ones used by this save game. Errors may occur during load or game play. Do you wish to continue? +MissingContentFilesList: |- + {files, plural, + one{\n\nFound missing file: } + few{\n\nFound {files} missing files:\n} + other{\n\nFound {files} missing files:\n} + } +MissingContentFilesListCopy: |- + {files, plural, + one{\n\nPress Copy to place its name to the clipboard.} + few{\n\nPress Copy to place their names to the clipboard.} + other{\n\nPress Copy to place their names to the clipboard.} + } OverwriteGameConfirmation: "Are you sure you want to overwrite this saved game?" SelectCharacter: "Select Character..." TimePlayed: "Time played" diff --git a/files/data/l10n/OMWEngine/fr.yaml b/files/data/l10n/OMWEngine/fr.yaml index 5a6209b44c..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..." @@ -37,10 +40,22 @@ DeleteGame: "Supprimer la partie" DeleteGameConfirmation: "Voulez-vous réellement supprimer cette partie sauvegardée ?" EmptySaveNameError: "Impossible de sauvegarder une partie lui donner un nom !" LoadGameConfirmation: "Voulez-vous charger cette autre partie ? Toute progression non sauvegardée sera perdue." -MissingContentFilesConfirmation: | +MissingContentFilesConfirmation: |- Les données de jeu actuellement sélectionnées ne correspondent pas à celle indiquée dans cette sauvegarde. Cela peut entraîner des erreurs lors du chargement, mais aussi lors de votre partie. Voulez-vous continuer ? +#MissingContentFilesList: |- +# {files, plural, +# one{\n\nFound missing file: } +# few{\n\nFound {files} missing files:\n} +# other{\n\nFound {files} missing files:\n} +# } +#MissingContentFilesListCopy: |- +# {files, plural, +# one{\n\nPress Copy to place its name to the clipboard.} +# few{\n\nPress Copy to place their names to the clipboard.} +# other{\n\nPress Copy to place their names to the clipboard.} +# } OverwriteGameConfirmation: "Écraser la sauvegarde précédente ?" diff --git a/files/data/l10n/OMWEngine/ru.yaml b/files/data/l10n/OMWEngine/ru.yaml index cbc71f91e4..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: "Сохранение..." @@ -34,10 +37,22 @@ DeleteGame: "Удалить игру" DeleteGameConfirmation: "Вы уверены, что хотите удалить это сохранение?" EmptySaveNameError: "Имя сохранения не может быть пустым!" LoadGameConfirmation: "Вы хотите загрузить сохранение? Текущая игра будет потеряна." -MissingContentFilesConfirmation: | +MissingContentFilesConfirmation: |- Выбранные ESM/ESP файлы не соответствуют тем, которые использовались для этого сохранения. Во время загрузки или в процессе игры могут возникнуть ошибки. Вы хотите продолжить? +MissingContentFilesList: |- + {files, plural, + one{\n\nОтсутствует файл } + few{\n\nОтсутствуют {files} файла:\n} + other{\n\nОтсутствуют {files} файлов:\n} + } +MissingContentFilesListCopy: |- + {files, plural, + one{\n\nНажмите Скопировать, чтобы поместить его название в буфер обмена.} + few{\n\nНажмите Скопировать, чтобы поместить их названия в буфер обмена.} + other{\n\nНажмите Скопировать, чтобы поместить их названия в буфер обмена.} + } OverwriteGameConfirmation: "Вы уверены, что хотите перезаписать это сохранение?" SelectCharacter: "Выберите персонажа..." TimePlayed: "Время в игре" diff --git a/files/data/l10n/OMWEngine/sv.yaml b/files/data/l10n/OMWEngine/sv.yaml index 1ee8bdc707..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..." @@ -35,10 +38,22 @@ DeleteGame: "Radera spel" DeleteGameConfirmation: "Är du säker på att du vill radera sparfilen?" EmptySaveNameError: "Spelet kan inte sparas utan ett namn!" LoadGameConfirmation: "Vill du ladda ett sparat spel och förlora det pågående spelet?" -MissingContentFilesConfirmation: | +MissingContentFilesConfirmation: |- De valda innehållsfilerna matchar inte filerna som används av denna sparfil. Fel kan uppstå vid laddning eller under spel. Vill du fortsätta? +#MissingContentFilesList: |- +# {files, plural, +# one{\n\nFound missing file: } +# few{\n\nFound {files} missing files:\n} +# other{\n\nFound {files} missing files:\n} +# } +#MissingContentFilesListCopy: |- +# {files, plural, +# one{\n\nPress Copy to place its name to the clipboard.} +# few{\n\nPress Copy to place their names to the clipboard.} +# other{\n\nPress Copy to place their names to the clipboard.} +# } OverwriteGameConfirmation: "Är du säker på att du vill skriva över det här sparade spelet?" SelectCharacter: "Välj spelfigur..." @@ -140,11 +155,11 @@ TextureFilteringBilinear: "Bilinjär" TextureFilteringDisabled: "Ingen" TextureFilteringOther: "Annan" TextureFilteringTrilinear: "Trilinjär" -ToggleHUD: "Växla till HUD" -TogglePostProcessorHUD: "Växla till Postprocess-HUD" +ToggleHUD: "Visa/dölj HUD" +TogglePostProcessorHUD: "Visa/dölj Postprocess-HUD" TransparencyFull: "Full" TransparencyNone: "Ingen" -Video: "Video" +Video: "Bild" VSync: "VSynk" ViewDistance: "Siktavstånd" Water: "Vatten" diff --git a/files/data/l10n/OMWShaders/en.yaml b/files/data/l10n/OMWShaders/en.yaml index 8221be933f..6588591f00 100644 --- a/files/data/l10n/OMWShaders/en.yaml +++ b/files/data/l10n/OMWShaders/en.yaml @@ -34,6 +34,7 @@ DisplayDepthFactorName: "Depth colour factor" DisplayDepthFactorDescription: "Determines correlation between pixel depth value and its output colour. High values lead to brighter image." DisplayDepthName: "Visualize depth buffer" DisplayNormalsName: "Visualize pass normals" +NormalsInWorldSpace: "Show normals in world space" ContrastLevelDescription: "Constrast level." ContrastLevelName: "Constrast" GammaLevelDescription: "Gamma level." diff --git a/files/data/l10n/OMWShaders/ru.yaml b/files/data/l10n/OMWShaders/ru.yaml index b886f72b54..7a2bcfe80d 100644 --- a/files/data/l10n/OMWShaders/ru.yaml +++ b/files/data/l10n/OMWShaders/ru.yaml @@ -33,6 +33,7 @@ DisplayDepthName: "Визуализация буфера глубины" DisplayDepthFactorDescription: "Определяет соотношение между значением глубины пикселя и его цветом. Чем выше значение, тем ярче будет изображение." DisplayDepthFactorName: "Соотношение цвета" DisplayNormalsName: "Визуализация нормалей" +NormalsInWorldSpace: "Показывать нормали мирового пространства" ContrastLevelDescription: "Контрастность изображения" ContrastLevelName: "Контрастность" GammaLevelDescription: "Яркость изображения" diff --git a/files/data/l10n/OMWShaders/sv.yaml b/files/data/l10n/OMWShaders/sv.yaml index c1f1a5a333..f7876b9ee3 100644 --- a/files/data/l10n/OMWShaders/sv.yaml +++ b/files/data/l10n/OMWShaders/sv.yaml @@ -34,6 +34,7 @@ DisplayDepthFactorName: "Färgfaktor" DisplayDepthFactorDescription: "Avgör korrelation mellan djupvärdet på pixeln och dess producerade färg. Högre värden ger ljusare bild." DisplayDepthName: "Visualisera djupbufferten." DisplayNormalsName: "Visualisera normalvektorer" # på engelska står det "pass normals", finns bättre översättning? +NormalsInWorldSpace: "Visa normalvektorer enligt världens koordinatsystem" ContrastLevelDescription: "Kontrastnivå" ContrastLevelName: "Kontrast" GammaLevelDescription: "Gammanivå" 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 0bd7cf6bea..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,70 +112,167 @@ 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 - end - end + self.controls.movement = movement + self.controls.sideMovement = sideMovement + self.controls.run = run - if core.isWorldPaused() or I.UI.getMode() then - return + if not settings:get('toggleSneak') then + self.controls.sneak = input.getBooleanActionValue('Sneak') 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) +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)) + +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)) + +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 + } +end + +local function onLoad(data) + if not data then return end + self.controls.sneak = data.sneaking or false end return { engineHandlers = { onFrame = onFrame, - onInputAction = onInputAction, + onSave = onSave, + onLoad = onLoad, }, interfaceName = 'Controls', --- @@ -231,4 +301,3 @@ return { overrideUiControls = function(v) uiControlsOverridden = v end, } } - diff --git a/files/data/scripts/omw/settings/renderers.lua b/files/data/scripts/omw/settings/renderers.lua index 82005916de..fd9ab0b743 100644 --- a/files/data/scripts/omw/settings/renderers.lua +++ b/files/data/scripts/omw/settings/renderers.lua @@ -151,17 +151,26 @@ return function(registerRenderer) if not argument.l10n then error('"select" renderer requires a "l10n" argument') end + if not pcall(function() + local _ = ipairs(argument.items) + assert(#argument.items > 0) + end) + then + error('"select" renderer requires an "items" array as an argument') + end local l10n = core.l10n(argument.l10n) local index = nil - local itemCount = 0 + local itemCount = #argument.items for i, item in ipairs(argument.items) do - itemCount = itemCount + 1 if item == value then index = i end end - if not index then return {} end - local label = l10n(value) + local label = l10n(tostring(value)) + local labelColor = nil + if index == nil then + labelColor = util.color.rgb(1, 0, 0) + end local body = { type = ui.TYPE.Flex, props = { @@ -177,6 +186,10 @@ return function(registerRenderer) }, events = { mouseClick = async:callback(function() + if not index then + set(argument.items[#argument.items]) + return + end index = (index - 2) % itemCount + 1 set(argument.items[index]) end), @@ -187,6 +200,7 @@ return function(registerRenderer) template = I.MWUI.templates.textNormal, props = { text = label, + textColor = labelColor, }, external = { grow = 1, @@ -201,6 +215,10 @@ return function(registerRenderer) }, events = { mouseClick = async:callback(function() + if not index then + set(argument.items[1]) + return + end index = (index) % itemCount + 1 set(argument.items[index]) end), @@ -246,7 +264,9 @@ return function(registerRenderer) focusLoss = async:callback(function() if not lastInput then return end if not pcall(function() set(util.color.hex(lastInput)) end) - then set(value) end + then + set(value) + end end), }, } diff --git a/files/data/scripts/omw/ui.lua b/files/data/scripts/omw/ui.lua index 48412f6a0f..1e76b8e141 100644 --- a/files/data/scripts/omw/ui.lua +++ b/files/data/scripts/omw/ui.lua @@ -236,7 +236,7 @@ return { --- -- Returns if the player HUD is visible or not -- @function [parent=#UI] isHudVisible - -- @return #bool + -- @return #boolean isHudVisible = function() return ui._isHudVisible() end, -- TODO diff --git a/files/data/scripts/omw/usehandlers.lua b/files/data/scripts/omw/usehandlers.lua index 01203b225c..563e31b3b7 100644 --- a/files/data/scripts/omw/usehandlers.lua +++ b/files/data/scripts/omw/usehandlers.lua @@ -22,7 +22,7 @@ local function useItem(obj, actor, force) end end end - world._runStandardUseAction(obj, actor, force) + world._runStandardUseAction(obj, actor, options.force) end return { diff --git a/files/data/shaders/debug.omwfx b/files/data/shaders/debug.omwfx index 360dfa26cd..a0c8754ec4 100644 --- a/files/data/shaders/debug.omwfx +++ b/files/data/shaders/debug.omwfx @@ -19,6 +19,11 @@ uniform_bool uDisplayNormals { display_name = "#{OMWShaders:DisplayNormalsName}"; } +uniform_bool uNormalsInWorldSpace { + default = false; + display_name = "#{OMWShaders:NormalsInWorldSpace}"; +} + fragment main { omw_In vec2 omw_TexCoord; @@ -30,8 +35,12 @@ fragment main { if (uDisplayDepth) omw_FragColor = vec4(vec3(omw_GetLinearDepth(omw_TexCoord) / omw.far * uDepthFactor), 1.0); #if OMW_NORMALS - if (uDisplayNormals && (!uDisplayDepth || omw_TexCoord.x < 0.5)) - omw_FragColor.rgb = omw_GetNormals(omw_TexCoord) * 0.5 + 0.5; + if (uDisplayNormals && (!uDisplayDepth || omw_TexCoord.x < 0.5)) { + if (uNormalsInWorldSpace) + omw_FragColor.rgb = omw_GetNormalsWorldSpace(omw_TexCoord) * 0.5 + 0.5; + else + omw_FragColor.rgb = omw_GetNormals(omw_TexCoord) * 0.5 + 0.5; + } #endif } } 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 abdb099ed7..890532fc13 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 @@ -161,7 +166,7 @@ -- @field openmw.util#Transform startingRotation The object original rotation -- @field #string ownerRecordId NPC who owns the object (nil if missing). Global and self scripts can set the value. -- @field #string ownerFactionId Faction who owns the object (nil if missing). Global and self scripts can set the value. --- @field #number ownerFactionRank Rank required to be allowed to pick up the object. Global and self scripts can set the value. +-- @field #number ownerFactionRank Rank required to be allowed to pick up the object (`nil` if any rank is allowed). Global and self scripts can set the value. -- @field #Cell cell The cell where the object currently is. During loading a game and for objects in an inventory or a container `cell` is nil. -- @field #GameObject parentContainer Container or actor that contains (or has in inventory) this object. It is nil if the object is in a cell. -- @field #any type Type of the object (one of the tables from the package @{openmw.types#types}). @@ -265,6 +270,7 @@ -- @function [parent=#GameObject] split -- @param self -- @param #number count The number of items to return. +-- @return #GameObject -- @usage -- take 50 coins from `money` and put to the container `cont` -- money:split(50):moveInto(types.Container.content(cont)) @@ -861,8 +867,11 @@ -- @field #string name Human-readable name -- @field #string description Human-readable description -- @field #string icon VFS path to the icon +-- @field #string specialization Skill specialization. Either combat, magic, or stealth. -- @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 4ca4e5af4e..0a85602bcc 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 @@ -108,6 +107,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 @@ -153,7 +153,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 --- @@ -187,10 +187,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. @@ -327,4 +327,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/interfaces.lua b/files/lua_api/openmw/interfaces.lua index 5048fc2f3e..d4a290aa47 100644 --- a/files/lua_api/openmw/interfaces.lua +++ b/files/lua_api/openmw/interfaces.lua @@ -20,6 +20,9 @@ --- -- @field [parent=#interfaces] scripts.omw.ui#scripts.omw.ui UI +--- +-- @field [parent=#interfaces] scripts.omw.usehandlers#scripts.omw.usehandlers ItemUsage + --- -- @function [parent=#interfaces] __index -- @param #interfaces self diff --git a/files/lua_api/openmw/nearby.lua b/files/lua_api/openmw/nearby.lua index 2ee44a6df6..70b09efd90 100644 --- a/files/lua_api/openmw/nearby.lua +++ b/files/lua_api/openmw/nearby.lua @@ -108,22 +108,22 @@ --- -- @type NAVIGATOR_FLAGS --- @field [parent=#NAVIGATOR_FLAGS] #number Walk allow agent to walk on the ground area; --- @field [parent=#NAVIGATOR_FLAGS] #number Swim allow agent to swim on the water surface; --- @field [parent=#NAVIGATOR_FLAGS] #number OpenDoor allow agent to open doors on the way; --- @field [parent=#NAVIGATOR_FLAGS] #number UsePathgrid allow agent to use predefined pathgrid imported from ESM files. +-- @field [parent=#NAVIGATOR_FLAGS] #number Walk Allow agent to walk on the ground area. +-- @field [parent=#NAVIGATOR_FLAGS] #number Swim Allow agent to swim on the water surface. +-- @field [parent=#NAVIGATOR_FLAGS] #number OpenDoor Allow agent to open doors on the way. +-- @field [parent=#NAVIGATOR_FLAGS] #number UsePathgrid Allow agent to use predefined pathgrid imported from ESM files. --- -- @type COLLISION_SHAPE_TYPE -- @field [parent=#COLLISION_SHAPE_TYPE] #number Aabb Axis-Aligned Bounding Box is used for NPC and symmetric --- Creatures; +-- Creatures. -- @field [parent=#COLLISION_SHAPE_TYPE] #number RotatingBox is used for Creatures with big difference in width and --- height; +-- height. -- @field [parent=#COLLISION_SHAPE_TYPE] #number Cylinder is used for NPC and symmetric Creatures. --- -- @type FIND_PATH_STATUS --- @field [parent=#FIND_PATH_STATUS] #number Success Path is found; +-- @field [parent=#FIND_PATH_STATUS] #number Success Path is found. -- @field [parent=#FIND_PATH_STATUS] #number PartialPath Last path point is not a destination but a nearest position -- among found; -- @field [parent=#FIND_PATH_STATUS] #number NavMeshNotFound Provided `agentBounds` don't have corresponding navigation @@ -135,16 +135,63 @@ -- @field [parent=#FIND_PATH_STATUS] #number EndPolygonNotFound `destination` position is too far from available -- navigation mesh. The status may appear when navigation mesh is not fully generated or position is outside of covered -- area; --- @field [parent=#FIND_PATH_STATUS] #number TargetPolygonNotFound adjusted `destination` position is too far from available --- navigation mesh. The status may appear when navigation mesh is not fully generated or position is outside of covered --- area; +-- @field [parent=#FIND_PATH_STATUS] #number TargetPolygonNotFound adjusted `destination` position is too far from +-- available navigation mesh. The status may appear when navigation mesh is not fully generated or position is outside +-- of covered area; -- @field [parent=#FIND_PATH_STATUS] #number MoveAlongSurfaceFailed Found path couldn't be smoothed due to imperfect -- algorithm implementation or bad navigation mesh data; -- @field [parent=#FIND_PATH_STATUS] #number FindPathOverPolygonsFailed Path over navigation mesh from `source` to -- `destination` does not exist or navigation mesh is not fully generated to provide the path; -- @field [parent=#FIND_PATH_STATUS] #number InitNavMeshQueryFailed Couldn't initialize required data due to bad input -- or bad navigation mesh data. --- @field [parent=#FIND_PATH_STATUS] #number FindStraightPathFailed Couldn't map path over polygons into world coordinates. +-- @field [parent=#FIND_PATH_STATUS] #number FindStraightPathFailed Couldn't map path over polygons into world +-- coordinates. + +--- +-- A table of parameters identifying navmesh +-- @type AgentBounds +-- @field [parent=#AgentBounds] #COLLISION_SHAPE_TYPE shapeType. +-- @field [parent=#AgentBounds] openmw.util#Vector3 halfExtents. + +--- +-- A table of parameters to specify relative path cost per each area type +-- @type AreaCosts +-- @field [parent=#AreaCosts] #number ground Value >= 0, used in combination with @{#NAVIGATOR_FLAGS.Walk} (default: 1). +-- @field [parent=#AreaCosts] #number water Value >= 0, used in combination with @{#NAVIGATOR_FLAGS.Swim} (default: 1). +-- @field [parent=#AreaCosts] #number door Value >= 0, used in combination with @{#NAVIGATOR_FLAGS.OpenDoor} +-- (default: 2). +-- @field [parent=#AreaCosts] #number pathgrid Value >= 0, used in combination with @{#NAVIGATOR_FLAGS.UsePathgrid} +-- (default: 1). + +--- +-- A table of parameters for @{#nearby.findPath} +-- @type FindPathOptions +-- @field [parent=#FindPathOptions] #AgentBounds agentBounds identifies which navmesh to use. +-- @field [parent=#FindPathOptions] #number includeFlags allowed areas for agent to move, a sum of @{#NAVIGATOR_FLAGS} +-- values (default: @{#NAVIGATOR_FLAGS.Walk} + @{#NAVIGATOR_FLAGS.Swim} + @{#NAVIGATOR_FLAGS.OpenDoor} +-- + @{#NAVIGATOR_FLAGS.UsePathgrid}). +-- @field [parent=#FindPathOptions] #AreaCosts areaCosts a table defining relative cost for each type of area. +-- @field [parent=#FindPathOptions] #number destinationTolerance a floating point number representing maximum allowed +-- distance between destination and a nearest point on the navigation mesh in addition to agent size (default: 1). + +--- +-- A table of parameters for @{#nearby.findRandomPointAroundCircle} and @{#nearby.castNavigationRay} +-- @type NavMeshOptions +-- @field [parent=#NavMeshOptions] #AgentBounds agentBounds Identifies which navmesh to use. +-- @field [parent=#NavMeshOptions] #number includeFlags Allowed areas for agent to move, a sum of @{#NAVIGATOR_FLAGS} +-- values (default: @{#NAVIGATOR_FLAGS.Walk} + @{#NAVIGATOR_FLAGS.Swim} + @{#NAVIGATOR_FLAGS.OpenDoor} +-- + @{#NAVIGATOR_FLAGS.UsePathgrid}). + +--- +-- A table of parameters for @{#nearby.findNearestNavMeshPosition} +-- @type FindNearestNavMeshPositionOptions +-- @field [parent=#NavMeshOptions] #AgentBounds agentBounds Identifies which navmesh to use. +-- @field [parent=#NavMeshOptions] #number includeFlags Allowed areas for agent to move, a sum of @{#NAVIGATOR_FLAGS} +-- values (default: @{#NAVIGATOR_FLAGS.Walk} + @{#NAVIGATOR_FLAGS.Swim} + @{#NAVIGATOR_FLAGS.OpenDoor} +-- + @{#NAVIGATOR_FLAGS.UsePathgrid}). +-- @field [parent=#NavMeshOptions] openmw.util#Vector3 searchAreaHalfExtents Defines AABB like area half extents around +-- given position (default: (1 + 2 * CellGridRadius) * CellSize * (1, 1, 1) where CellGridRadius and depends on cell +-- type to cover the whole active grid). --- -- Find path over navigation mesh from source to destination with given options. Result is unstable since navigation @@ -152,24 +199,7 @@ -- @function [parent=#nearby] findPath -- @param openmw.util#Vector3 source Initial path position. -- @param openmw.util#Vector3 destination Final path position. --- @param #table options An optional table with additional optional arguments. Can contain: --- --- * `agentBounds` - a table identifying which navmesh to use, can contain: --- --- * `shapeType` - one of @{#COLLISION_SHAPE_TYPE} values; --- * `halfExtents` - @{openmw.util#Vector3} defining agent bounds size; --- * `includeFlags` - allowed areas for agent to move, a sum of @{#NAVIGATOR_FLAGS} values --- (default: @{#NAVIGATOR_FLAGS.Walk} + @{#NAVIGATOR_FLAGS.Swim} + --- @{#NAVIGATOR_FLAGS.OpenDoor} + @{#NAVIGATOR_FLAGS.UsePathgrid}); --- * `areaCosts` - a table defining relative cost for each type of area, can contain: --- --- * `ground` - a floating point number >= 0, used in combination with @{#NAVIGATOR_FLAGS.Walk} (default: 1); --- * `water` - a floating point number >= 0, used in combination with @{#NAVIGATOR_FLAGS.Swim} (default: 1); --- * `door` - a floating point number >= 0, used in combination with @{#NAVIGATOR_FLAGS.OpenDoor} (default: 2); --- * `pathgrid` - a floating point number >= 0, used in combination with @{#NAVIGATOR_FLAGS.UsePathgrid} --- (default: 1); --- * `destinationTolerance` - a floating point number representing maximum allowed distance between destination and a --- nearest point on the navigation mesh in addition to agent size (default: 1); +-- @param #FindPathOptions options An optional table with additional optional arguments. -- @return #FIND_PATH_STATUS -- @return #list -- @usage local status, path = nearby.findPath(source, destination) @@ -189,15 +219,7 @@ -- @function [parent=#nearby] findRandomPointAroundCircle -- @param openmw.util#Vector3 position Center of the search circle. -- @param #number maxRadius Approximate maximum search distance. --- @param #table options An optional table with additional optional arguments. Can contain: --- --- * `agentBounds` - a table identifying which navmesh to use, can contain: --- --- * `shapeType` - one of @{#COLLISION_SHAPE_TYPE} values; --- * `halfExtents` - @{openmw.util#Vector3} defining agent bounds size; --- * `includeFlags` - allowed areas for agent to move, a sum of @{#NAVIGATOR_FLAGS} values --- (default: @{#NAVIGATOR_FLAGS.Walk} + @{#NAVIGATOR_FLAGS.Swim} + --- @{#NAVIGATOR_FLAGS.OpenDoor} + @{#NAVIGATOR_FLAGS.UsePathgrid}); +-- @param #NavMeshOptions options An optional table with additional optional arguments. -- @return openmw.util#Vector3, #nil -- @usage local position = nearby.findRandomPointAroundCircle(position, maxRadius) -- @usage local position = nearby.findRandomPointAroundCircle(position, maxRadius, { @@ -213,15 +235,7 @@ -- @function [parent=#nearby] castNavigationRay -- @param openmw.util#Vector3 from Initial ray position. -- @param openmw.util#Vector3 to Target ray position. --- @param #table options An optional table with additional optional arguments. Can contain: --- --- * `agentBounds` - a table identifying which navmesh to use, can contain: --- --- * `shapeType` - one of @{#COLLISION_SHAPE_TYPE} values; --- * `halfExtents` - @{openmw.util#Vector3} defining agent bounds size; --- * `includeFlags` - allowed areas for agent to move, a sum of @{#NAVIGATOR_FLAGS} values --- (default: @{#NAVIGATOR_FLAGS.Walk} + @{#NAVIGATOR_FLAGS.Swim} + --- @{#NAVIGATOR_FLAGS.OpenDoor} + @{#NAVIGATOR_FLAGS.UsePathgrid}); +-- @param #NavMeshOptions options An optional table with additional optional arguments. -- @return openmw.util#Vector3, #nil -- @usage local position = nearby.castNavigationRay(from, to) -- @usage local position = nearby.castNavigationRay(from, to, { @@ -231,4 +245,22 @@ -- agentBounds = Actor.getPathfindingAgentBounds(self), -- }) +--- +-- Finds a nearest position on navigation mesh to the given position within given search area. +-- @function [parent=#nearby] findNearestNavMeshPosition +-- @param openmw.util#Vector3 position Search area center. +-- @param #FindNearestNavMeshPositionOptions options An optional table with additional optional arguments. +-- @return openmw.util#Vector3, #nil +-- @usage local navMeshPosition = nearby.findNearestNavMeshPosition(position) +-- @usage local navMeshPosition = nearby.findNearestNavMeshPosition(position, { +-- includeFlags = nearby.NAVIGATOR_FLAGS.Swim, +-- }) +-- @usage local navMeshPosition = nearby.findNearestNavMeshPosition(position, { +-- agentBounds = Actor.getPathfindingAgentBounds(self), +-- }) +-- @usage local navMeshPosition = nearby.findNearestNavMeshPosition(position, { +-- searchAreaHalfExtents = util.vector3(1000, 1000, 1000), +-- includeFlags = nearby.NAVIGATOR_FLAGS.Walk, +-- }) + return nil diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index 69ce5fbaf2..a350d4dbea 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -15,6 +15,12 @@ -- @param openmw.core#GameObject actor -- @return #number +--- +-- Check if the given actor is dead. +-- @function [parent=#Actor] isDead +-- @param openmw.core#GameObject actor +-- @return #boolean + --- -- Agent bounds to be used for pathfinding functions. -- @function [parent=#Actor] getPathfindingAgentBounds @@ -669,6 +675,15 @@ -- @param openmw.core#GameObject object -- @return #boolean +--- +-- Set of properties that differentiates one item from another of the same record type. +-- @function [parent=#Item] itemData +-- @param openmw.core#GameObject item +-- @return #ItemData + +--- +-- @type ItemData +-- @field #number condition The item's current condition. Time remaining for lights. Uses left for lockpicks and probes. Current health for weapons and armor. -------------------------------------------------------------------------------- -- @{#Creature} functions @@ -712,7 +727,7 @@ -- @field #number soulValue The soul value of the creature record -- @field #number type The @{#Creature.TYPE} of the creature -- @field #number baseGold The base barter gold of the creature --- @field #list<#string> servicesOffered The services of the creature, in a table. Value is if the service is provided or not, and they are indexed by: Spells, Spellmaking, Enchanting, Training, Repair, Barter, Weapon, Armor, Clothing, Books, Ingredients, Picks, Probes, Lights, Apparatus, RepairItems, Misc, Potions, MagicItems, Travel. +-- @field #map<#string, #boolean> servicesOffered The services of the creature, in a table. Value is if the service is provided or not, and they are indexed by: Spells, Spellmaking, Enchanting, Training, Repair, Barter, Weapon, Armor, Clothing, Books, Ingredients, Picks, Probes, Lights, Apparatus, RepairItems, Misc, Potions, MagicItems, Travel. --- @{#NPC} functions @@ -831,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. @@ -872,6 +887,31 @@ -- @param openmw.core#GameObject actor -- @return #number +--- @{#Classes}: Class Data +-- @field [parent=#NPC] #Classes classes + +--- +-- A read-only list of all @{#ClassRecord}s in the world database. +-- @field [parent=#Classes] #list<#ClassRecord> records + +--- +-- Returns a read-only @{#ClassRecord} +-- @function [parent=#Classes] record +-- @param #string recordId +-- @return #ClassRecord + +--- +-- Class data record +-- @type ClassRecord +-- @field #string id Class id +-- @field #string name Class name +-- @field #list<#string> attributes A read-only list containing the specialized attributes of the class. +-- @field #list<#string> majorSkills A read-only list containing the major skills of the class. +-- @field #list<#string> minorSkills A read-only list containing the minor skills of the class. +-- @field #string description Class description +-- @field #boolean isPlayable True if the player can play as this class +-- @field #string specialization Class specialization. Either combat, magic, or stealth. + --- -- Whether the NPC or player is in the werewolf form at the moment. -- @function [parent=#NPC] isWerewolf @@ -896,7 +936,7 @@ -- @field #number baseGold The base barter gold of the NPC -- @field #number baseDisposition NPC's starting disposition -- @field #bool isMale The gender setting of the NPC --- @field #list<#string> servicesOffered The services of the NPC, in a table. Value is if the service is provided or not, and they are indexed by: Spells, Spellmaking, Enchanting, Training, Repair, Barter, Weapon, Armor, Clothing, Books, Ingredients, Picks, Probes, Lights, Apparatus, RepairItems, Misc, Potions, MagicItems, Travel. +-- @field #map<#string, #boolean> servicesOffered The services of the NPC, in a table. Value is if the service is provided or not, and they are indexed by: Spells, Spellmaking, Enchanting, Training, Repair, Barter, Weapon, Armor, Clothing, Books, Ingredients, Picks, Probes, Lights, Apparatus, RepairItems, Misc, Potions, MagicItems, Travel. -------------------------------------------------------------------------------- @@ -919,7 +959,7 @@ -- @function [parent=#Player] getCrimeLevel -- @param openmw.core#GameObject player -- @return #number - + --- -- Whether the character generation for this player is finished. -- @function [parent=#Player] isCharGenFinished @@ -930,7 +970,6 @@ -- Whether teleportation for this player is enabled. -- @function [parent=#Player] isTeleportingEnabled -- @param openmw.core#GameObject player --- @param #boolean player -- @return #boolean --- @@ -1058,7 +1097,7 @@ -- @param #ArmorRecord armor A Lua table with the fields of a ArmorRecord, with an additional field `template` that accepts a @{#ArmorRecord} as a base. -- @return #ArmorRecord A strongly typed Armor record. -- @usage local armorTemplate = types.Armor.record('orcish_cuirass') --- local armorTable = {name = "Better Orcish Cuirass",template = armorTemplate,baseArmor = armorTemplate.baseArmor + 10} +-- local armorTable = {name = "Better Orcish Cuirass",template = armorTemplate,baseArmor = armorTemplate.baseArmor + 10} -- --This is the new record we want to create, with a record provided as a template. -- local recordDraft = types.Armor.createRecordDraft(armorTable)--Need to convert the table into the record draft -- local newRecord = world.createRecord(recordDraft)--This creates the actual record @@ -1185,7 +1224,7 @@ -- @param #ClothingRecord clothing A Lua table with the fields of a ClothingRecord, with an additional field `template` that accepts a @{#ClothingRecord} as a base. -- @return #ClothingRecord A strongly typed clothing record. -- @usage local clothingTemplate = types.Clothing.record('exquisite_robe_01') --- local clothingTable = {name = "Better Exquisite Robe",template = clothingTemplate,enchantCapacity = clothingTemplate.enchantCapacity + 10} +-- local clothingTable = {name = "Better Exquisite Robe",template = clothingTemplate,enchantCapacity = clothingTemplate.enchantCapacity + 10} -- --This is the new record we want to create, with a record provided as a template. -- local recordDraft = types.Clothing.createRecordDraft(clothingTable)--Need to convert the table into the record draft -- local newRecord = world.createRecord(recordDraft)--This creates the actual record @@ -1875,6 +1914,9 @@ --- Functions for @{#ESM4Ingredient} objects -- @field [parent=#types] #ESM4Ingredient ESM4Ingredient +--- Functions for @{#ESM4ItemMod} objects +-- @field [parent=#types] #ESM4ItemMod ESM4ItemMod + --- Functions for @{#ESM4Light} objects -- @field [parent=#types] #ESM4Light ESM4Light diff --git a/files/lua_api/openmw/ui.lua b/files/lua_api/openmw/ui.lua index 2fefe4fd84..451f919077 100644 --- a/files/lua_api/openmw/ui.lua +++ b/files/lua_api/openmw/ui.lua @@ -164,9 +164,9 @@ --- -- Content. An array-like container, which allows to reference elements by their name. --- Implements [iterables#List](iterables.html#List) of #Layout and [iterables#Map](iterables.html#Map) of #string to #Layout. +-- Implements [iterables#List](iterables.html#List) of #Layout or #Element and [iterables#Map](iterables.html#Map) of #string to #Layout or #Element. -- @type Content --- @list <#Layout> +-- @list <#any> -- @usage -- local content = ui.content { -- { name = 'input' }, @@ -200,27 +200,27 @@ -- @function [parent=#Content] __index -- @param self -- @param #string name --- @return #Layout +-- @return #any --- -- Puts the layout at given index by shifting all the elements after it -- @function [parent=#Content] insert -- @param self -- @param #number index --- @param #Layout layout +-- @param #any layoutOrElement --- -- Adds the layout at the end of the Content -- (same as calling insert with `last index + 1`) -- @function [parent=#Content] add -- @param self --- @param #Layout layout +-- @param #any layoutOrElement --- -- Finds the index of the given layout. If it is not in the container, returns nil -- @function [parent=#Content] indexOf -- @param self --- @param #Layout layout +-- @param #any layoutOrElement -- @return #number, #nil index --- @@ -228,10 +228,35 @@ -- @type Element --- --- Refreshes the rendered element to match the current layout state +-- Refreshes the rendered element to match the current layout state. +-- Refreshes positions and sizes, but not the layout of the child Elements. -- @function [parent=#Element] update -- @param self +-- @usage +-- local child = ui.create { +-- type = ui.TYPE.Text, +-- props = { +-- text = 'child 1', +-- }, +-- } +-- local parent = ui.create { +-- content = ui.content { +-- child, +-- { +-- type = ui.TYPE.Text, +-- props = { +-- text = 'parent 1', +-- }, +-- } +-- } +-- } +-- -- ... +-- child.layout.props.text = 'child 2' +-- parent.layout.content[2].props.text = 'parent 2' +-- parent:update() -- will show 'parent 2', but 'child 1' + + --- -- Destroys the element -- @function [parent=#Element] destroy diff --git a/files/lua_api/openmw/world.lua b/files/lua_api/openmw/world.lua index 13fa75e0ad..404b744eb8 100644 --- a/files/lua_api/openmw/world.lua +++ b/files/lua_api/openmw/world.lua @@ -22,6 +22,10 @@ -- Functions related to MWScript. -- @type MWScriptFunctions +--- +-- @type MWScriptVariables +-- @map <#string, #number> + --- -- Returns local mwscript on ``object``. Returns `nil` if the script doesn't exist or is not started. -- @function [parent=#MWScriptFunctions] getLocalScript @@ -33,7 +37,7 @@ -- Returns mutable global variables. In multiplayer, these may be specific to the provided player. -- @function [parent=#MWScriptFunctions] getGlobalVariables -- @param openmw.core#GameObject player (optional) Will be used in multiplayer mode to get the globals if there is a separate instance for each player. Currently has no effect. --- @return #list<#number> +-- @return #MWScriptVariables --- -- Returns global mwscript with given recordId. Returns `nil` if the script doesn't exist or is not started. @@ -107,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 ece773a677..4a90a46cc5 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -365,6 +365,11 @@ unarmed creature attacks damage armor = false # 2 = Cylinder actor collision shape type = 0 +# When false the player character will base movement on animations. This will sway the camera +# while moving in third person like in vanilla, and reproduce movement bugs caused by glitchy +# vanilla animations. +player movement ignores animation = false + [General] # Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16). @@ -640,7 +645,7 @@ contrast = 1.0 # Video gamma setting. (>0.0). No effect in Linux. gamma = 1.0 -# Type of screenshot to take (regular, cylindrical, spherical or planet), optionally followed by +# Type of screenshot to take (regular, cylindrical, spherical, planet or cubemap), optionally followed by # screenshot width, height and cubemap resolution in pixels. (e.g. spherical 1600 1000 1200) screenshot type = regular @@ -1131,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 8c429947b0..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 += getSpecular(viewNormal, normalize(passViewPos.xyz), shininess, matSpec) * 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 4caf6c97e2..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 += getSpecular(viewNormal, viewVec, shininess, matSpec) * 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 a2fbddf3f7..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 += getSpecular(viewNormal, normalize(passViewPos), shininess, matSpec) * 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 83fb8a53e0..5817b0c5ae 100644 --- a/files/shaders/compatibility/water.frag +++ b/files/shaders/compatibility/water.frag @@ -43,6 +43,7 @@ const float SCATTER_AMOUNT = 0.3; // amount of sunlight scatter const vec3 SCATTER_COLOUR = vec3(0.0,1.0,0.95); // colour of sunlight scattering const vec3 SUN_EXT = vec3(0.45, 0.55, 0.68); //sunlight extinction +const float SUN_SPEC_FADING_THRESHOLD = 0.15; // visibility at which sun specularity starts to fade const float SPEC_HARDNESS = 256.0; // specular highlights hardness @@ -80,6 +81,7 @@ uniform float near; uniform float far; uniform float rainIntensity; +uniform bool enableRainRipples; uniform vec2 screenRes; @@ -112,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); @@ -172,6 +174,8 @@ void main(void) vec3 waterColor = WATER_COLOR * sunFade; vec4 sunSpec = lcalcSpecular(0); + // 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 vec3 skyColorEstimate = vec3(max(0.0, mix(-0.3, 1.0, sunFade))); @@ -179,7 +183,7 @@ void main(void) #if REFRACTION // no alpha here, so make sure raindrop ripple specularity gets properly subdued - rainSpecular *= clamp(fresnel*6.0 + specular * sunSpec.w, 0.0, 1.0); + rainSpecular *= clamp(fresnel*6.0 + specular * sunSpec.a, 0.0, 1.0); // refraction vec3 refraction = sampleRefractionMap(screenCoords - screenCoordsOffset).rgb; @@ -199,8 +203,8 @@ 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); - gl_FragData[0].xyz = mix( mix(refraction, scatterColour, lightScatter), reflection, fresnel) + specular * sunSpec.xyz + rainSpecular; + 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; // wobbly water: hard-fade into refraction texture at extremely low depth, with a wobble based on normal mapping @@ -213,8 +217,8 @@ void main(void) shoreOffset = clamp(mix(shoreOffset, 1.0, clamp(linearDepth / WOBBLY_SHORE_FADE_DISTANCE, 0.0, 1.0)), 0.0, 1.0); gl_FragData[0].xyz = mix(rawRefraction, gl_FragData[0].xyz, shoreOffset); #else - gl_FragData[0].xyz = mix(reflection, waterColor, (1.0-fresnel)*0.5) + specular * sunSpec.xyz + rainSpecular; - gl_FragData[0].w = clamp(fresnel*6.0 + specular * sunSpec.w, 0.0, 1.0); //clamp(fresnel*2.0 + specular * gl_LightSource[0].specular.w, 0.0, 1.0); + gl_FragData[0].xyz = mix(reflection, waterColor, (1.0-fresnel)*0.5) + specular * sunSpec.rgb * sunSpec.a + rainSpecular; + gl_FragData[0].w = clamp(fresnel*6.0 + specular * sunSpec.a, 0.0, 1.0); //clamp(fresnel*2.0 + specular * gl_LightSource[0].specular.a, 0.0, 1.0); #endif gl_FragData[0] = applyFogAtDist(gl_FragData[0], radialDepth, linearDepth, far); diff --git a/files/shaders/lib/light/lighting.glsl b/files/shaders/lib/light/lighting.glsl index 8c1262ba4d..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,86 +17,67 @@ 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; + return lambert; } -void perLightPoint(out vec3 ambientOut, out vec3 diffuseOut, int lightIndex, vec3 viewPos, vec3 viewNormal) +float calcSpecIntensity(vec3 viewNormal, vec3 viewDir, float shininess, vec3 lightDir) { - 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) + if (dot(viewNormal, lightDir) > 0.0) { - ambientOut = vec3(0.0); - diffuseOut = vec3(0.0); - return; + vec3 halfVec = normalize(lightDir - viewDir); + float NdotH = max(dot(viewNormal, halfVec), 0.0); + return pow(NdotH, shininess); } -#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; + return 0.0; } #if PER_PIXEL_LIGHTING -void doLighting(vec3 viewPos, vec3 viewNormal, float shadowing, out vec3 diffuseLight, out vec3 ambientLight) +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, out vec3 diffuseLight, out vec3 ambientLight, out vec3 shadowDiffuse) +void doLighting(vec3 viewPos, vec3 viewNormal, float shininess, out vec3 diffuseLight, out vec3 ambientLight, out vec3 specularLight, out vec3 shadowDiffuse, out vec3 shadowSpecular) #endif { - vec3 ambientOut, diffuseOut; + vec3 viewDir = normalize(viewPos); + shininess = max(shininess, 1e-4); - perLightSun(diffuseOut, viewPos, viewNormal); + 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 = diffuseOut * shadowing; + diffuseLight *= shadowing; + specularLight *= shadowing; #else - shadowDiffuse = diffuseOut; + shadowDiffuse = diffuseLight; + shadowSpecular = specularLight; diffuseLight = vec3(0.0); + specularLight = vec3(0.0); #endif for (int i = @startLight; i < @endLight; ++i) { #if @lightingMethodUBO - perLightPoint(ambientOut, diffuseOut, PointLightIndex[i], viewPos, viewNormal); + int lightIndex = PointLightIndex[i]; #else - perLightPoint(ambientOut, diffuseOut, i, viewPos, viewNormal); + int lightIndex = i; #endif - ambientLight += ambientOut; - diffuseLight += diffuseOut; + 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 + + vec3 lightDir = lightPos / lightDistance; + + 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; } } -vec3 getSpecular(vec3 viewNormal, vec3 viewDirection, float shininess, vec3 matSpec) -{ - vec3 lightDir = normalize(lcalcPosition(0)); - float NdotL = dot(viewNormal, lightDir); - if (NdotL <= 0.0) - return vec3(0.0); - vec3 halfVec = normalize(lightDir - viewDirection); - float NdotH = dot(viewNormal, halfVec); - return pow(max(NdotH, 0.0), max(1e-4, shininess)) * lcalcSpecular(0).xyz * matSpec; -} - #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 93c16a8b88..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) @@ -82,7 +80,8 @@ testing.registerLocalTest('findPath', } local status, path = nearby.findPath(src, dst, options) testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status') - testing.expectLessOrEqual((path[path:size()] - dst):length(), 1, 'Last path point') + testing.expectLessOrEqual((path[path:size()] - dst):length(), 1, + 'Last path point ' .. testing.formatActualExpected(path[path:size()], dst)) end) testing.registerLocalTest('findRandomPointAroundCircle', @@ -94,7 +93,8 @@ testing.registerLocalTest('findRandomPointAroundCircle', includeFlags = nearby.NAVIGATOR_FLAGS.Walk, } local result = nearby.findRandomPointAroundCircle(position, maxRadius, options) - testing.expectGreaterThan((result - position):length(), 1, 'Random point') + testing.expectGreaterThan((result - position):length(), 1, + 'Random point ' .. testing.formatActualExpected(result, position)) end) testing.registerLocalTest('castNavigationRay', @@ -106,7 +106,22 @@ testing.registerLocalTest('castNavigationRay', includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim, } local result = nearby.castNavigationRay(src, dst, options) - testing.expectLessOrEqual((result - dst):length(), 1, 'Navigation hit point') + testing.expectLessOrEqual((result - dst):length(), 1, + 'Navigation hit point ' .. testing.formatActualExpected(result, dst)) + end) + +testing.registerLocalTest('findNearestNavMeshPosition', + function() + local position = util.vector3(4096, 4096, 1000) + local options = { + agentBounds = types.Actor.getPathfindingAgentBounds(self), + includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim, + searchAreaHalfExtents = util.vector3(1000, 1000, 1000), + } + local result = nearby.findNearestNavMeshPosition(position, options) + local expected = util.vector3(4096, 4096, 872.674) + testing.expectLessOrEqual((result - expected):length(), 1, + 'Navigation mesh position ' .. testing.formatActualExpected(result, expected)) end) return { diff --git a/scripts/data/integration_tests/test_lua_api/test.lua b/scripts/data/integration_tests/test_lua_api/test.lua index acc43eca2a..2ec9f09b97 100644 --- a/scripts/data/integration_tests/test_lua_api/test.lua +++ b/scripts/data/integration_tests/test_lua_api/test.lua @@ -95,6 +95,10 @@ tests = { initPlayer() testing.runLocalTest(player, 'castNavigationRay') end}, + {'findNearestNavMeshPosition', function() + initPlayer() + testing.runLocalTest(player, 'findNearestNavMeshPosition') + end}, {'teleport', testTeleport}, {'getGMST', testGetGMST}, } diff --git a/scripts/data/integration_tests/testing_util/testing_util.lua b/scripts/data/integration_tests/testing_util/testing_util.lua index db67cd3b1a..f73ea83e79 100644 --- a/scripts/data/integration_tests/testing_util/testing_util.lua +++ b/scripts/data/integration_tests/testing_util/testing_util.lua @@ -154,6 +154,10 @@ function M.expectThat(value, matcher, msg) end end +function M.formatActualExpected(actual, expected) + return string.format('actual: %s, expected: %s', actual, expected) +end + local localTests = {} local localTestRunner = nil 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), )