diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 06c7e63cb0..406d6411d9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -533,7 +533,7 @@ macOS14_Xcode15_arm64: .Windows_Ninja_Base: tags: - - windows + - saas-windows-medium-amd64 rules: - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" before_script: @@ -570,10 +570,10 @@ macOS14_Xcode15_arm64: - $env:CCACHE_BASEDIR = Get-Location - $env:CCACHE_DIR = "$(Get-Location)\ccache" - New-Item -Type Directory -Force -Path $env:CCACHE_DIR - - New-Item -Type File -Force -Path MSVC2019_64_Ninja\.cmake\api\v1\query\codemodel-v2 - - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2019 -k -V -N -b -t -C $multiview -E + - New-Item -Type File -Force -Path MSVC2022_64_Ninja\.cmake\api\v1\query\codemodel-v2 + - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2022 -k -V -N -b -t -C $multiview -E - Get-Volume - - cd MSVC2019_64_Ninja + - cd MSVC2022_64_Ninja - .\ActivateMSVC.ps1 - cmake --build . --config $config - ccache --show-stats -v @@ -583,47 +583,41 @@ macOS14_Xcode15_arm64: - Get-ChildItem -Recurse *.ilk | Remove-Item - | if (Get-ChildItem -Recurse *.pdb) { - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt + 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt if (Test-Path env:AWS_ACCESS_KEY_ID) { - aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory} + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory} } Push-Location .. ..\CI\Store-Symbols.ps1 if (Test-Path env:AWS_ACCESS_KEY_ID) { aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp --recursive --exclude * --include *.ex_ --include *.dl_ --include *.pd_ .\SymStore s3://openmw-sym } - 7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt + 7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt Pop-Location Get-ChildItem -Recurse *.pdb | Remove-Item } - - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*' + - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*' - | if (Test-Path env:AWS_ACCESS_KEY_ID) { - aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory} + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory} } - if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } } after_script: - Get-Volume - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: - key: ninja-v8 + key: ninja-2022-v9 paths: - ccache - deps - - MSVC2019_64_Ninja/deps/Qt + - MSVC2022_64_Ninja/deps/Qt artifacts: when: always paths: - "*.zip" - "*.log" - - MSVC2019_64_Ninja/*.log - - MSVC2019_64_Ninja/*/*.log - - MSVC2019_64_Ninja/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*/*/*/*/*.log + - MSVC2022_64_Ninja/*.log + - MSVC2022_64_Ninja/**/*.log # 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 @@ -656,7 +650,7 @@ macOS14_Xcode15_arm64: .Windows_MSBuild_Base: tags: - - windows + - saas-windows-medium-amd64 rules: - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" before_script: @@ -688,9 +682,9 @@ macOS14_Xcode15_arm64: - $time = (Get-Date -Format "HH:mm:ss") - echo ${time} - echo "started by ${GITLAB_USER_NAME}" - - New-Item -Type File -Force -Path MSVC2019_64\.cmake\api\v1\query\codemodel-v2 - - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2019 -k -V -b -t -C $multiview -E - - cd MSVC2019_64 + - New-Item -Type File -Force -Path MSVC2022_64\.cmake\api\v1\query\codemodel-v2 + - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2022 -k -V -b -t -C $multiview -E + - cd MSVC2022_64 - Get-Volume - cmake --build . --config $config - cd $config @@ -699,46 +693,40 @@ macOS14_Xcode15_arm64: - Get-ChildItem -Recurse *.ilk | Remove-Item - | if (Get-ChildItem -Recurse *.pdb) { - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt + 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt if (Test-Path env:AWS_ACCESS_KEY_ID) { - aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory} + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory} } Push-Location .. ..\CI\Store-Symbols.ps1 if (Test-Path env:AWS_ACCESS_KEY_ID) { aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp --recursive --exclude * --include *.ex_ --include *.dl_ --include *.pd_ .\SymStore s3://openmw-sym } - 7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt + 7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt Pop-Location Get-ChildItem -Recurse *.pdb | Remove-Item } - - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*' + - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*' - | if (Test-Path env:AWS_ACCESS_KEY_ID) { - aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory} + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory} } - if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } } after_script: - Get-Volume - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: - key: msbuild-v8 + key: msbuild-2022-v9 paths: - deps - - MSVC2019_64/deps/Qt + - MSVC2022_64/deps/Qt artifacts: when: always paths: - "*.zip" - "*.log" - - MSVC2019_64/*.log - - MSVC2019_64/*/*.log - - MSVC2019_64/*/*/*.log - - MSVC2019_64/*/*/*/*.log - - MSVC2019_64/*/*/*/*/*.log - - MSVC2019_64/*/*/*/*/*/*.log - - MSVC2019_64/*/*/*/*/*/*/*.log - - MSVC2019_64/*/*/*/*/*/*/*/*.log + - MSVC2022_64/*.log + - MSVC2022_64/**/*.log # 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f2ee0eb63..e935364264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ 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 #6723: "Turn to movement direction" makes the player rotate wildly with COLLADA 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 @@ -169,6 +170,7 @@ Bug #7899: Editor: Doors can't be unlocked Bug #7901: Editor: Teleport-related fields shouldn't be editable if a ref does not teleport Bug #7908: Key bindings names in the settings menu are layout-specific + Bug #7943: Using "addSoulGem" and "dropSoulGem" commands to creatures works only with "Weapon & Shield" flagged ones Feature #2566: Handle NAM9 records for manual cell references Feature #3537: Shader-based water ripples Feature #5173: Support for NiFogProperty @@ -218,6 +220,7 @@ 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 + Feature #7777: Support external Bethesda material files (BGSM/BGEM) Feature #7792: Support Timescale Clouds Feature #7795: Support MaxNumberRipples INI setting Feature #7805: Lua Menu context diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index 3919507bd1..2423f0c54f 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -357,6 +357,16 @@ add_qt_image_dlls() { QT_IMAGEFORMATS[$CONFIG]="${QT_IMAGEFORMATS[$CONFIG]} $@" } +declare -A QT_ICONENGINES +QT_ICONENGINES["Release"]="" +QT_ICONENGINES["Debug"]="" +QT_ICONENGINES["RelWithDebInfo"]="" +add_qt_icon_dlls() { + local CONFIG=$1 + shift + QT_ICONENGINES[$CONFIG]="${QT_ICONENGINES[$CONFIG]} $@" +} + if [ -z $PLATFORM ]; then PLATFORM="$(uname -m)" fi @@ -563,7 +573,7 @@ ICU_VER="70_1" LUAJIT_VER="v2.1.0-beta3-452-g7a0cf5fd" LZ4_VER="1.9.2" OPENAL_VER="1.23.0" -QT_VER="5.15.2" +QT_VER="6.6.2" OSG_ARCHIVE_NAME="OSGoS 3.6.5" OSG_ARCHIVE="OSGoS-3.6.5-123-g68c5c573d-msvc${OSG_MSVC_YEAR}-win${BITS}" @@ -894,7 +904,7 @@ printf "Qt ${QT_VER}... " printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then pushd "$DEPS" > /dev/null - AQT_VERSION="v3.1.7" + AQT_VERSION="v3.1.12" if ! [ -f "aqt_x64-${AQT_VERSION}.exe" ]; then download "aqt ${AQT_VERSION}"\ "https://github.com/miurahr/aqtinstall/releases/download/${AQT_VERSION}/aqt_x64.exe" \ @@ -915,6 +925,9 @@ printf "Qt ${QT_VER}... " echo Done. fi + QT_MAJOR_VER=$(echo "${QT_VER}" | awk -F '[.]' '{printf "%d", $1}') + QT_MINOR_VER=$(echo "${QT_VER}" | awk -F '[.]' '{printf "%d", $2}') + cd $QT_SDK for CONFIGURATION in ${CONFIGURATIONS[@]}; do if [ $CONFIGURATION == "Debug" ]; then @@ -922,14 +935,24 @@ printf "Qt ${QT_VER}... " else DLLSUFFIX="" fi - if [ "${QT_VER:0:1}" -eq "6" ]; then - add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_VER:0:1}"{Core,Gui,Network,OpenGL,OpenGLWidgets,Widgets,Svg}${DLLSUFFIX}.dll + + if [ "${QT_MAJOR_VER}" -eq 6 ]; then + add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_MAJOR_VER}"{Core,Gui,Network,OpenGL,OpenGLWidgets,Widgets,Svg}${DLLSUFFIX}.dll + + # Since Qt 6.7.0 plugin is called "qmodernwindowsstyle" + if [ "${QT_MINOR_VER}" -ge 7 ]; then + add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qmodernwindowsstyle${DLLSUFFIX}.dll" + else + add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" + fi else - add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_VER:0:1}"{Core,Gui,Network,OpenGL,Widgets,Svg}${DLLSUFFIX}.dll + add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_MAJOR_VER}"{Core,Gui,Network,OpenGL,Widgets,Svg}${DLLSUFFIX}.dll + add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" fi + add_qt_platform_dlls $CONFIGURATION "$(pwd)/plugins/platforms/qwindows${DLLSUFFIX}.dll" - add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" add_qt_image_dlls $CONFIGURATION "$(pwd)/plugins/imageformats/qsvg${DLLSUFFIX}.dll" + add_qt_icon_dlls $CONFIGURATION "$(pwd)/plugins/iconengines/qsvgicon${DLLSUFFIX}.dll" done echo Done. } @@ -1123,7 +1146,7 @@ fi echo " $(basename $DLL)" cp "$DLL" "${DLL_PREFIX}styles" done - + echo echo "- Qt Image Format DLLs..." mkdir -p ${DLL_PREFIX}imageformats for DLL in ${QT_IMAGEFORMATS[$CONFIGURATION]}; do @@ -1131,6 +1154,13 @@ fi cp "$DLL" "${DLL_PREFIX}imageformats" done echo + echo "- Qt Icon Engine DLLs..." + mkdir -p ${DLL_PREFIX}iconengines + for DLL in ${QT_ICONENGINES[$CONFIGURATION]}; do + echo " $(basename $DLL)" + cp "$DLL" "${DLL_PREFIX}iconengines" + done + echo done #fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 074c91344b..b478a799eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -825,6 +825,18 @@ if (OPENMW_OSX_DEPLOYMENT AND APPLE) get_filename_component(QT_QMACSTYLE_PLUGIN_NAME "${QT_QMACSTYLE_PLUGIN_PATH}" NAME) configure_file("${QT_QMACSTYLE_PLUGIN_PATH}" "${APP_BUNDLE_DIR}/Contents/PlugIns/${QT_QMACSTYLE_PLUGIN_GROUP}/${QT_QMACSTYLE_PLUGIN_NAME}" COPYONLY) + get_property(QT_QSVG_PLUGIN_PATH TARGET Qt${QT_VERSION_MAJOR}::QSvgPlugin PROPERTY LOCATION_RELEASE) + get_filename_component(QT_QSVG_PLUGIN_DIR "${QT_QSVG_PLUGIN_PATH}" DIRECTORY) + get_filename_component(QT_QSVG_PLUGIN_GROUP "${QT_QSVG_PLUGIN_DIR}" NAME) + get_filename_component(QT_QSVG_PLUGIN_NAME "${QT_QSVG_PLUGIN_PATH}" NAME) + configure_file("${QT_QSVG_PLUGIN_PATH}" "${APP_BUNDLE_DIR}/Contents/PlugIns/${QT_QSVG_PLUGIN_GROUP}/${QT_QSVG_PLUGIN_NAME}" COPYONLY) + + get_property(QT_QSVG_ICON_PLUGIN_PATH TARGET Qt${QT_VERSION_MAJOR}::QSvgIconPlugin PROPERTY LOCATION_RELEASE) + get_filename_component(QT_QSVG_ICON_PLUGIN_DIR "${QT_QSVG_ICON_PLUGIN_PATH}" DIRECTORY) + get_filename_component(QT_QSVG_ICON_PLUGIN_GROUP "${QT_QSVG_ICON_PLUGIN_DIR}" NAME) + get_filename_component(QT_QSVG_ICON_PLUGIN_NAME "${QT_QSVG_ICON_PLUGIN_PATH}" NAME) + configure_file("${QT_QSVG_ICON_PLUGIN_PATH}" "${APP_BUNDLE_DIR}/Contents/PlugIns/${QT_QSVG_ICON_PLUGIN_GROUP}/${QT_QSVG_ICON_PLUGIN_NAME}" COPYONLY) + configure_file("${OpenMW_SOURCE_DIR}/files/mac/qt.conf" "${APP_BUNDLE_DIR}/Contents/Resources/qt.conf" COPYONLY) if (BUILD_OPENCS) @@ -832,13 +844,9 @@ if (OPENMW_OSX_DEPLOYMENT AND APPLE) set(OPENCS_BUNDLE_NAME "${OPENCS_BUNDLE_NAME_TMP}.app") configure_file("${QT_COCOA_PLUGIN_PATH}" "${OPENCS_BUNDLE_NAME}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}" COPYONLY) configure_file("${QT_QMACSTYLE_PLUGIN_PATH}" "${OPENCS_BUNDLE_NAME}/Contents/PlugIns/${QT_QMACSTYLE_PLUGIN_GROUP}/${QT_QMACSTYLE_PLUGIN_NAME}" COPYONLY) - configure_file("${OpenMW_SOURCE_DIR}/files/mac/qt.conf" "${OPENCS_BUNDLE_NAME}/Contents/Resources/qt.conf" COPYONLY) - - get_property(QT_QSVG_PLUGIN_PATH TARGET Qt${QT_VERSION_MAJOR}::QSvgPlugin PROPERTY LOCATION_RELEASE) - get_filename_component(QT_QSVG_PLUGIN_DIR "${QT_QSVG_PLUGIN_PATH}" DIRECTORY) - get_filename_component(QT_QSVG_PLUGIN_GROUP "${QT_QSVG_PLUGIN_DIR}" NAME) - get_filename_component(QT_QSVG_PLUGIN_NAME "${QT_QSVG_PLUGIN_PATH}" NAME) configure_file("${QT_QSVG_PLUGIN_PATH}" "${OPENCS_BUNDLE_NAME}/Contents/PlugIns/${QT_QSVG_PLUGIN_GROUP}/${QT_QSVG_PLUGIN_NAME}" COPYONLY) + configure_file("${QT_QSVG_ICON_PLUGIN_PATH}" "${OPENCS_BUNDLE_NAME}/Contents/PlugIns/${QT_QSVG_ICON_PLUGIN_GROUP}/${QT_QSVG_ICON_PLUGIN_NAME}" COPYONLY) + configure_file("${OpenMW_SOURCE_DIR}/files/mac/qt.conf" "${OPENCS_BUNDLE_NAME}/Contents/Resources/qt.conf" COPYONLY) endif () install(DIRECTORY "${APP_BUNDLE_DIR}" USE_SOURCE_PERMISSIONS DESTINATION "." COMPONENT Runtime) diff --git a/apps/bulletobjecttool/main.cpp b/apps/bulletobjecttool/main.cpp index b27c8135d6..4dbdb56350 100644 --- a/apps/bulletobjecttool/main.cpp +++ b/apps/bulletobjecttool/main.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -173,7 +174,8 @@ namespace constexpr double expiryDelay = 0; Resource::ImageManager imageManager(&vfs, expiryDelay); Resource::NifFileManager nifFileManager(&vfs, &encoder.getStatelessEncoder()); - Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager, expiryDelay); + Resource::BgsmFileManager bgsmFileManager(&vfs, expiryDelay); + Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager, &bgsmFileManager, expiryDelay); Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager, expiryDelay); Resource::forEachBulletObject( diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index bd6a7062fd..91b985948d 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -83,7 +83,7 @@ target_link_libraries(openmw-launcher components_qt ) -target_link_libraries(openmw-launcher Qt::Widgets Qt::Core) +target_link_libraries(openmw-launcher Qt::Widgets Qt::Core Qt::Svg) if (BUILD_WITH_CODE_COVERAGE) target_compile_options(openmw-launcher PRIVATE --coverage) diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp index 94ab7ef082..d75a1af5e2 100644 --- a/apps/navmeshtool/main.cpp +++ b/apps/navmeshtool/main.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -220,7 +221,8 @@ namespace NavMeshTool Resource::ImageManager imageManager(&vfs, expiryDelay); Resource::NifFileManager nifFileManager(&vfs, &encoder.getStatelessEncoder()); - Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager, expiryDelay); + Resource::BgsmFileManager bgsmFileManager(&vfs, expiryDelay); + Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager, &bgsmFileManager, expiryDelay); Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager, expiryDelay); DetourNavigator::RecastGlobalAllocator::init(); DetourNavigator::Settings navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager(); diff --git a/apps/niftest/niftest.cpp b/apps/niftest/niftest.cpp index b37d85d739..0b8aa8e275 100644 --- a/apps/niftest/niftest.cpp +++ b/apps/niftest/niftest.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -25,7 +26,7 @@ namespace bpo = boost::program_options; /// See if the file has the named extension -bool hasExtension(const std::filesystem::path& filename, const std::string& extensionToFind) +bool hasExtension(const std::filesystem::path& filename, std::string_view extensionToFind) { const auto extension = Files::pathToUnicodeString(filename.extension()); return Misc::StringUtils::ciEqual(extension, extensionToFind); @@ -36,6 +37,13 @@ bool isNIF(const std::filesystem::path& filename) { return hasExtension(filename, ".nif") || hasExtension(filename, ".kf"); } + +/// Check if the file is a material file. +bool isMaterial(const std::filesystem::path& filename) +{ + return hasExtension(filename, ".bgem") || hasExtension(filename, ".bgsm"); +} + /// See if the file has the "bsa" extension. bool isBSA(const std::filesystem::path& filename) { @@ -51,16 +59,17 @@ std::unique_ptr makeArchive(const std::filesystem::path& path) return nullptr; } -void readNIF( +void readFile( const std::filesystem::path& source, const std::filesystem::path& path, const VFS::Manager* vfs, bool quiet) { const std::string pathStr = Files::pathToUnicodeString(path); + const bool isNif = isNIF(path); if (!quiet) { - if (hasExtension(path, ".kf")) - std::cout << "Reading KF file '" << pathStr << "'"; + if (isNif) + std::cout << "Reading " << (hasExtension(path, ".nif") ? "NIF" : "KF") << " file '" << pathStr << "'"; else - std::cout << "Reading NIF file '" << pathStr << "'"; + std::cout << "Reading " << (hasExtension(path, ".bgsm") ? "BGSM" : "BGEM") << " file '" << pathStr << "'"; if (!source.empty()) std::cout << " from '" << Files::pathToUnicodeString(isBSA(source) ? source.filename() : source) << "'"; std::cout << std::endl; @@ -68,12 +77,22 @@ void readNIF( const std::filesystem::path fullPath = !source.empty() ? source / path : path; try { - Nif::NIFFile file(Files::pathToUnicodeString(fullPath)); - Nif::Reader reader(file, nullptr); - if (vfs != nullptr) - reader.parse(vfs->get(pathStr)); + if (isNif) + { + Nif::NIFFile file(Files::pathToUnicodeString(fullPath)); + Nif::Reader reader(file, nullptr); + if (vfs != nullptr) + reader.parse(vfs->get(pathStr)); + else + reader.parse(Files::openConstrainedFileStream(fullPath)); + } else - reader.parse(Files::openConstrainedFileStream(fullPath)); + { + if (vfs != nullptr) + Bgsm::parse(vfs->get(pathStr)); + else + Bgsm::parse(Files::openConstrainedFileStream(fullPath)); + } } catch (std::exception& e) { @@ -95,11 +114,11 @@ void readVFS(std::unique_ptr&& archive, const std::filesystem::pat vfs.addArchive(std::move(archive)); vfs.buildIndex(); - for (const auto& name : vfs.getRecursiveDirectoryIterator("")) + for (const auto& name : vfs.getRecursiveDirectoryIterator()) { - if (isNIF(name.value())) + if (isNIF(name.value()) || isMaterial(name.value())) { - readNIF(archivePath, name.value(), &vfs, quiet); + readFile(archivePath, name.value(), &vfs, quiet); } } @@ -129,10 +148,10 @@ void readVFS(std::unique_ptr&& archive, const std::filesystem::pat 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, KF and BSA/BA2 files + bpo::options_description desc(R"(Ensure that OpenMW can use the provided NIF, KF, BGEM/BGSM and BSA/BA2 files Usages: - niftest + niftest Scan the file or directories for NIF errors. Allowed options)"); @@ -221,9 +240,9 @@ int main(int argc, char** argv) const std::string pathStr = Files::pathToUnicodeString(path); try { - if (isNIF(path)) + if (isNIF(path) || isMaterial(path)) { - readNIF({}, path, vfs.get(), quiet); + readFile({}, path, vfs.get(), quiet); } else if (auto archive = makeArchive(path)) { @@ -231,7 +250,7 @@ int main(int argc, char** argv) } else { - std::cerr << "Error: '" << pathStr << "' is not a NIF/KF file, BSA/BA2 archive, or directory" + std::cerr << "Error: '" << pathStr << "' is not a NIF/KF/BGEM/BGSM file, BSA/BA2 archive, or directory" << std::endl; } } diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 3353a4586f..d6af64b33b 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -257,6 +257,7 @@ endif() if (WIN32) target_link_libraries(openmw-cs-lib ${Boost_LOCALE_LIBRARY}) + target_sources(openmw-cs PRIVATE ${CMAKE_SOURCE_DIR}/files/windows/openmw-cs.exe.manifest) endif() if (WIN32 AND BUILD_OPENCS) diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index bdbb8f697c..d4c35c5cec 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -324,7 +324,6 @@ namespace CSMWorld { ColumnId_MaxAttack, "Max Attack" }, { ColumnId_CreatureMisc, "Creature Misc" }, - { ColumnId_Idle1, "Idle 1" }, { ColumnId_Idle2, "Idle 2" }, { ColumnId_Idle3, "Idle 3" }, { ColumnId_Idle4, "Idle 4" }, @@ -332,6 +331,7 @@ namespace CSMWorld { ColumnId_Idle6, "Idle 6" }, { ColumnId_Idle7, "Idle 7" }, { ColumnId_Idle8, "Idle 8" }, + { ColumnId_Idle9, "Idle 9" }, { ColumnId_RegionWeather, "Weather" }, { ColumnId_WeatherName, "Type" }, diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index 3c4bff07f6..469c1eee33 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -310,14 +310,14 @@ namespace CSMWorld ColumnId_MaxAttack = 284, ColumnId_CreatureMisc = 285, - ColumnId_Idle1 = 286, - ColumnId_Idle2 = 287, - ColumnId_Idle3 = 288, - ColumnId_Idle4 = 289, - ColumnId_Idle5 = 290, - ColumnId_Idle6 = 291, - ColumnId_Idle7 = 292, - ColumnId_Idle8 = 293, + ColumnId_Idle2 = 286, + ColumnId_Idle3 = 287, + ColumnId_Idle4 = 288, + ColumnId_Idle5 = 289, + ColumnId_Idle6 = 290, + ColumnId_Idle7 = 291, + ColumnId_Idle8 = 292, + ColumnId_Idle9 = 293, ColumnId_RegionWeather = 294, ColumnId_WeatherName = 295, diff --git a/apps/opencs/model/world/refidcollection.cpp b/apps/opencs/model/world/refidcollection.cpp index c3af3d4673..e0d5799726 100644 --- a/apps/opencs/model/world/refidcollection.cpp +++ b/apps/opencs/model/world/refidcollection.cpp @@ -210,7 +210,6 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_AiDuration, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_AiWanderToD, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle1, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle2, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle3, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle4, CSMWorld::ColumnBase::Display_Integer)); @@ -218,6 +217,7 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle6, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle7, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle8, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle9, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_AiWanderRepeat, CSMWorld::ColumnBase::Display_Boolean)); mColumns.back().addColumn( diff --git a/apps/opencs/model/world/resources.cpp b/apps/opencs/model/world/resources.cpp index 9957af3d66..7082575c64 100644 --- a/apps/opencs/model/world/resources.cpp +++ b/apps/opencs/model/world/resources.cpp @@ -28,7 +28,7 @@ void CSMWorld::Resources::recreate(const VFS::Manager* vfs, const char* const* e size_t baseSize = mBaseDirectory.size(); - for (const auto& filepath : vfs->getRecursiveDirectoryIterator("")) + for (const auto& filepath : vfs->getRecursiveDirectoryIterator()) { const std::string_view view = filepath.view(); if (view.size() < baseSize + 1 || !view.starts_with(mBaseDirectory) || view[baseSize] != '/') diff --git a/apps/opencs/view/doc/startup.cpp b/apps/opencs/view/doc/startup.cpp index 27463b0456..e6323bf1a1 100644 --- a/apps/opencs/view/doc/startup.cpp +++ b/apps/opencs/view/doc/startup.cpp @@ -10,7 +10,7 @@ #include #include -QPushButton* CSVDoc::StartupDialogue::addButton(const QString& label, const QIcon& icon) +QPushButton* CSVDoc::StartupDialogue::addButton(const QString& label, const QString& icon) { int column = mColumn--; @@ -39,13 +39,13 @@ QWidget* CSVDoc::StartupDialogue::createButtons() mLayout = new QGridLayout(widget); /// \todo add icons - QPushButton* loadDocument = addButton("Edit A Content File", QIcon(":startup/edit-content")); + QPushButton* loadDocument = addButton("Edit A Content File", ":startup/edit-content"); connect(loadDocument, &QPushButton::clicked, this, &StartupDialogue::loadDocument); - QPushButton* createAddon = addButton("Create A New Addon", QIcon(":startup/create-addon")); + QPushButton* createAddon = addButton("Create A New Addon", ":startup/create-addon"); connect(createAddon, &QPushButton::clicked, this, &StartupDialogue::createAddon); - QPushButton* createGame = addButton("Create A New Game", QIcon(":startup/create-game")); + QPushButton* createGame = addButton("Create A New Game", ":startup/create-game"); connect(createGame, &QPushButton::clicked, this, &StartupDialogue::createGame); for (int i = 0; i < 3; ++i) diff --git a/apps/opencs/view/doc/startup.hpp b/apps/opencs/view/doc/startup.hpp index 061b91b2d1..f2cccfcd38 100644 --- a/apps/opencs/view/doc/startup.hpp +++ b/apps/opencs/view/doc/startup.hpp @@ -20,7 +20,7 @@ namespace CSVDoc int mColumn; QGridLayout* mLayout; - QPushButton* addButton(const QString& label, const QIcon& icon); + QPushButton* addButton(const QString& label, const QString& icon); QWidget* createButtons(); diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index f5cdb1b8fc..88a33108c0 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -1110,7 +1110,16 @@ void CSVDoc::View::updateWidth(bool isGrowLimit, int minSubViewWidth) { QRect rect; if (isGrowLimit) - rect = QApplication::screenAt(pos())->geometry(); + { + // Widget position can be negative, we should clamp it. + QPoint position = pos(); + if (position.x() <= 0) + position.setX(0); + if (position.y() <= 0) + position.setY(0); + + rect = QApplication::screenAt(position)->geometry(); + } else rect = desktopRect(); diff --git a/apps/opencs/view/render/actor.hpp b/apps/opencs/view/render/actor.hpp index a9cc34b00d..09d896e7e7 100644 --- a/apps/opencs/view/render/actor.hpp +++ b/apps/opencs/view/render/actor.hpp @@ -62,7 +62,7 @@ namespace CSVRender osg::ref_ptr mBaseNode; SceneUtil::Skeleton* mSkeleton; - SceneUtil::NodeMapVisitor::NodeMap mNodeMap; + SceneUtil::NodeMap mNodeMap; }; } diff --git a/apps/opencs/view/render/instancemovemode.cpp b/apps/opencs/view/render/instancemovemode.cpp index e4004a1537..bc999eb633 100644 --- a/apps/opencs/view/render/instancemovemode.cpp +++ b/apps/opencs/view/render/instancemovemode.cpp @@ -8,7 +8,7 @@ class QWidget; CSVRender::InstanceMoveMode::InstanceMoveMode(QWidget* parent) - : ModeButton(QIcon(QPixmap(":scenetoolbar/transform-move")), + : ModeButton(QIcon(":scenetoolbar/transform-move"), "Move selected instances" "
  • Use {scene-edit-primary} to move instances around freely
  • " "
  • Use {scene-edit-secondary} to move instances around within the grid
  • " diff --git a/apps/opencs/view/widget/scenetoolmode.cpp b/apps/opencs/view/widget/scenetoolmode.cpp index f2795d6de9..1fa7cfb690 100644 --- a/apps/opencs/view/widget/scenetoolmode.cpp +++ b/apps/opencs/view/widget/scenetoolmode.cpp @@ -94,7 +94,7 @@ void CSVWidget::SceneToolMode::showPanel(const QPoint& position) void CSVWidget::SceneToolMode::addButton(const std::string& icon, const std::string& id, const QString& tooltip) { - ModeButton* button = new ModeButton(QIcon(QPixmap(icon.c_str())), tooltip, mPanel); + ModeButton* button = new ModeButton(QIcon(icon.c_str()), tooltip, mPanel); addButton(button, id); } diff --git a/apps/opencs/view/widget/scenetoolshapebrush.cpp b/apps/opencs/view/widget/scenetoolshapebrush.cpp index 57b78ffc71..0e040c2385 100644 --- a/apps/opencs/view/widget/scenetoolshapebrush.cpp +++ b/apps/opencs/view/widget/scenetoolshapebrush.cpp @@ -60,10 +60,10 @@ CSVWidget::ShapeBrushWindow::ShapeBrushWindow(CSMDoc::Document& document, QWidge : QFrame(parent, Qt::Popup) , mDocument(document) { - mButtonPoint = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-point")), "", this); - mButtonSquare = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-square")), "", this); - mButtonCircle = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-circle")), "", this); - mButtonCustom = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-custom")), "", this); + mButtonPoint = new QPushButton(QIcon(":scenetoolbar/brush-point"), "", this); + mButtonSquare = new QPushButton(QIcon(":scenetoolbar/brush-square"), "", this); + mButtonCircle = new QPushButton(QIcon(":scenetoolbar/brush-circle"), "", this); + mButtonCustom = new QPushButton(QIcon(":scenetoolbar/brush-custom"), "", this); mSizeSliders = new ShapeBrushSizeControls("Brush size", this); @@ -201,25 +201,25 @@ void CSVWidget::SceneToolShapeBrush::setButtonIcon(CSVWidget::BrushShape brushSh { case BrushShape_Point: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-point"))); + setIcon(QIcon(":scenetoolbar/brush-point")); tooltip += mShapeBrushWindow->toolTipPoint; break; case BrushShape_Square: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-square"))); + setIcon(QIcon(":scenetoolbar/brush-square")); tooltip += mShapeBrushWindow->toolTipSquare; break; case BrushShape_Circle: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-circle"))); + setIcon(QIcon(":scenetoolbar/brush-circle")); tooltip += mShapeBrushWindow->toolTipCircle; break; case BrushShape_Custom: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-custom"))); + setIcon(QIcon(":scenetoolbar/brush-custom")); tooltip += mShapeBrushWindow->toolTipCustom; break; } diff --git a/apps/opencs/view/widget/scenetooltexturebrush.cpp b/apps/opencs/view/widget/scenetooltexturebrush.cpp index 9c3e723009..2e002aaf2e 100644 --- a/apps/opencs/view/widget/scenetooltexturebrush.cpp +++ b/apps/opencs/view/widget/scenetooltexturebrush.cpp @@ -90,10 +90,10 @@ CSVWidget::TextureBrushWindow::TextureBrushWindow(CSMDoc::Document& document, QW mSelectedBrush = new QLabel(QString::fromStdString(mBrushTextureLabel)); } - mButtonPoint = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-point")), "", this); - mButtonSquare = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-square")), "", this); - mButtonCircle = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-circle")), "", this); - mButtonCustom = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-custom")), "", this); + mButtonPoint = new QPushButton(QIcon(":scenetoolbar/brush-point"), "", this); + mButtonSquare = new QPushButton(QIcon(":scenetoolbar/brush-square"), "", this); + mButtonCircle = new QPushButton(QIcon(":scenetoolbar/brush-circle"), "", this); + mButtonCustom = new QPushButton(QIcon(":scenetoolbar/brush-custom"), "", this); mSizeSliders = new BrushSizeControls("Brush size", this); @@ -282,25 +282,25 @@ void CSVWidget::SceneToolTextureBrush::setButtonIcon(CSVWidget::BrushShape brush { case BrushShape_Point: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-point"))); + setIcon(QIcon(":scenetoolbar/brush-point")); tooltip += mTextureBrushWindow->toolTipPoint; break; case BrushShape_Square: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-square"))); + setIcon(QIcon(":scenetoolbar/brush-square")); tooltip += mTextureBrushWindow->toolTipSquare; break; case BrushShape_Circle: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-circle"))); + setIcon(QIcon(":scenetoolbar/brush-circle")); tooltip += mTextureBrushWindow->toolTipCircle; break; case BrushShape_Custom: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-custom"))); + setIcon(QIcon(":scenetoolbar/brush-custom")); tooltip += mTextureBrushWindow->toolTipCustom; break; } diff --git a/apps/opencs/view/widget/scenetooltoggle2.cpp b/apps/opencs/view/widget/scenetooltoggle2.cpp index 8dbd1e804c..44bffa34a6 100644 --- a/apps/opencs/view/widget/scenetooltoggle2.cpp +++ b/apps/opencs/view/widget/scenetooltoggle2.cpp @@ -88,7 +88,7 @@ void CSVWidget::SceneToolToggle2::addButton( stream << mSingleIcon << id; PushButton* button = new PushButton( - QIcon(QPixmap(stream.str().c_str())), PushButton::Type_Toggle, tooltip.isEmpty() ? name : tooltip, mPanel); + QIcon(stream.str().c_str()), PushButton::Type_Toggle, tooltip.isEmpty() ? name : tooltip, mPanel); button->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); button->setIconSize(QSize(mIconSize, mIconSize)); diff --git a/apps/opencs/view/world/dragrecordtable.cpp b/apps/opencs/view/world/dragrecordtable.cpp index 11a0c9a540..a8a3df309c 100644 --- a/apps/opencs/view/world/dragrecordtable.cpp +++ b/apps/opencs/view/world/dragrecordtable.cpp @@ -29,7 +29,7 @@ void CSVWorld::DragRecordTable::startDragFromTable(const CSVWorld::DragRecordTab mime->setIndexAtDragStart(index); QDrag* drag = new QDrag(this); drag->setMimeData(mime); - drag->setPixmap(QString::fromUtf8(mime->getIcon().c_str())); + drag->setPixmap(QIcon(mime->getIcon().c_str()).pixmap(QSize(16, 16))); drag->exec(Qt::CopyAction); } diff --git a/apps/opencs/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index 891d954ad4..b48eaec31d 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -78,8 +78,16 @@ CSVWorld::TableSubView::TableSubView( widget->setLayout(layout); setWidget(widget); + + // Widget position can be negative, we should clamp it. + QPoint position = pos(); + if (position.x() <= 0) + position.setX(0); + if (position.y() <= 0) + position.setY(0); + // prefer height of the screen and full width of the table - const QRect rect = QApplication::screenAt(pos())->geometry(); + const QRect rect = QApplication::screenAt(position)->geometry(); int frameHeight = 40; // set a reasonable default QWidget* topLevel = QApplication::topLevelAt(pos()); if (topLevel) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 63473fe67d..179dbcdc32 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -64,6 +64,7 @@ #include "mwscript/interpretercontext.hpp" #include "mwscript/scriptmanagerimp.hpp" +#include "mwsound/constants.hpp" #include "mwsound/soundmanagerimp.hpp" #include "mwworld/class.hpp" @@ -987,9 +988,8 @@ void OMW::Engine::go() // start in main menu mWindowManager->pushGuiMode(MWGui::GM_MainMenu); - std::string titlefile = "music/special/morrowind title.mp3"; - if (mVFS->exists(titlefile)) - mSoundManager->streamMusic(titlefile, MWSound::MusicType::Special); + if (mVFS->exists(MWSound::titleMusic)) + mSoundManager->streamMusic(MWSound::titleMusic, MWSound::MusicType::Special); else Log(Debug::Warning) << "Title music not found"; diff --git a/apps/openmw/mwbase/soundmanager.hpp b/apps/openmw/mwbase/soundmanager.hpp index 05b925f87d..ab3f9c5605 100644 --- a/apps/openmw/mwbase/soundmanager.hpp +++ b/apps/openmw/mwbase/soundmanager.hpp @@ -117,7 +117,7 @@ namespace MWBase virtual void stopMusic() = 0; ///< Stops music if it's playing - virtual void streamMusic(const std::string& filename, MWSound::MusicType type, float fade = 1.f) = 0; + virtual void streamMusic(VFS::Path::NormalizedView filename, MWSound::MusicType type, float fade = 1.f) = 0; ///< Play a soundifle /// \param filename name of a sound file in the data directory. /// \param type music type. @@ -126,7 +126,7 @@ namespace MWBase virtual bool isMusicPlaying() = 0; ///< Returns true if music is playing - virtual void playPlaylist(const std::string& playlist) = 0; + virtual void playPlaylist(VFS::Path::NormalizedView playlist) = 0; ///< Start playing music from the selected folder /// \param name of the folder that contains the playlist /// Title music playlist is predefined diff --git a/apps/openmw/mwgui/levelupdialog.cpp b/apps/openmw/mwgui/levelupdialog.cpp index 4950e3edf4..f1a40a3f16 100644 --- a/apps/openmw/mwgui/levelupdialog.cpp +++ b/apps/openmw/mwgui/levelupdialog.cpp @@ -22,6 +22,8 @@ #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" +#include "../mwsound/constants.hpp" + #include "class.hpp" namespace @@ -216,8 +218,7 @@ namespace MWGui center(); // Play LevelUp Music - MWBase::Environment::get().getSoundManager()->streamMusic( - "Music/Special/MW_Triumph.mp3", MWSound::MusicType::Special); + MWBase::Environment::get().getSoundManager()->streamMusic(MWSound::triumphMusic, MWSound::MusicType::Special); } void LevelupDialog::onOkButtonClicked(MyGUI::Widget* sender) diff --git a/apps/openmw/mwgui/loadingscreen.cpp b/apps/openmw/mwgui/loadingscreen.cpp index 8ba2bb8312..3a9aba7828 100644 --- a/apps/openmw/mwgui/loadingscreen.cpp +++ b/apps/openmw/mwgui/loadingscreen.cpp @@ -67,7 +67,8 @@ namespace MWGui != supported_extensions.end(); }; - for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator("Splash/")) + constexpr VFS::Path::NormalizedView splash("splash/"); + for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(splash)) { if (isSupportedExtension(Misc::getFileExtension(name))) mSplashScreens.push_back(name); diff --git a/apps/openmw/mwgui/mainmenu.cpp b/apps/openmw/mwgui/mainmenu.cpp index be3700342a..53f791fdac 100644 --- a/apps/openmw/mwgui/mainmenu.cpp +++ b/apps/openmw/mwgui/mainmenu.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include "../mwbase/environment.hpp" @@ -37,7 +38,9 @@ namespace MWGui getWidget(mVersionText, "VersionText"); mVersionText->setCaption(versionDescription); - mHasAnimatedMenu = mVFS->exists("video/menu_background.bik"); + constexpr VFS::Path::NormalizedView menuBackgroundVideo("video/menu_background.bik"); + + mHasAnimatedMenu = mVFS->exists(menuBackgroundVideo); updateMenu(); } diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 396d0b18a3..075a338592 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -402,7 +402,8 @@ namespace MWGui std::vector availableLanguages; const VFS::Manager* vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - for (const auto& path : vfs->getRecursiveDirectoryIterator("l10n/")) + constexpr VFS::Path::NormalizedView l10n("l10n/"); + for (const auto& path : vfs->getRecursiveDirectoryIterator(l10n)) { if (Misc::getFileExtension(path) == "yaml") { diff --git a/apps/openmw/mwgui/windowbase.cpp b/apps/openmw/mwgui/windowbase.cpp index eff5c98588..f5d90590f8 100644 --- a/apps/openmw/mwgui/windowbase.cpp +++ b/apps/openmw/mwgui/windowbase.cpp @@ -7,7 +7,6 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" -#include #include #include "draganddrop.hpp" @@ -80,12 +79,12 @@ void WindowBase::center() void WindowBase::clampWindowCoordinates(MyGUI::Window* window) { - auto minSize = window->getMinSize(); - minSize.height = static_cast(minSize.height * Settings::gui().mScalingFactor); - minSize.width = static_cast(minSize.width * Settings::gui().mScalingFactor); + MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + if (window->getLayer()) + viewSize = window->getLayer()->getSize(); // Window's minimum size is larger than the screen size, can not clamp coordinates - MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + auto minSize = window->getMinSize(); if (minSize.width > viewSize.width || minSize.height > viewSize.height) return; diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 61a8b3361c..3212e8f02b 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -356,7 +356,8 @@ namespace MWGui mWindows.push_back(std::move(console)); trackWindow(mConsole, makeConsoleWindowSettingValues()); - bool questList = mResourceSystem->getVFS()->exists("textures/tx_menubook_options_over.dds"); + constexpr VFS::Path::NormalizedView menubookOptionsOverTexture("textures/tx_menubook_options_over.dds"); + const bool questList = mResourceSystem->getVFS()->exists(menubookOptionsOverTexture); auto journal = JournalWindow::create(JournalViewModel::create(), questList, mEncoding); mGuiModeStates[GM_Journal] = GuiModeState(journal.get()); mWindows.push_back(std::move(journal)); diff --git a/apps/openmw/mwlua/soundbindings.cpp b/apps/openmw/mwlua/soundbindings.cpp index 57d1e606c1..a5ec69b4fc 100644 --- a/apps/openmw/mwlua/soundbindings.cpp +++ b/apps/openmw/mwlua/soundbindings.cpp @@ -141,7 +141,7 @@ namespace MWLua api["streamMusic"] = [](std::string_view fileName, const sol::optional& options) { auto args = getStreamMusicArgs(options); MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->streamMusic(std::string(fileName), MWSound::MusicType::Scripted, args.mFade); + sndMgr->streamMusic(VFS::Path::Normalized(fileName), MWSound::MusicType::Scripted, args.mFade); }; api["say"] diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index addf62df34..32f81c398e 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -39,6 +39,8 @@ #include "../mwrender/vismask.hpp" +#include "../mwsound/constants.hpp" + #include "actor.hpp" #include "actorutil.hpp" #include "aicombataction.hpp" @@ -1798,7 +1800,7 @@ namespace MWMechanics MWBase::Environment::get().getStateManager()->askLoadRecent(); // Play Death Music if it was the player dying MWBase::Environment::get().getSoundManager()->streamMusic( - "Music/Special/MW_Death.mp3", MWSound::MusicType::Special); + MWSound::deathMusic, MWSound::MusicType::Special); } else { diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 47fba46c75..048476c6ef 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -29,6 +29,8 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwsound/constants.hpp" + #include "actor.hpp" #include "actors.hpp" #include "actorutil.hpp" @@ -1678,12 +1680,12 @@ namespace MWMechanics if (mMusicType != MWSound::MusicType::Explore && !hasHostiles && !(player.getClass().getCreatureStats(player).isDead() && musicPlaying)) { - MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore")); + MWBase::Environment::get().getSoundManager()->playPlaylist(MWSound::explorePlaylist); mMusicType = MWSound::MusicType::Explore; } else if (mMusicType != MWSound::MusicType::Battle && hasHostiles) { - MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Battle")); + MWBase::Environment::get().getSoundManager()->playPlaylist(MWSound::battlePlaylist); mMusicType = MWSound::MusicType::Battle; } } diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 0baf68ed5d..31b5d36c79 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -530,6 +530,7 @@ namespace MWRender , mHasMagicEffects(false) , mAlpha(1.f) , mPlayScriptedOnly(false) + , mRequiresBoneMap(false) { for (size_t i = 0; i < sNumBlendMasks; i++) mAnimationTimePtr[i] = std::make_shared(); @@ -964,8 +965,17 @@ namespace MWRender { if (!mNodeMapCreated && mObjectRoot) { - SceneUtil::NodeMapVisitor visitor(mNodeMap); - mObjectRoot->accept(visitor); + // If the base of this animation is an osgAnimation, we should map the bones not matrix transforms + if (mRequiresBoneMap) + { + SceneUtil::NodeMapVisitorBoneOnly visitor(mNodeMap); + mObjectRoot->accept(visitor); + } + else + { + SceneUtil::NodeMapVisitor visitor(mNodeMap); + mObjectRoot->accept(visitor); + } mNodeMapCreated = true; } return mNodeMap; @@ -1479,6 +1489,10 @@ namespace MWRender mInsert->addChild(mObjectRoot); } + // osgAnimation formats with skeletons should have their nodemap be bone instances + // FIXME: better way to detect osgAnimation here instead of relying on extension? + mRequiresBoneMap = mSkeleton != nullptr && !Misc::StringUtils::ciEndsWith(model, ".nif"); + if (previousStateset) mObjectRoot->setStateSet(previousStateset); diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 22b7167a9c..4cff658011 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -246,6 +246,7 @@ namespace MWRender osg::ref_ptr mLightListCallback; bool mPlayScriptedOnly; + bool mRequiresBoneMap; const NodeMap& getNodeMap() const; diff --git a/apps/openmw/mwrender/rotatecontroller.cpp b/apps/openmw/mwrender/rotatecontroller.cpp index d7f8bb902c..61a2a2628a 100644 --- a/apps/openmw/mwrender/rotatecontroller.cpp +++ b/apps/openmw/mwrender/rotatecontroller.cpp @@ -1,6 +1,7 @@ #include "rotatecontroller.hpp" #include +#include namespace MWRender { @@ -43,6 +44,17 @@ namespace MWRender node->setMatrix(matrix); + // If we are linked to a bone we must call setMatrixInSkeletonSpace + osgAnimation::Bone* b = dynamic_cast(node); + if (b) + { + osgAnimation::Bone* parent = b->getBoneParent(); + if (parent) + matrix *= parent->getMatrixInSkeletonSpace(); + + b->setMatrixInSkeletonSpace(matrix); + } + traverse(node, nv); } diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index a28ca0b7b7..cbc560d5ac 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -715,8 +715,6 @@ namespace MWRender osg::ref_ptr normalMap( new osg::Texture2D(mResourceSystem->getImageManager()->getImage("textures/omw/water_nm.png"))); - if (normalMap->getImage()) - normalMap->getImage()->flipVertical(); normalMap->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); normalMap->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); normalMap->setMaxAnisotropy(16); diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 481a0e2ec1..cfdeb8c658 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -631,7 +631,7 @@ namespace MWScript ESM::RefId gem = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - if (!ptr.getClass().hasInventoryStore(ptr)) + if (!ptr.getClass().isActor()) return; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); @@ -664,10 +664,10 @@ namespace MWScript for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); - if (!ptr.getClass().hasInventoryStore(ptr)) + if (!ptr.getClass().isActor()) return; - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { if (it->getCellRef().getSoul() == soul) @@ -780,10 +780,10 @@ namespace MWScript ESM::RefId soul = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - if (!ptr.getClass().hasInventoryStore(ptr)) + if (!ptr.getClass().isActor()) return; - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); for (MWWorld::ContainerStoreIterator iter(store.begin()); iter != store.end(); ++iter) { diff --git a/apps/openmw/mwscript/soundextensions.cpp b/apps/openmw/mwscript/soundextensions.cpp index ee39860584..79bdf20160 100644 --- a/apps/openmw/mwscript/soundextensions.cpp +++ b/apps/openmw/mwscript/soundextensions.cpp @@ -63,7 +63,7 @@ namespace MWScript public: void execute(Interpreter::Runtime& runtime) override { - std::string music{ runtime.getStringLiteral(runtime[0].mInteger) }; + const VFS::Path::Normalized music(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); MWBase::Environment::get().getSoundManager()->streamMusic( diff --git a/apps/openmw/mwsound/constants.hpp b/apps/openmw/mwsound/constants.hpp new file mode 100644 index 0000000000..217dd1935e --- /dev/null +++ b/apps/openmw/mwsound/constants.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_APPS_OPENMW_MWSOUND_CONSTANTS_H +#define OPENMW_APPS_OPENMW_MWSOUND_CONSTANTS_H + +#include + +namespace MWSound +{ + constexpr VFS::Path::NormalizedView battlePlaylist("battle"); + constexpr VFS::Path::NormalizedView explorePlaylist("explore"); + constexpr VFS::Path::NormalizedView titleMusic("music/special/morrowind title.mp3"); + constexpr VFS::Path::NormalizedView triumphMusic("music/special/mw_triumph.mp3"); + constexpr VFS::Path::NormalizedView deathMusic("music/special/mw_death.mp3"); +} + +#endif diff --git a/apps/openmw/mwsound/ffmpeg_decoder.cpp b/apps/openmw/mwsound/ffmpeg_decoder.cpp index 9e7a2be3a8..e9a5a1eb94 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.cpp +++ b/apps/openmw/mwsound/ffmpeg_decoder.cpp @@ -208,7 +208,7 @@ namespace MWSound return dec; } - void FFmpeg_Decoder::open(const std::string& fname) + void FFmpeg_Decoder::open(VFS::Path::NormalizedView fname) { close(); mDataStream = mResourceMgr->get(fname); @@ -224,7 +224,7 @@ namespace MWSound formatCtx->pb = ioCtx.get(); // avformat_open_input frees user supplied AVFormatContext on failure - if (avformat_open_input(&formatCtx, fname.c_str(), nullptr, nullptr) != 0) + if (avformat_open_input(&formatCtx, fname.value().data(), nullptr, nullptr) != 0) throw std::runtime_error("Failed to open input"); AVFormatContextPtr formatCtxPtr(std::exchange(formatCtx, nullptr)); diff --git a/apps/openmw/mwsound/ffmpeg_decoder.hpp b/apps/openmw/mwsound/ffmpeg_decoder.hpp index ed3297403e..264ff8fab7 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.hpp +++ b/apps/openmw/mwsound/ffmpeg_decoder.hpp @@ -93,7 +93,7 @@ namespace MWSound bool getAVAudioData(); size_t readAVAudioData(void* data, size_t length); - void open(const std::string& fname) override; + void open(VFS::Path::NormalizedView fname) override; void close() override; std::string getName() override; diff --git a/apps/openmw/mwsound/movieaudiofactory.cpp b/apps/openmw/mwsound/movieaudiofactory.cpp index 1bb5275c45..68ea221321 100644 --- a/apps/openmw/mwsound/movieaudiofactory.cpp +++ b/apps/openmw/mwsound/movieaudiofactory.cpp @@ -24,8 +24,10 @@ namespace MWSound private: MWSound::MovieAudioDecoder* mDecoder; - void open(const std::string& fname) override; - void close() override; + void open(VFS::Path::NormalizedView fname) override { throw std::runtime_error("Method not implemented"); } + + void close() override {} + std::string getName() override; void getInfo(int* samplerate, ChannelConfig* chans, SampleType* type) override; size_t read(char* buffer, size_t bytes) override; @@ -92,12 +94,6 @@ namespace MWSound std::shared_ptr mDecoderBridge; }; - void MWSoundDecoderBridge::open(const std::string& fname) - { - throw std::runtime_error("Method not implemented"); - } - void MWSoundDecoderBridge::close() {} - std::string MWSoundDecoderBridge::getName() { return mDecoder->getStreamName(); diff --git a/apps/openmw/mwsound/sound_decoder.hpp b/apps/openmw/mwsound/sound_decoder.hpp index f6dcdb7032..17f9d28909 100644 --- a/apps/openmw/mwsound/sound_decoder.hpp +++ b/apps/openmw/mwsound/sound_decoder.hpp @@ -1,6 +1,8 @@ #ifndef GAME_SOUND_SOUND_DECODER_H #define GAME_SOUND_SOUND_DECODER_H +#include + #include #include @@ -36,7 +38,7 @@ namespace MWSound { const VFS::Manager* mResourceMgr; - virtual void open(const std::string& fname) = 0; + virtual void open(VFS::Path::NormalizedView fname) = 0; virtual void close() = 0; virtual std::string getName() = 0; diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index 56224b4dcb..bcaec8ddfd 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -26,14 +26,14 @@ #include "../mwmechanics/actorutil.hpp" +#include "constants.hpp" +#include "ffmpeg_decoder.hpp" +#include "openal_output.hpp" #include "sound.hpp" #include "sound_buffer.hpp" #include "sound_decoder.hpp" #include "sound_output.hpp" -#include "ffmpeg_decoder.hpp" -#include "openal_output.hpp" - namespace MWSound { namespace @@ -252,13 +252,13 @@ namespace MWSound } } - void SoundManager::streamMusicFull(const std::string& filename) + void SoundManager::streamMusicFull(VFS::Path::NormalizedView filename) { if (!mOutput->isInitialized()) return; stopMusic(); - if (filename.empty()) + if (filename.value().empty()) return; Log(Debug::Info) << "Playing \"" << filename << "\""; @@ -269,7 +269,7 @@ namespace MWSound { decoder->open(filename); } - catch (std::exception& e) + catch (const std::exception& e) { Log(Debug::Error) << "Failed to load audio from \"" << filename << "\": " << e.what(); return; @@ -285,7 +285,7 @@ namespace MWSound mOutput->streamSound(std::move(decoder), mMusic.get()); } - void SoundManager::advanceMusic(const std::string& filename, float fadeOut) + void SoundManager::advanceMusic(VFS::Path::NormalizedView filename, float fadeOut) { if (!isMusicPlaying()) { @@ -300,13 +300,16 @@ namespace MWSound void SoundManager::startRandomTitle() { - const std::vector& filelist = mMusicFiles[mCurrentPlaylist]; - if (filelist.empty()) + const auto playlist = mMusicFiles.find(mCurrentPlaylist); + + if (playlist == mMusicFiles.end() || playlist->second.empty()) { - advanceMusic(std::string()); + advanceMusic(VFS::Path::NormalizedView()); return; } + const std::vector& filelist = playlist->second; + auto& tracklist = mMusicToPlay[mCurrentPlaylist]; // Do a Fisher-Yates shuffle @@ -335,7 +338,7 @@ namespace MWSound return mMusic && mOutput->isStreamPlaying(mMusic.get()); } - void SoundManager::streamMusic(const std::string& filename, MusicType type, float fade) + void SoundManager::streamMusic(VFS::Path::NormalizedView filename, MusicType type, float fade) { const auto mechanicsManager = MWBase::Environment::get().getMechanicsManager(); @@ -344,35 +347,36 @@ namespace MWSound && type != MusicType::Special) return; - std::string normalizedName = VFS::Path::normalizeFilename(filename); - mechanicsManager->setMusicType(type); - advanceMusic(normalizedName, fade); + advanceMusic(filename, fade); if (type == MWSound::MusicType::Battle) - mCurrentPlaylist = "Battle"; + mCurrentPlaylist = battlePlaylist; else if (type == MWSound::MusicType::Explore) - mCurrentPlaylist = "Explore"; + mCurrentPlaylist = explorePlaylist; } - void SoundManager::playPlaylist(const std::string& playlist) + void SoundManager::playPlaylist(VFS::Path::NormalizedView playlist) { if (mCurrentPlaylist == playlist) return; - if (mMusicFiles.find(playlist) == mMusicFiles.end()) + auto it = mMusicFiles.find(playlist); + + if (it == mMusicFiles.end()) { - std::vector filelist; - auto playlistPath = Misc::ResourceHelpers::correctMusicPath(playlist) + '/'; - for (const auto& name : mVFS->getRecursiveDirectoryIterator(playlistPath)) + std::vector filelist; + const VFS::Path::Normalized playlistPath + = Misc::ResourceHelpers::correctMusicPath(playlist) / VFS::Path::NormalizedView(); + for (const auto& name : mVFS->getRecursiveDirectoryIterator(VFS::Path::NormalizedView(playlistPath))) filelist.push_back(name); - mMusicFiles[playlist] = std::move(filelist); + it = mMusicFiles.emplace_hint(it, playlist, std::move(filelist)); } // No Battle music? Use Explore playlist - if (playlist == "Battle" && mMusicFiles[playlist].empty()) + if (playlist == battlePlaylist && it->second.empty()) { - playPlaylist("Explore"); + playPlaylist(explorePlaylist); return; } @@ -1019,7 +1023,7 @@ namespace MWSound mTimePassed = 0.0f; // Make sure music is still playing - if (!isMusicPlaying() && !mCurrentPlaylist.empty()) + if (!isMusicPlaying() && !mCurrentPlaylist.value().empty()) startRandomTitle(); Environment env = Env_Normal; @@ -1137,10 +1141,10 @@ namespace MWSound if (!mMusic || !mMusic->updateFade(duration) || !mOutput->isStreamPlaying(mMusic.get())) { stopMusic(); - if (!mNextMusic.empty()) + if (!mNextMusic.value().empty()) { streamMusicFull(mNextMusic); - mNextMusic.clear(); + mNextMusic = VFS::Path::Normalized(); } } else @@ -1160,9 +1164,8 @@ namespace MWSound if (isMainMenu && !isMusicPlaying()) { - std::string titlefile = "music/special/morrowind title.mp3"; - if (mVFS->exists(titlefile)) - streamMusic(titlefile, MWSound::MusicType::Special); + if (mVFS->exists(MWSound::titleMusic)) + streamMusic(MWSound::titleMusic, MWSound::MusicType::Special); } updateSounds(duration); diff --git a/apps/openmw/mwsound/soundmanagerimp.hpp b/apps/openmw/mwsound/soundmanagerimp.hpp index 75b1193118..1ba80f1d73 100644 --- a/apps/openmw/mwsound/soundmanagerimp.hpp +++ b/apps/openmw/mwsound/soundmanagerimp.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include "../mwbase/soundmanager.hpp" @@ -52,9 +53,10 @@ namespace MWSound std::unique_ptr mOutput; // Caches available music tracks by - std::unordered_map> mMusicFiles; + std::unordered_map, VFS::Path::Hash, std::equal_to<>> + mMusicFiles; std::unordered_map> mMusicToPlay; // A list with music files not yet played - std::string mLastPlayedMusic; // The music file that was last played + VFS::Path::Normalized mLastPlayedMusic; // The music file that was last played WaterSoundUpdater mWaterSoundUpdater; @@ -90,7 +92,7 @@ namespace MWSound TrackList mActiveTracks; StreamPtr mMusic; - std::string mCurrentPlaylist; + VFS::Path::Normalized mCurrentPlaylist; bool mListenerUnderwater; osg::Vec3f mListenerPos; @@ -102,7 +104,7 @@ namespace MWSound Sound* mUnderwaterSound; Sound* mNearWaterSound; - std::string mNextMusic; + VFS::Path::Normalized mNextMusic; bool mPlaybackPaused; RegionSoundSelector mRegionSoundSelector; @@ -123,8 +125,8 @@ namespace MWSound StreamPtr playVoice(DecoderPtr decoder, const osg::Vec3f& pos, bool playlocal); - void streamMusicFull(const std::string& filename); - void advanceMusic(const std::string& filename, float fadeOut = 1.f); + void streamMusicFull(VFS::Path::NormalizedView filename); + void advanceMusic(VFS::Path::NormalizedView filename, float fadeOut = 1.f); void startRandomTitle(); void cull3DSound(SoundBase* sound); @@ -174,7 +176,7 @@ namespace MWSound void stopMusic() override; ///< Stops music if it's playing - void streamMusic(const std::string& filename, MWSound::MusicType type, float fade = 1.f) override; + void streamMusic(VFS::Path::NormalizedView filename, MWSound::MusicType type, float fade = 1.f) override; ///< Play a soundifle /// \param filename name of a sound file in the data directory. /// \param type music type. @@ -183,7 +185,7 @@ namespace MWSound bool isMusicPlaying() override; ///< Returns true if music is playing - void playPlaylist(const std::string& playlist) override; + void playPlaylist(VFS::Path::NormalizedView playlist) override; ///< Start playing music from the selected folder /// \param name of the folder that contains the playlist /// Title music playlist is predefined diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 64a258cff8..beb519b9e6 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -190,7 +190,8 @@ namespace { const DetourNavigator::AgentBounds agentBounds = world.getPathfindingAgentBounds(ptr); if (!navigator.addAgent(agentBounds)) - Log(Debug::Warning) << "Agent bounds are not supported by navigator: " << agentBounds; + Log(Debug::Warning) << "Agent bounds are not supported by navigator for " << ptr.toString() << ": " + << agentBounds; } } diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 74335a1534..c72a9993d6 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -90,6 +90,8 @@ #include "../mwphysics/object.hpp" #include "../mwphysics/physicssystem.hpp" +#include "../mwsound/constants.hpp" + #include "actionteleport.hpp" #include "cellstore.hpp" #include "containerstore.hpp" @@ -394,7 +396,7 @@ namespace MWWorld { // Make sure that we do not continue to play a Title music after a new game video. MWBase::Environment::get().getSoundManager()->stopMusic(); - MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore")); + MWBase::Environment::get().getSoundManager()->playPlaylist(MWSound::explorePlaylist); MWBase::Environment::get().getWindowManager()->playVideo(video, true); } } diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index b91940cd77..db6ecb816a 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -100,6 +100,8 @@ file(GLOB UNITTEST_SRC_FILES resource/testobjectcache.cpp vfs/testpathutil.cpp + + sceneutil/osgacontroller.cpp ) source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) diff --git a/apps/openmw_test_suite/misc/test_resourcehelpers.cpp b/apps/openmw_test_suite/misc/test_resourcehelpers.cpp index 5290630394..48edb72578 100644 --- a/apps/openmw_test_suite/misc/test_resourcehelpers.cpp +++ b/apps/openmw_test_suite/misc/test_resourcehelpers.cpp @@ -8,19 +8,22 @@ namespace TEST(CorrectSoundPath, wav_files_not_overridden_with_mp3_in_vfs_are_not_corrected) { std::unique_ptr mVFS = TestingOpenMW::createTestVFS({ { "sound/bar.wav", nullptr } }); - EXPECT_EQ(correctSoundPath("sound/bar.wav", *mVFS), "sound/bar.wav"); + constexpr VFS::Path::NormalizedView path("sound/bar.wav"); + EXPECT_EQ(correctSoundPath(path, *mVFS), "sound/bar.wav"); } TEST(CorrectSoundPath, wav_files_overridden_with_mp3_in_vfs_are_corrected) { std::unique_ptr mVFS = TestingOpenMW::createTestVFS({ { "sound/foo.mp3", nullptr } }); - EXPECT_EQ(correctSoundPath("sound/foo.wav", *mVFS), "sound/foo.mp3"); + constexpr VFS::Path::NormalizedView path("sound/foo.wav"); + EXPECT_EQ(correctSoundPath(path, *mVFS), "sound/foo.mp3"); } TEST(CorrectSoundPath, corrected_path_does_not_check_existence_in_vfs) { std::unique_ptr mVFS = TestingOpenMW::createTestVFS({}); - EXPECT_EQ(correctSoundPath("sound/foo.wav", *mVFS), "sound/foo.mp3"); + constexpr VFS::Path::NormalizedView path("sound/foo.wav"); + EXPECT_EQ(correctSoundPath(path, *mVFS), "sound/foo.mp3"); } namespace diff --git a/apps/openmw_test_suite/nifosg/testnifloader.cpp b/apps/openmw_test_suite/nifosg/testnifloader.cpp index f05d651301..fa023fff0d 100644 --- a/apps/openmw_test_suite/nifosg/testnifloader.cpp +++ b/apps/openmw_test_suite/nifosg/testnifloader.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ namespace { VFS::Manager mVfs; Resource::ImageManager mImageManager{ &mVfs, 0 }; + Resource::BgsmFileManager mMaterialManager{ &mVfs, 0 }; const osgDB::ReaderWriter* mReaderWriter = osgDB::Registry::instance()->getReaderWriterForExtension("osgt"); osg::ref_ptr mOptions = new osgDB::Options; @@ -70,7 +72,7 @@ namespace init(node); Nif::NIFFile file("test.nif"); file.mRoots.push_back(&node); - auto result = Loader::load(file, &mImageManager); + auto result = Loader::load(file, &mImageManager, &mMaterialManager); EXPECT_EQ(serialize(*result), R"( osg::Group { UniqueID 1 @@ -259,7 +261,7 @@ osg::Group { node.mProperties.push_back(Nif::RecordPtrT(&property)); Nif::NIFFile file("test.nif"); file.mRoots.push_back(&node); - auto result = Loader::load(file, &mImageManager); + auto result = Loader::load(file, &mImageManager, &mMaterialManager); EXPECT_EQ(serialize(*result), formatOsgNodeForBSShaderProperty(GetParam().mExpectedShaderPrefix)); } @@ -289,7 +291,7 @@ osg::Group { node.mProperties.push_back(Nif::RecordPtrT(&property)); Nif::NIFFile file("test.nif"); file.mRoots.push_back(&node); - auto result = Loader::load(file, &mImageManager); + auto result = Loader::load(file, &mImageManager, &mMaterialManager); EXPECT_EQ(serialize(*result), formatOsgNodeForBSLightingShaderProperty(GetParam().mExpectedShaderPrefix)); } diff --git a/apps/openmw_test_suite/sceneutil/osgacontroller.cpp b/apps/openmw_test_suite/sceneutil/osgacontroller.cpp new file mode 100644 index 0000000000..309de4a878 --- /dev/null +++ b/apps/openmw_test_suite/sceneutil/osgacontroller.cpp @@ -0,0 +1,131 @@ +#include + +#include +#include + +#include +#include + +namespace +{ + using namespace SceneUtil; + + static const std::string ROOT_BONE_NAME = "bip01"; + + // Creates a merged anim track with a single root channel with two start/end matrix transforms + osg::ref_ptr createMergedAnimationTrack(std::string name, osg::Matrixf startTransform, + osg::Matrixf endTransform, float startTime = 0.0f, float endTime = 1.0f) + { + osg::ref_ptr mergedAnimationTrack = new Resource::Animation; + mergedAnimationTrack->setName(name); + + osgAnimation::MatrixKeyframeContainer* cbCntr = new osgAnimation::MatrixKeyframeContainer; + cbCntr->push_back(osgAnimation::MatrixKeyframe(startTime, startTransform)); + cbCntr->push_back(osgAnimation::MatrixKeyframe(endTime, endTransform)); + + osg::ref_ptr rootChannel = new osgAnimation::MatrixLinearChannel; + rootChannel->setName("transform"); + rootChannel->setTargetName(ROOT_BONE_NAME); + rootChannel->getOrCreateSampler()->setKeyframeContainer(cbCntr); + mergedAnimationTrack->addChannel(rootChannel); + return mergedAnimationTrack; + } + + TEST(OsgAnimationControllerTest, getTranslationShouldReturnSampledChannelTranslationForBip01) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); // should sample this + emulatedAnimations.push_back({ 1.1f, 2.0f, "test2" }); // should ignore this + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + osg::Matrixf startTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform2 = osg::Matrixf::identity(); + endTransform.setTrans(1.0f, 1.0f, 1.0f); + controller.addMergedAnimationTrack(createMergedAnimationTrack("test1", startTransform, endTransform)); + endTransform2.setTrans(2.0f, 2.0f, 2.0f); + controller.addMergedAnimationTrack( + createMergedAnimationTrack("test2", endTransform, endTransform2, 0.1f, 0.9f)); + + // should be halfway between 0,0,0 and 1,1,1 + osg::Vec3f translation = controller.getTranslation(0.5f); + EXPECT_EQ(translation, osg::Vec3f(0.5f, 0.5f, 0.5f)); + } + + TEST(OsgAnimationControllerTest, getTranslationShouldReturnZeroVectorIfNotFound) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + osg::Matrixf startTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform = osg::Matrixf::identity(); + endTransform.setTrans(1.0f, 1.0f, 1.0f); + controller.addMergedAnimationTrack(createMergedAnimationTrack("test1", startTransform, endTransform)); + + // Has no emulated animation at time so will return 0,0,0 + osg::Vec3f translation = controller.getTranslation(100.0f); + EXPECT_EQ(translation, osg::Vec3f(0.0f, 0.0f, 0.0f)); + } + + TEST(OsgAnimationControllerTest, getTranslationShouldReturnZeroVectorIfNoMergedTracks) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + // Has no merged tracks so will return 0,0,0 + osg::Vec3f translation = controller.getTranslation(0.5); + EXPECT_EQ(translation, osg::Vec3f(0.0f, 0.0f, 0.0f)); + } + + TEST(OsgAnimationControllerTest, getTransformShouldReturnIdentityIfNotFound) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + osg::Matrixf startTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform = osg::Matrixf::identity(); + endTransform.setTrans(1.0f, 1.0f, 1.0f); + controller.addMergedAnimationTrack(createMergedAnimationTrack("test1", startTransform, endTransform)); + + // Has no emulated animation at time so will return identity + EXPECT_EQ(controller.getTransformForNode(100.0f, ROOT_BONE_NAME), osg::Matrixf::identity()); + + // Has no bone animation at time so will return identity + EXPECT_EQ(controller.getTransformForNode(0.5f, "wrongbone"), osg::Matrixf::identity()); + } + + TEST(OsgAnimationControllerTest, getTransformShouldReturnSampledAnimMatrixAtTime) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); // should sample this + emulatedAnimations.push_back({ 1.1f, 2.0f, "test2" }); // should ignore this + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + osg::Matrixf startTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform = osg::Matrixf::identity(); + endTransform.setTrans(1.0f, 1.0f, 1.0f); + controller.addMergedAnimationTrack(createMergedAnimationTrack("test1", startTransform, endTransform)); + osg::Matrixf endTransform2 = osg::Matrixf::identity(); + endTransform2.setTrans(2.0f, 2.0f, 2.0f); + controller.addMergedAnimationTrack( + createMergedAnimationTrack("test2", endTransform, endTransform2, 0.1f, 0.9f)); + + EXPECT_EQ(controller.getTransformForNode(0.0f, ROOT_BONE_NAME), startTransform); // start of test1 + EXPECT_EQ(controller.getTransformForNode(1.0f, ROOT_BONE_NAME), endTransform); // end of test1 + EXPECT_EQ(controller.getTransformForNode(1.1f, ROOT_BONE_NAME), endTransform); // start of test2 + EXPECT_EQ(controller.getTransformForNode(2.0f, ROOT_BONE_NAME), endTransform2); // end of test2 + } +} diff --git a/apps/openmw_test_suite/vfs/testpathutil.cpp b/apps/openmw_test_suite/vfs/testpathutil.cpp index 6eb84f97d5..3819f9905a 100644 --- a/apps/openmw_test_suite/vfs/testpathutil.cpp +++ b/apps/openmw_test_suite/vfs/testpathutil.cpp @@ -39,7 +39,7 @@ namespace VFS::Path TEST(NormalizedTest, shouldSupportConstructorFromNormalizedView) { - const NormalizedView view = "foo/bar/baz"; + const NormalizedView view("foo/bar/baz"); const Normalized value(view); EXPECT_EQ(value.view(), "foo/bar/baz"); } diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index 3a17210cd3..444290f3a6 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -85,7 +85,7 @@ target_link_libraries(openmw-wizard components_qt ) -target_link_libraries(openmw-wizard Qt::Widgets Qt::Core) +target_link_libraries(openmw-wizard Qt::Widgets Qt::Core Qt::Svg) if (OPENMW_USE_UNSHIELD) target_link_libraries(openmw-wizard ${LIBUNSHIELD_LIBRARIES}) diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp index dc94d2d002..1da28b1237 100644 --- a/apps/wizard/installationtargetpage.cpp +++ b/apps/wizard/installationtargetpage.cpp @@ -19,6 +19,8 @@ Wizard::InstallationTargetPage::InstallationTargetPage(QWidget* parent, const Fi setupUi(this); + folderIconLabel->setPixmap(QIcon(":folder").pixmap(QSize(48, 48))); + registerField(QLatin1String("installation.path*"), targetLineEdit); } diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp index 7dcf642dd6..7d4c2184ee 100644 --- a/apps/wizard/languageselectionpage.cpp +++ b/apps/wizard/languageselectionpage.cpp @@ -9,6 +9,8 @@ Wizard::LanguageSelectionPage::LanguageSelectionPage(QWidget* parent) setupUi(this); + flagIconLabel->setPixmap(QIcon(":preferences-desktop-locale").pixmap(QSize(48, 48))); + registerField(QLatin1String("installation.language"), languageComboBox, "currentData", "currentDataChanged"); } diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp index b3a1c73635..2ff7db5487 100644 --- a/apps/wizard/methodselectionpage.cpp +++ b/apps/wizard/methodselectionpage.cpp @@ -11,6 +11,10 @@ Wizard::MethodSelectionPage::MethodSelectionPage(QWidget* parent) setupUi(this); + installerIconLabel->setPixmap(QIcon(":system-installer").pixmap(QSize(48, 48))); + folderIconLabel->setPixmap(QIcon(":folder").pixmap(QSize(48, 48))); + buyIconLabel->setPixmap(QIcon(":dollar").pixmap(QSize(48, 48))); + #ifndef OPENMW_USE_UNSHIELD retailDiscRadioButton->setEnabled(false); existingLocationRadioButton->setChecked(true); diff --git a/apps/wizard/ui/installationtargetpage.ui b/apps/wizard/ui/installationtargetpage.ui index c87214d8d9..4b1f4238d4 100644 --- a/apps/wizard/ui/installationtargetpage.ui +++ b/apps/wizard/ui/installationtargetpage.ui @@ -30,9 +30,6 @@ 0 - - <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> - Qt::RichText diff --git a/apps/wizard/ui/languageselectionpage.ui b/apps/wizard/ui/languageselectionpage.ui index fccd2aa424..3cff2c5b44 100644 --- a/apps/wizard/ui/languageselectionpage.ui +++ b/apps/wizard/ui/languageselectionpage.ui @@ -30,9 +30,6 @@ 0 - - <html><head/><body><p><img src=":/icons/tango/48x48/preferences-desktop-locale.png"/></p></body></html> - Qt::RichText diff --git a/apps/wizard/ui/methodselectionpage.ui b/apps/wizard/ui/methodselectionpage.ui index c2dd260527..28755ad438 100644 --- a/apps/wizard/ui/methodselectionpage.ui +++ b/apps/wizard/ui/methodselectionpage.ui @@ -59,9 +59,6 @@ 0 - - <html><head/><body><p><img src=":/icons/tango/48x48/system-installer.png"/></p></body></html> - Qt::RichText @@ -124,9 +121,6 @@ 0 - - <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> - Qt::RichText @@ -191,9 +185,6 @@ 0 - - <html><head/><body><p><img src=":/icons/tango/48x48/dollar.png"/></p></body></html> - diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 68411be2fc..1235072de5 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -107,6 +107,10 @@ add_component_dir (settings windowmode ) +add_component_dir (bgsm + stream file + ) + add_component_dir (bsa bsa_file compressedbsafile ba2gnrlfile ba2dx10file ba2file memorystream ) @@ -125,7 +129,7 @@ add_component_dir (vfs add_component_dir (resource scenemanager keyframemanager imagemanager bulletshapemanager bulletshape niffilemanager objectcache multiobjectcache resourcesystem - resourcemanager stats animation foreachbulletobject errormarker cachestats + resourcemanager stats animation foreachbulletobject errormarker cachestats bgsmfilemanager ) add_component_dir (shader @@ -511,7 +515,7 @@ set (ESM_UI ${CMAKE_CURRENT_SOURCE_DIR}/contentselector/contentselector.ui if (USE_QT) add_component_qt_dir (contentselector model/modelitem model/esmfile - model/naturalsort model/contentmodel + model/contentmodel model/loadordererror view/combobox view/contentselector ) diff --git a/components/bgsm/file.cpp b/components/bgsm/file.cpp new file mode 100644 index 0000000000..2aaacaf25c --- /dev/null +++ b/components/bgsm/file.cpp @@ -0,0 +1,236 @@ +#include "file.hpp" + +#include +#include + +#include "stream.hpp" + +namespace Bgsm +{ + MaterialFilePtr parse(Files::IStreamPtr&& inputStream) + { + std::shared_ptr file; + BGSMStream stream(std::move(inputStream)); + + std::array signature; + stream.readArray(signature); + std::string shaderType(signature.data(), 4); + if (shaderType == "BGEM") + { + file = std::make_shared(); + file->mShaderType = Bgsm::ShaderType::Effect; + } + else if (shaderType == "BGSM") + { + file = std::make_shared(); + file->mShaderType = Bgsm::ShaderType::Lighting; + } + else + throw std::runtime_error("Invalid material file"); + + file->read(stream); + return file; + } + + void MaterialFile::read(BGSMStream& stream) + { + stream.read(mVersion); + stream.read(mClamp); + stream.read(mUVOffset); + stream.read(mUVScale); + stream.read(mTransparency); + stream.read(mAlphaBlend); + stream.read(mSourceBlendMode); + stream.read(mDestinationBlendMode); + stream.read(mAlphaTestThreshold); + stream.read(mAlphaTest); + stream.read(mDepthWrite); + stream.read(mDepthTest); + stream.read(mSSR); + stream.read(mWetnessControlSSR); + stream.read(mDecal); + stream.read(mTwoSided); + stream.read(mDecalNoFade); + stream.read(mNonOccluder); + stream.read(mRefraction); + stream.read(mRefractionFalloff); + stream.read(mRefractionPower); + if (mVersion < 10) + { + stream.read(mEnvMapEnabled); + stream.read(mEnvMapMaskScale); + } + else + { + stream.read(mDepthBias); + } + stream.read(mGrayscaleToPaletteColor); + if (mVersion >= 6) + stream.read(mMaskWrites); + } + + void BGSMFile::read(BGSMStream& stream) + { + MaterialFile::read(stream); + + stream.read(mDiffuseMap); + stream.read(mNormalMap); + stream.read(mSmoothSpecMap); + stream.read(mGrayscaleMap); + if (mVersion >= 3) + { + stream.read(mGlowMap); + stream.read(mWrinkleMap); + stream.read(mSpecularMap); + stream.read(mLightingMap); + stream.read(mFlowMap); + if (mVersion >= 17) + stream.read(mDistanceFieldAlphaMap); + } + else + { + stream.read(mEnvMap); + stream.read(mGlowMap); + stream.read(mInnerLayerMap); + stream.read(mWrinkleMap); + stream.read(mDisplacementMap); + } + stream.read(mEnableEditorAlphaThreshold); + if (mVersion >= 8) + { + stream.read(mTranslucency); + stream.read(mTranslucencyThickObject); + stream.read(mTranslucencyMixAlbedoWithSubsurfaceColor); + stream.read(mTranslucencySubsurfaceColor); + stream.read(mTranslucencyTransmissiveScale); + stream.read(mTranslucencyTurbulence); + } + else + { + stream.read(mRimLighting); + stream.read(mRimPower); + stream.read(mBackLightPower); + stream.read(mSubsurfaceLighting); + stream.read(mSubsurfaceLightingRolloff); + } + stream.read(mSpecularEnabled); + stream.read(mSpecularColor); + stream.read(mSpecularMult); + stream.read(mSmoothness); + stream.read(mFresnelPower); + stream.read(mWetnessControlSpecScale); + stream.read(mWetnessControlSpecPowerScale); + stream.read(mWetnessControlSpecMinvar); + if (mVersion < 10) + stream.read(mWetnessControlEnvMapScale); + stream.read(mWetnessControlFresnelPower); + stream.read(mWetnessControlMetalness); + if (mVersion >= 3) + { + stream.read(mPBR); + if (mVersion >= 9) + { + stream.read(mCustomPorosity); + stream.read(mPorosityValue); + } + } + stream.read(mRootMaterialPath); + stream.read(mAnisoLighting); + stream.read(mEmitEnabled); + if (mEmitEnabled) + stream.read(mEmittanceColor); + stream.read(mEmittanceMult); + stream.read(mModelSpaceNormals); + stream.read(mExternalEmittance); + if (mVersion >= 12) + { + stream.read(mLumEmittance); + if (mVersion >= 13) + { + stream.read(mUseAdaptiveEmissive); + stream.read(mAdaptiveEmissiveExposureParams); + } + } + else if (mVersion < 8) + { + stream.read(mBackLighting); + } + stream.read(mReceiveShadows); + stream.read(mHideSecret); + stream.read(mCastShadows); + stream.read(mDissolveFade); + stream.read(mAssumeShadowmask); + stream.read(mGlowMapEnabled); + if (mVersion < 7) + { + stream.read(mEnvMapWindow); + stream.read(mEnvMapEye); + } + stream.read(mHair); + stream.read(mHairTintColor); + stream.read(mTree); + stream.read(mFacegen); + stream.read(mSkinTint); + stream.read(mTessellate); + if (mVersion < 3) + { + stream.read(mDisplacementMapParams); + stream.read(mTessellationParams); + } + stream.read(mGrayscaleToPaletteScale); + if (mVersion >= 1) + { + stream.read(mSkewSpecularAlpha); + stream.read(mTerrain); + if (mTerrain) + { + if (mVersion == 3) + stream.skip(4); // Unknown + + stream.read(mTerrainParams); + } + } + } + + void BGEMFile::read(BGSMStream& stream) + { + MaterialFile::read(stream); + + stream.read(mBaseMap); + stream.read(mGrayscaleMap); + stream.read(mEnvMap); + stream.read(mNormalMap); + stream.read(mEnvMapMask); + if (mVersion >= 11) + { + stream.read(mSpecularMap); + stream.read(mLightingMap); + stream.read(mGlowMap); + } + if (mVersion >= 10) + { + stream.read(mEnvMapEnabled); + stream.read(mEnvMapMaskScale); + } + stream.read(mBlood); + stream.read(mEffectLighting); + stream.read(mFalloff); + stream.read(mFalloffColor); + stream.read(mGrayscaleToPaletteAlpha); + stream.read(mSoft); + stream.read(mBaseColor); + stream.read(mBaseColorScale); + stream.read(mFalloffParams); + stream.read(mLightingInfluence); + stream.read(mEnvmapMinLOD); + stream.read(mSoftDepth); + if (mVersion >= 11) + stream.read(mEmittanceColor); + if (mVersion >= 15) + stream.read(mAdaptiveEmissiveExposureParams); + if (mVersion >= 16) + stream.read(mGlowMapEnabled); + if (mVersion >= 20) + stream.read(mEffectPbrSpecular); + } +} diff --git a/components/bgsm/file.hpp b/components/bgsm/file.hpp new file mode 100644 index 0000000000..b409752d74 --- /dev/null +++ b/components/bgsm/file.hpp @@ -0,0 +1,167 @@ +#ifndef OPENMW_COMPONENTS_BGSM_FILE_HPP +#define OPENMW_COMPONENTS_BGSM_FILE_HPP + +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace Bgsm +{ + class BGSMStream; + + enum class ShaderType + { + Lighting, + Effect, + }; + + struct MaterialFile + { + ShaderType mShaderType; + std::uint32_t mVersion; + std::uint32_t mClamp; + osg::Vec2f mUVOffset, mUVScale; + float mTransparency; + bool mAlphaBlend; + std::uint32_t mSourceBlendMode; + std::uint32_t mDestinationBlendMode; + std::uint8_t mAlphaTestThreshold; + bool mAlphaTest; + bool mDepthWrite, mDepthTest; + bool mSSR; + bool mWetnessControlSSR; + bool mDecal; + bool mTwoSided; + bool mDecalNoFade; + bool mNonOccluder; + bool mRefraction; + bool mRefractionFalloff; + float mRefractionPower; + bool mEnvMapEnabled; + float mEnvMapMaskScale; + bool mDepthBias; + bool mGrayscaleToPaletteColor; + std::uint8_t mMaskWrites; + + MaterialFile() = default; + virtual void read(BGSMStream& stream); + virtual ~MaterialFile() = default; + }; + + struct BGSMFile : MaterialFile + { + std::string mDiffuseMap; + std::string mNormalMap; + std::string mSmoothSpecMap; + std::string mGrayscaleMap; + std::string mGlowMap; + std::string mWrinkleMap; + std::string mSpecularMap; + std::string mLightingMap; + std::string mFlowMap; + std::string mDistanceFieldAlphaMap; + std::string mEnvMap; + std::string mInnerLayerMap; + std::string mDisplacementMap; + bool mEnableEditorAlphaThreshold; + bool mTranslucency; + bool mTranslucencyThickObject; + bool mTranslucencyMixAlbedoWithSubsurfaceColor; + osg::Vec3f mTranslucencySubsurfaceColor; + float mTranslucencyTransmissiveScale; + float mTranslucencyTurbulence; + bool mRimLighting; + float mRimPower; + float mBackLightPower; + bool mSubsurfaceLighting; + float mSubsurfaceLightingRolloff; + bool mSpecularEnabled; + osg::Vec3f mSpecularColor; + float mSpecularMult; + float mSmoothness; + float mFresnelPower; + float mWetnessControlSpecScale; + float mWetnessControlSpecPowerScale; + float mWetnessControlSpecMinvar; + float mWetnessControlEnvMapScale; + float mWetnessControlFresnelPower; + float mWetnessControlMetalness; + bool mPBR; + bool mCustomPorosity; + float mPorosityValue; + std::string mRootMaterialPath; + bool mAnisoLighting; + bool mEmitEnabled; + osg::Vec3f mEmittanceColor; + float mEmittanceMult; + bool mModelSpaceNormals; + bool mExternalEmittance; + float mLumEmittance; + bool mUseAdaptiveEmissive; + osg::Vec3f mAdaptiveEmissiveExposureParams; + bool mBackLighting; + bool mReceiveShadows; + bool mHideSecret; + bool mCastShadows; + bool mDissolveFade; + bool mAssumeShadowmask; + bool mGlowMapEnabled; + bool mEnvMapWindow; + bool mEnvMapEye; + bool mHair; + osg::Vec3f mHairTintColor; + bool mTree; + bool mFacegen; + bool mSkinTint; + bool mTessellate; + osg::Vec2f mDisplacementMapParams; + osg::Vec3f mTessellationParams; + float mGrayscaleToPaletteScale; + bool mSkewSpecularAlpha; + bool mTerrain; + osg::Vec3f mTerrainParams; + + void read(BGSMStream& stream) override; + }; + + struct BGEMFile : MaterialFile + { + std::string mBaseMap; + std::string mGrayscaleMap; + std::string mEnvMap; + std::string mNormalMap; + std::string mEnvMapMask; + std::string mSpecularMap; + std::string mLightingMap; + std::string mGlowMap; + bool mBlood; + bool mEffectLighting; + bool mFalloff; + bool mFalloffColor; + bool mGrayscaleToPaletteAlpha; + bool mSoft; + osg::Vec3f mBaseColor; + float mBaseColorScale; + osg::Vec4f mFalloffParams; + float mLightingInfluence; + std::uint8_t mEnvmapMinLOD; + float mSoftDepth; + osg::Vec3f mEmittanceColor; + osg::Vec3f mAdaptiveEmissiveExposureParams; + bool mGlowMapEnabled; + bool mEffectPbrSpecular; + + void read(BGSMStream& stream) override; + }; + + using MaterialFilePtr = std::shared_ptr; + MaterialFilePtr parse(Files::IStreamPtr&& stream); +} +#endif diff --git a/components/bgsm/stream.cpp b/components/bgsm/stream.cpp new file mode 100644 index 0000000000..c4fa9c1d8c --- /dev/null +++ b/components/bgsm/stream.cpp @@ -0,0 +1,39 @@ +#include "stream.hpp" + +namespace Bgsm +{ + template <> + void BGSMStream::read(osg::Vec2f& vec) + { + readBufferOfType(mStream, vec._v); + } + + template <> + void BGSMStream::read(osg::Vec3f& vec) + { + readBufferOfType(mStream, vec._v); + } + + template <> + void BGSMStream::read(osg::Vec4f& vec) + { + readBufferOfType(mStream, vec._v); + } + + template <> + void BGSMStream::read(std::string& str) + { + std::uint32_t length; + read(length); + // Prevent potential memory allocation freezes; strings this long are not expected in BGSM + if (length > 1024) + throw std::runtime_error("Requested string length is too large: " + std::to_string(length)); + str = std::string(length, '\0'); + mStream->read(str.data(), length); + if (mStream->bad()) + throw std::runtime_error("Failed to read sized string of " + std::to_string(length) + " chars"); + std::size_t end = str.find('\0'); + if (end != std::string::npos) + str.erase(end); + } +} diff --git a/components/bgsm/stream.hpp b/components/bgsm/stream.hpp new file mode 100644 index 0000000000..a355523367 --- /dev/null +++ b/components/bgsm/stream.hpp @@ -0,0 +1,77 @@ +#ifndef OPENMW_COMPONENTS_BGSM_STREAM_HPP +#define OPENMW_COMPONENTS_BGSM_STREAM_HPP + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace Bgsm +{ + template + inline void readBufferOfType(Files::IStreamPtr& pIStream, T* dest) + { + static_assert(std::is_arithmetic_v, "Buffer element type is not arithmetic"); + pIStream->read(reinterpret_cast(dest), numInstances * sizeof(T)); + if (pIStream->bad()) + throw std::runtime_error("Failed to read typed (" + std::string(typeid(T).name()) + ") buffer of " + + std::to_string(numInstances) + " instances"); + if constexpr (Misc::IS_BIG_ENDIAN) + for (std::size_t i = 0; i < numInstances; i++) + Misc::swapEndiannessInplace(dest[i]); + } + + template + inline void readBufferOfType(Files::IStreamPtr& pIStream, T (&dest)[numInstances]) + { + readBufferOfType(pIStream, static_cast(dest)); + } + + class BGSMStream + { + Files::IStreamPtr mStream; + + public: + explicit BGSMStream(Files::IStreamPtr&& stream) + : mStream(std::move(stream)) + { + } + + void skip(size_t size) { mStream->ignore(size); } + + /// Read into a single instance of type + template + void read(T& data) + { + readBufferOfType<1>(mStream, &data); + } + + /// Read multiple instances of type into an array + template + void readArray(std::array& arr) + { + readBufferOfType(mStream, arr.data()); + } + }; + + template <> + void BGSMStream::read(osg::Vec2f& vec); + template <> + void BGSMStream::read(osg::Vec3f& vec); + template <> + void BGSMStream::read(osg::Vec4f& vec); + template <> + void BGSMStream::read(std::string& str); +} + +#endif diff --git a/components/contentselector/model/naturalsort.cpp b/components/contentselector/model/naturalsort.cpp deleted file mode 100644 index b090137d97..0000000000 --- a/components/contentselector/model/naturalsort.cpp +++ /dev/null @@ -1,112 +0,0 @@ -/* - * This file contains code found in the QtGui module of the Qt Toolkit. - * See Qt's qfilesystemmodel source files for more information - */ - -#include "naturalsort.hpp" - -static inline QChar getNextChar(const QString& s, int location) -{ - return (location < s.length()) ? s.at(location) : QChar(); -} - -/*! - * Natural number sort, skips spaces. - * - * Examples: - * 1, 2, 10, 55, 100 - * 01.jpg, 2.jpg, 10.jpg - * - * Note on the algorithm: - * Only as many characters as necessary are looked at and at most they all - * are looked at once. - * - * Slower then QString::compare() (of course) - */ -int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs) -{ - for (int l1 = 0, l2 = 0; l1 <= s1.count() && l2 <= s2.count(); ++l1, ++l2) - { - // skip spaces, tabs and 0's - QChar c1 = getNextChar(s1, l1); - while (c1.isSpace()) - c1 = getNextChar(s1, ++l1); - QChar c2 = getNextChar(s2, l2); - while (c2.isSpace()) - c2 = getNextChar(s2, ++l2); - - if (c1.isDigit() && c2.isDigit()) - { - while (c1.digitValue() == 0) - c1 = getNextChar(s1, ++l1); - while (c2.digitValue() == 0) - c2 = getNextChar(s2, ++l2); - - int lookAheadLocation1 = l1; - int lookAheadLocation2 = l2; - int currentReturnValue = 0; - // find the last digit, setting currentReturnValue as we go if it isn't equal - for (QChar lookAhead1 = c1, lookAhead2 = c2; - (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length()); - lookAhead1 = getNextChar(s1, ++lookAheadLocation1), lookAhead2 = getNextChar(s2, ++lookAheadLocation2)) - { - bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit(); - bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit(); - if (!is1ADigit && !is2ADigit) - break; - if (!is1ADigit) - return -1; - if (!is2ADigit) - return 1; - if (currentReturnValue == 0) - { - if (lookAhead1 < lookAhead2) - { - currentReturnValue = -1; - } - else if (lookAhead1 > lookAhead2) - { - currentReturnValue = 1; - } - } - } - if (currentReturnValue != 0) - return currentReturnValue; - } - - if (cs == Qt::CaseInsensitive) - { - if (!c1.isLower()) - c1 = c1.toLower(); - if (!c2.isLower()) - c2 = c2.toLower(); - } - int r = QString::localeAwareCompare(c1, c2); - if (r < 0) - return -1; - if (r > 0) - return 1; - } - // The two strings are the same (02 == 2) so fall back to the normal sort - return QString::compare(s1, s2, cs); -} - -bool naturalSortLessThanCS(const QString& left, const QString& right) -{ - return (naturalCompare(left, right, Qt::CaseSensitive) < 0); -} - -bool naturalSortLessThanCI(const QString& left, const QString& right) -{ - return (naturalCompare(left, right, Qt::CaseInsensitive) < 0); -} - -bool naturalSortGreaterThanCS(const QString& left, const QString& right) -{ - return (naturalCompare(left, right, Qt::CaseSensitive) > 0); -} - -bool naturalSortGreaterThanCI(const QString& left, const QString& right) -{ - return (naturalCompare(left, right, Qt::CaseInsensitive) > 0); -} diff --git a/components/contentselector/model/naturalsort.hpp b/components/contentselector/model/naturalsort.hpp deleted file mode 100644 index 6973c55c2e..0000000000 --- a/components/contentselector/model/naturalsort.hpp +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef NATURALSORT_H -#define NATURALSORT_H - -#include - -bool naturalSortLessThanCS(const QString& left, const QString& right); -bool naturalSortLessThanCI(const QString& left, const QString& right); -bool naturalSortGreaterThanCS(const QString& left, const QString& right); -bool naturalSortGreaterThanCI(const QString& left, const QString& right); - -#endif diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index a3fd224390..ab12f45fd7 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -31,7 +31,7 @@ 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)); mContentModel = new ContentSelectorModel::ContentModel(this, warningIcon, showOMWScripts); } diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index 1d5b57bfd9..5ed0bcc972 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -173,6 +173,11 @@ std::string Misc::ResourceHelpers::correctActorModelPath(const std::string& resP return mdlname; } +std::string Misc::ResourceHelpers::correctMaterialPath(std::string_view resPath, const VFS::Manager* vfs) +{ + return correctResourcePath({ { "materials" } }, resPath, vfs); +} + std::string Misc::ResourceHelpers::correctMeshPath(std::string_view resPath) { std::string res = "meshes\\"; @@ -186,9 +191,10 @@ VFS::Path::Normalized Misc::ResourceHelpers::correctSoundPath(VFS::Path::Normali return prefix / resPath; } -std::string Misc::ResourceHelpers::correctMusicPath(const std::string& resPath) +VFS::Path::Normalized Misc::ResourceHelpers::correctMusicPath(VFS::Path::NormalizedView resPath) { - return "music\\" + resPath; + static constexpr VFS::Path::NormalizedView prefix("music"); + return prefix / resPath; } std::string_view Misc::ResourceHelpers::meshPathForESM3(std::string_view resPath) diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp index cda99d928d..069d921fa9 100644 --- a/components/misc/resourcehelpers.hpp +++ b/components/misc/resourcehelpers.hpp @@ -34,15 +34,16 @@ namespace Misc /// Use "xfoo.nif" instead of "foo.nif" if "xfoo.kf" is available /// Note that if "xfoo.nif" is actually unavailable, we can't fall back to "foo.nif". :( std::string correctActorModelPath(const std::string& resPath, const VFS::Manager* vfs); + std::string correctMaterialPath(std::string_view resPath, const VFS::Manager* vfs); // Adds "meshes\\". std::string correctMeshPath(std::string_view resPath); - // Adds "sound\\". + // Prepends "sound/". VFS::Path::Normalized correctSoundPath(VFS::Path::NormalizedView resPath); - // Adds "music\\". - std::string correctMusicPath(const std::string& resPath); + // Prepends "music/". + VFS::Path::Normalized correctMusicPath(VFS::Path::NormalizedView resPath); // Removes "meshes\\". std::string_view meshPathForESM3(std::string_view resPath); diff --git a/components/misc/strings/algorithm.hpp b/components/misc/strings/algorithm.hpp index 28bc696cd3..18f72104bd 100644 --- a/components/misc/strings/algorithm.hpp +++ b/components/misc/strings/algorithm.hpp @@ -15,6 +15,13 @@ namespace Misc::StringUtils bool operator()(char x, char y) const { return toLower(x) < toLower(y); } }; + inline std::string underscoresToSpaces(const std::string_view oldName) + { + std::string newName(oldName); + std::replace(newName.begin(), newName.end(), '_', ' '); + return newName; + } + inline bool ciLess(std::string_view x, std::string_view y) { return std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end(), CiCharLess()); diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 8d46b0f751..05a8378c11 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include // particle @@ -42,6 +43,7 @@ #include #include +#include #include #include #include @@ -247,6 +249,8 @@ namespace NifOsg } std::filesystem::path mFilename; unsigned int mVersion, mUserVersion, mBethVersion; + Resource::BgsmFileManager* mMaterialManager{ nullptr }; + Resource::ImageManager* mImageManager{ nullptr }; size_t mFirstRootTextureIndex{ ~0u }; bool mFoundFirstRootTexturingProperty = false; @@ -339,7 +343,6 @@ namespace NifOsg struct HandleNodeArgs { unsigned int mNifVersion; - Resource::ImageManager* mImageManager; SceneUtil::TextKeyMap* mTextKeys; std::vector mBoundTextures = {}; int mAnimFlags = 0; @@ -349,7 +352,7 @@ namespace NifOsg osg::Node* mRootNode = nullptr; }; - osg::ref_ptr load(Nif::FileView nif, Resource::ImageManager* imageManager) + osg::ref_ptr load(Nif::FileView nif) { const size_t numRoots = nif.numRoots(); std::vector roots; @@ -371,10 +374,8 @@ namespace NifOsg created->setDataVariance(osg::Object::STATIC); for (const Nif::NiAVObject* root : roots) { - auto node = handleNode(root, nullptr, nullptr, - { .mNifVersion = nif.getVersion(), - .mImageManager = imageManager, - .mTextKeys = &textkeys->mTextKeys }); + auto node = handleNode( + root, nullptr, nullptr, { .mNifVersion = nif.getVersion(), .mTextKeys = &textkeys->mTextKeys }); created->addChild(node); } if (mHasNightDayLabel) @@ -405,8 +406,7 @@ namespace NifOsg } void applyNodeProperties(const Nif::NiAVObject* nifNode, osg::Node* applyTo, - SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, - std::vector& boundTextures, int animflags) + SceneUtil::CompositeStateSetUpdater* composite, std::vector& boundTextures, int animflags) { bool hasStencilProperty = false; @@ -444,8 +444,7 @@ namespace NifOsg if (property.getPtr()->recIndex == mFirstRootTextureIndex) applyTo->setUserValue("overrideFx", 1); } - handleProperty(property.getPtr(), applyTo, composite, imageManager, boundTextures, animflags, - hasStencilProperty); + handleProperty(property.getPtr(), applyTo, composite, boundTextures, animflags, hasStencilProperty); } } @@ -457,8 +456,7 @@ namespace NifOsg shaderprop = static_cast(nifNode)->mShaderProperty; if (!shaderprop.empty()) - handleProperty(shaderprop.getPtr(), applyTo, composite, imageManager, boundTextures, animflags, - hasStencilProperty); + handleProperty(shaderprop.getPtr(), applyTo, composite, boundTextures, animflags, hasStencilProperty); } static void setupController(const Nif::NiTimeController* ctrl, SceneUtil::Controller* toSetup, int animflags) @@ -522,32 +520,21 @@ namespace NifOsg sequenceNode->setMode(osg::Sequence::START); } - osg::ref_ptr handleSourceTexture( - const Nif::NiSourceTexture* st, Resource::ImageManager* imageManager) + osg::ref_ptr handleSourceTexture(const Nif::NiSourceTexture* st) const { - if (!st) - return nullptr; + if (st) + { + if (st->mExternal) + return getTextureImage(st->mFile); - osg::ref_ptr image; - if (st->mExternal) - { - std::string filename = Misc::ResourceHelpers::correctTexturePath(st->mFile, imageManager->getVFS()); - image = imageManager->getImage(filename); + if (!st->mData.empty()) + return handleInternalTexture(st->mData.getPtr()); } - else if (!st->mData.empty()) - { - image = handleInternalTexture(st->mData.getPtr()); - } - return image; + + return nullptr; } - void handleTextureWrapping(osg::Texture2D* texture, bool wrapS, bool wrapT) - { - texture->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); - texture->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); - } - - bool handleEffect(const Nif::NiAVObject* nifNode, osg::StateSet* stateset, Resource::ImageManager* imageManager) + bool handleEffect(const Nif::NiAVObject* nifNode, osg::StateSet* stateset) { if (nifNode->recType != Nif::RC_NiTextureEffect) { @@ -590,16 +577,12 @@ namespace NifOsg return false; } - osg::ref_ptr image(handleSourceTexture(textureEffect->mTexture.getPtr(), imageManager)); - osg::ref_ptr texture2d(new osg::Texture2D(image)); - if (image) - texture2d->setTextureSize(image->s(), image->t()); - texture2d->setName("envMap"); - handleTextureWrapping(texture2d, textureEffect->wrapS(), textureEffect->wrapT()); - - int texUnit = 3; // FIXME - - stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); + const unsigned int uvSet = 0; + const unsigned int texUnit = 3; // FIXME + std::vector boundTextures; + boundTextures.resize(3); // Dummy vector for attachNiSourceTexture + attachNiSourceTexture("envMap", textureEffect->mTexture.getPtr(), textureEffect->wrapS(), + textureEffect->wrapT(), uvSet, stateset, boundTextures); stateset->setTextureAttributeAndModes(texUnit, texGen, osg::StateAttribute::ON); stateset->setTextureAttributeAndModes(texUnit, createEmissiveTexEnv(), osg::StateAttribute::ON); @@ -761,7 +744,7 @@ namespace NifOsg osg::ref_ptr composite = new SceneUtil::CompositeStateSetUpdater; - applyNodeProperties(nifNode, node, composite, args.mImageManager, args.mBoundTextures, args.mAnimFlags); + applyNodeProperties(nifNode, node, composite, args.mBoundTextures, args.mAnimFlags); const bool isNiGeometry = isTypeNiGeometry(nifNode->recType); const bool isBSGeometry = isTypeBSGeometry(nifNode->recType); @@ -769,7 +752,7 @@ namespace NifOsg if (isGeometry && !args.mSkipMeshes) { - bool skip; + bool skip = false; if (args.mNifVersion <= Nif::NIFFile::NIFVersion::VER_MW) { skip = (args.mHasMarkers && Misc::StringUtils::ciStartsWith(nifNode->mName, "tri editormarker")) @@ -777,7 +760,11 @@ namespace NifOsg || Misc::StringUtils::ciStartsWith(nifNode->mName, "tri shadow"); } else - skip = args.mHasMarkers && Misc::StringUtils::ciStartsWith(nifNode->mName, "EditorMarker"); + { + if (args.mHasMarkers) + skip = Misc::StringUtils::ciStartsWith(nifNode->mName, "EditorMarker") + || Misc::StringUtils::ciStartsWith(nifNode->mName, "VisibilityEditorMarker"); + } if (!skip) { if (isNiGeometry) @@ -859,7 +846,7 @@ namespace NifOsg if (!effect.empty()) { osg::ref_ptr effectStateSet = new osg::StateSet; - if (handleEffect(effect.getPtr(), effectStateSet, args.mImageManager)) + if (handleEffect(effect.getPtr(), effectStateSet)) for (unsigned int i = 0; i < currentNode->getNumChildren(); ++i) currentNode->getChild(i)->getOrCreateStateSet()->merge(*effectStateSet); } @@ -1025,9 +1012,56 @@ namespace NifOsg } } + osg::ref_ptr getTextureImage(std::string_view path) const + { + if (!mImageManager) + return nullptr; + + std::string filename = Misc::ResourceHelpers::correctTexturePath(path, mImageManager->getVFS()); + return mImageManager->getImage(filename); + } + + osg::ref_ptr attachTexture(const std::string& name, osg::ref_ptr image, bool wrapS, + bool wrapT, unsigned int uvSet, osg::StateSet* stateset, std::vector& boundTextures) const + { + osg::ref_ptr texture2d = new osg::Texture2D(image); + if (image) + texture2d->setTextureSize(image->s(), image->t()); + texture2d->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); + texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); + unsigned int texUnit = boundTextures.size(); + if (stateset) + stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); + texture2d->setName(name); + boundTextures.emplace_back(uvSet); + return texture2d; + } + + osg::ref_ptr attachExternalTexture(const std::string& name, const std::string& path, bool wrapS, + bool wrapT, unsigned int uvSet, osg::StateSet* stateset, std::vector& boundTextures) const + { + return attachTexture(name, getTextureImage(path), wrapS, wrapT, uvSet, stateset, boundTextures); + } + + osg::ref_ptr attachNiSourceTexture(const std::string& name, const Nif::NiSourceTexture* st, + bool wrapS, bool wrapT, unsigned int uvSet, osg::StateSet* stateset, + std::vector& boundTextures) const + { + return attachTexture(name, handleSourceTexture(st), wrapS, wrapT, uvSet, stateset, boundTextures); + } + + static void clearBoundTextures(osg::StateSet* stateset, std::vector& boundTextures) + { + if (!boundTextures.empty()) + { + for (unsigned int i = 0; i < boundTextures.size(); ++i) + stateset->setTextureMode(i, GL_TEXTURE_2D, osg::StateAttribute::OFF); + boundTextures.clear(); + } + } + void handleTextureControllers(const Nif::NiProperty* texProperty, - SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, - osg::StateSet* stateset, int animflags) + SceneUtil::CompositeStateSetUpdater* composite, osg::StateSet* stateset, int animflags) { for (Nif::NiTimeControllerPtr ctrl = texProperty->mController; !ctrl.empty(); ctrl = ctrl->mNext) { @@ -1056,17 +1090,16 @@ namespace NifOsg wrapT = inherit->getWrap(osg::Texture2D::WRAP_T); } + const unsigned int uvSet = 0; + std::vector boundTextures; // Dummy list for attachTexture for (const auto& source : flipctrl->mSources) { if (source.empty()) continue; - osg::ref_ptr image(handleSourceTexture(source.getPtr(), imageManager)); - osg::ref_ptr texture(new osg::Texture2D(image)); - if (image) - texture->setTextureSize(image->s(), image->t()); - texture->setWrap(osg::Texture::WRAP_S, wrapS); - texture->setWrap(osg::Texture::WRAP_T, wrapT); + // NB: not changing the stateset + osg::ref_ptr texture + = attachNiSourceTexture({}, source.getPtr(), wrapS, wrapT, uvSet, nullptr, boundTextures); textures.push_back(texture); } osg::ref_ptr callback(new FlipController(flipctrl, textures)); @@ -1811,7 +1844,7 @@ namespace NifOsg } } - osg::ref_ptr handleInternalTexture(const Nif::NiPixelData* pixelData) + osg::ref_ptr handleInternalTexture(const Nif::NiPixelData* pixelData) const { if (pixelData->mMipmaps.empty()) return nullptr; @@ -1946,7 +1979,7 @@ namespace NifOsg return image; } - osg::ref_ptr createEmissiveTexEnv() + static osg::ref_ptr createEmissiveTexEnv() { osg::ref_ptr texEnv(new osg::TexEnvCombine); // Sum the previous colour and the emissive colour. @@ -1977,33 +2010,42 @@ namespace NifOsg void handleTextureProperty(const Nif::NiTexturingProperty* texprop, const std::string& nodeName, osg::StateSet* stateset, SceneUtil::CompositeStateSetUpdater* composite, - Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags) + std::vector& boundTextures, int animflags) { - if (!boundTextures.empty()) - { - // overriding a parent NiTexturingProperty, so remove what was previously bound - for (unsigned int i = 0; i < boundTextures.size(); ++i) - stateset->setTextureMode(i, GL_TEXTURE_2D, osg::StateAttribute::OFF); - boundTextures.clear(); - } + // overriding a parent NiTexturingProperty, so remove what was previously bound + clearBoundTextures(stateset, boundTextures); // If this loop is changed such that the base texture isn't guaranteed to end up in texture unit 0, the // shadow casting shader will need to be updated accordingly. for (size_t i = 0; i < texprop->mTextures.size(); ++i) { - if (texprop->mTextures[i].mEnabled - || (i == Nif::NiTexturingProperty::BaseTexture && !texprop->mController.empty())) + const Nif::NiTexturingProperty::Texture& tex = texprop->mTextures[i]; + if (tex.mEnabled || (i == Nif::NiTexturingProperty::BaseTexture && !texprop->mController.empty())) { + std::string textureName; switch (i) { // These are handled later on case Nif::NiTexturingProperty::BaseTexture: + textureName = "diffuseMap"; + break; case Nif::NiTexturingProperty::GlowTexture: + textureName = "glowMap"; + break; case Nif::NiTexturingProperty::DarkTexture: + textureName = "darkMap"; + break; case Nif::NiTexturingProperty::BumpTexture: + textureName = "bumpMap"; + break; case Nif::NiTexturingProperty::DetailTexture: + textureName = "detailMap"; + break; case Nif::NiTexturingProperty::DecalTexture: + textureName = "decalMap"; + break; case Nif::NiTexturingProperty::GlossTexture: + textureName = "glossMap"; break; default: { @@ -2013,12 +2055,9 @@ namespace NifOsg } } - unsigned int uvSet = 0; - // create a new texture, will later attempt to share using the SharedStateManager - osg::ref_ptr texture2d; - if (texprop->mTextures[i].mEnabled) + const unsigned int texUnit = boundTextures.size(); + if (tex.mEnabled) { - const Nif::NiTexturingProperty::Texture& tex = texprop->mTextures[i]; if (tex.mSourceTexture.empty() && texprop->mController.empty()) { if (i == 0) @@ -2028,32 +2067,18 @@ namespace NifOsg } if (!tex.mSourceTexture.empty()) - { - const Nif::NiSourceTexture* st = tex.mSourceTexture.getPtr(); - osg::ref_ptr image = handleSourceTexture(st, imageManager); - texture2d = new osg::Texture2D(image); - if (image) - texture2d->setTextureSize(image->s(), image->t()); - } + attachNiSourceTexture(textureName, tex.mSourceTexture.getPtr(), tex.wrapS(), tex.wrapT(), + tex.mUVSet, stateset, boundTextures); else - texture2d = new osg::Texture2D; - - handleTextureWrapping(texture2d, tex.wrapS(), tex.wrapT()); - - uvSet = tex.mUVSet; + attachTexture( + textureName, nullptr, tex.wrapS(), tex.wrapT(), tex.mUVSet, stateset, boundTextures); } else { // Texture only comes from NiFlipController, so tex is ignored, set defaults - texture2d = new osg::Texture2D; - handleTextureWrapping(texture2d, true, true); - uvSet = 0; + attachTexture(textureName, nullptr, true, true, 0, stateset, boundTextures); } - unsigned int texUnit = boundTextures.size(); - - stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); - if (i == Nif::NiTexturingProperty::GlowTexture) { stateset->setTextureAttributeAndModes(texUnit, createEmissiveTexEnv(), osg::StateAttribute::ON); @@ -2121,51 +2146,165 @@ namespace NifOsg texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA); stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); } - - switch (i) - { - case Nif::NiTexturingProperty::BaseTexture: - texture2d->setName("diffuseMap"); - break; - case Nif::NiTexturingProperty::BumpTexture: - texture2d->setName("bumpMap"); - break; - case Nif::NiTexturingProperty::GlowTexture: - texture2d->setName("emissiveMap"); - break; - case Nif::NiTexturingProperty::DarkTexture: - texture2d->setName("darkMap"); - break; - case Nif::NiTexturingProperty::DetailTexture: - texture2d->setName("detailMap"); - break; - case Nif::NiTexturingProperty::DecalTexture: - texture2d->setName("decalMap"); - break; - case Nif::NiTexturingProperty::GlossTexture: - texture2d->setName("glossMap"); - break; - default: - break; - } - - boundTextures.push_back(uvSet); } } - handleTextureControllers(texprop, composite, imageManager, stateset, animflags); + handleTextureControllers(texprop, composite, stateset, animflags); } - void handleTextureSet(const Nif::BSShaderTextureSet* textureSet, unsigned int clamp, - const std::string& nodeName, osg::StateSet* stateset, Resource::ImageManager* imageManager, - std::vector& boundTextures) + static Bgsm::MaterialFilePtr getShaderMaterial( + std::string_view path, Resource::BgsmFileManager* materialManager) { - if (!boundTextures.empty()) + if (!materialManager) + return nullptr; + + if (!Misc::StringUtils::ciEndsWith(path, ".bgem") && !Misc::StringUtils::ciEndsWith(path, ".bgsm")) + return nullptr; + + std::string normalizedPath = Misc::ResourceHelpers::correctMaterialPath(path, materialManager->getVFS()); + try { - for (unsigned int i = 0; i < boundTextures.size(); ++i) - stateset->setTextureMode(i, GL_TEXTURE_2D, osg::StateAttribute::OFF); - boundTextures.clear(); + return materialManager->get(VFS::Path::Normalized(normalizedPath)); + } + catch (std::exception& e) + { + Log(Debug::Error) << "Failed to load shader material: " << e.what(); + return nullptr; + } + } + + void handleShaderMaterialNodeProperties( + Bgsm::MaterialFilePtr material, osg::StateSet* stateset, std::vector& boundTextures) + { + const unsigned int uvSet = 0; + const bool wrapS = (material->mClamp >> 1) & 0x1; + const bool wrapT = material->mClamp & 0x1; + if (material->mShaderType == Bgsm::ShaderType::Lighting) + { + const Bgsm::BGSMFile* bgsm = static_cast(material.get()); + + if (!bgsm->mDiffuseMap.empty()) + attachExternalTexture( + "diffuseMap", bgsm->mDiffuseMap, wrapS, wrapT, uvSet, stateset, boundTextures); + + if (!bgsm->mNormalMap.empty()) + attachExternalTexture("normalMap", bgsm->mNormalMap, wrapS, wrapT, uvSet, stateset, boundTextures); + + if (bgsm->mGlowMapEnabled && !bgsm->mGlowMap.empty()) + attachExternalTexture("emissiveMap", bgsm->mGlowMap, wrapS, wrapT, uvSet, stateset, boundTextures); + + if (bgsm->mTree) + stateset->addUniform(new osg::Uniform("useTreeAnim", true)); + } + else if (material->mShaderType == Bgsm::ShaderType::Effect) + { + const Bgsm::BGEMFile* bgem = static_cast(material.get()); + + if (!bgem->mBaseMap.empty()) + attachExternalTexture("diffuseMap", bgem->mBaseMap, wrapS, wrapT, uvSet, stateset, boundTextures); + + bool useFalloff = bgem->mFalloff; + stateset->addUniform(new osg::Uniform("useFalloff", useFalloff)); + if (useFalloff) + stateset->addUniform(new osg::Uniform("falloffParams", bgem->mFalloffParams)); } + if (material->mTwoSided) + stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + handleDepthFlags(stateset, material->mDepthTest, material->mDepthWrite); + } + + void handleDecal(bool enabled, bool hasSortAlpha, osg::Node& node) + { + if (!enabled) + return; + osg::ref_ptr stateset = node.getOrCreateStateSet(); + osg::ref_ptr polygonOffset(new osg::PolygonOffset); + polygonOffset->setUnits(SceneUtil::AutoDepth::isReversed() ? 1.f : -1.f); + polygonOffset->setFactor(SceneUtil::AutoDepth::isReversed() ? 0.65f : -0.65f); + polygonOffset = shareAttribute(polygonOffset); + stateset->setAttributeAndModes(polygonOffset, osg::StateAttribute::ON); + if (!mPushedSorter && !hasSortAlpha) + stateset->setRenderBinDetails(1, "SORT_BACK_TO_FRONT"); + } + + void handleAlphaTesting( + bool enabled, osg::AlphaFunc::ComparisonFunction function, int threshold, osg::Node& node) + { + if (enabled) + { + osg::ref_ptr alphaFunc(new osg::AlphaFunc(function, threshold / 255.f)); + alphaFunc = shareAttribute(alphaFunc); + node.getOrCreateStateSet()->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); + } + else if (osg::StateSet* stateset = node.getStateSet()) + { + stateset->removeAttribute(osg::StateAttribute::ALPHAFUNC); + stateset->removeMode(GL_ALPHA_TEST); + } + } + + void handleAlphaBlending( + bool enabled, int sourceMode, int destMode, bool sort, bool& hasSortAlpha, osg::Node& node) + { + if (enabled) + { + osg::ref_ptr stateset = node.getOrCreateStateSet(); + osg::ref_ptr blendFunc( + new osg::BlendFunc(getBlendMode(sourceMode), getBlendMode(destMode))); + // on AMD hardware, alpha still seems to be stored with an RGBA framebuffer with OpenGL. + // This might be mandated by the OpenGL 2.1 specification section 2.14.9, or might be a bug. + // Either way, D3D8.1 doesn't do that, so adapt the destination factor. + if (blendFunc->getDestination() == GL_DST_ALPHA) + blendFunc->setDestination(GL_ONE); + blendFunc = shareAttribute(blendFunc); + stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); + + if (sort) + { + hasSortAlpha = true; + if (!mPushedSorter) + stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + } + else if (!mPushedSorter) + { + stateset->setRenderBinToInherit(); + } + } + else if (osg::ref_ptr stateset = node.getStateSet()) + { + stateset->removeAttribute(osg::StateAttribute::BLENDFUNC); + stateset->removeMode(GL_BLEND); + if (!mPushedSorter) + stateset->setRenderBinToInherit(); + } + } + + void handleShaderMaterialDrawableProperties( + Bgsm::MaterialFilePtr shaderMat, osg::ref_ptr mat, osg::Node& node, bool& hasSortAlpha) + { + mat->setAlpha(osg::Material::FRONT_AND_BACK, shaderMat->mTransparency); + handleAlphaTesting(shaderMat->mAlphaTest, osg::AlphaFunc::GREATER, shaderMat->mAlphaTestThreshold, node); + handleAlphaBlending(shaderMat->mAlphaBlend, shaderMat->mSourceBlendMode, shaderMat->mDestinationBlendMode, + true, hasSortAlpha, node); + handleDecal(shaderMat->mDecal, hasSortAlpha, node); + if (shaderMat->mShaderType == Bgsm::ShaderType::Lighting) + { + auto bgsm = static_cast(shaderMat.get()); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(bgsm->mEmittanceColor, 1.f)); + mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(bgsm->mSpecularColor, 1.f)); + } + else if (shaderMat->mShaderType == Bgsm::ShaderType::Effect) + { + auto bgem = static_cast(shaderMat.get()); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(bgem->mEmittanceColor, 1.f)); + if (bgem->mSoft) + SceneUtil::setupSoftEffect(node, bgem->mSoftDepth, true, bgem->mSoftDepth); + } + } + + void handleTextureSet(const Nif::BSShaderTextureSet* textureSet, bool wrapS, bool wrapT, + const std::string& nodeName, osg::StateSet* stateset, std::vector& boundTextures) + { const unsigned int uvSet = 0; for (size_t i = 0; i < textureSet->mTextures.size(); ++i) @@ -2175,8 +2314,16 @@ namespace NifOsg switch (static_cast(i)) { case Nif::BSShaderTextureSet::TextureType::Base: + attachExternalTexture( + "diffuseMap", textureSet->mTextures[i], wrapS, wrapT, uvSet, stateset, boundTextures); + break; case Nif::BSShaderTextureSet::TextureType::Normal: + attachExternalTexture( + "normalMap", textureSet->mTextures[i], wrapS, wrapT, uvSet, stateset, boundTextures); + break; case Nif::BSShaderTextureSet::TextureType::Glow: + attachExternalTexture( + "emissiveMap", textureSet->mTextures[i], wrapS, wrapT, uvSet, stateset, boundTextures); break; default: { @@ -2185,31 +2332,6 @@ namespace NifOsg continue; } } - std::string filename - = Misc::ResourceHelpers::correctTexturePath(textureSet->mTextures[i], imageManager->getVFS()); - osg::ref_ptr image = imageManager->getImage(filename); - osg::ref_ptr texture2d = new osg::Texture2D(image); - if (image) - texture2d->setTextureSize(image->s(), image->t()); - handleTextureWrapping(texture2d, (clamp >> 1) & 0x1, clamp & 0x1); - unsigned int texUnit = boundTextures.size(); - stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); - // BSShaderTextureSet presence means there's no need for FFP support for the affected node - switch (static_cast(i)) - { - case Nif::BSShaderTextureSet::TextureType::Base: - texture2d->setName("diffuseMap"); - break; - case Nif::BSShaderTextureSet::TextureType::Normal: - texture2d->setName("normalMap"); - break; - case Nif::BSShaderTextureSet::TextureType::Glow: - texture2d->setName("emissiveMap"); - break; - default: - break; - } - boundTextures.emplace_back(uvSet); } } @@ -2269,8 +2391,8 @@ namespace NifOsg } void handleProperty(const Nif::NiProperty* property, osg::Node* node, - SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, - std::vector& boundTextures, int animflags, bool hasStencilProperty) + SceneUtil::CompositeStateSetUpdater* composite, std::vector& boundTextures, int animflags, + bool hasStencilProperty) { switch (property->recType) { @@ -2352,8 +2474,7 @@ namespace NifOsg { const Nif::NiTexturingProperty* texprop = static_cast(property); osg::StateSet* stateset = node->getOrCreateStateSet(); - handleTextureProperty( - texprop, node->getName(), stateset, composite, imageManager, boundTextures, animflags); + handleTextureProperty(texprop, node->getName(), stateset, composite, boundTextures, animflags); node->setUserValue("applyMode", static_cast(texprop->mApplyMode)); break; } @@ -2364,13 +2485,13 @@ namespace NifOsg node->setUserValue("shaderPrefix", std::string(getBSShaderPrefix(texprop->mType))); node->setUserValue("shaderRequired", shaderRequired); osg::StateSet* stateset = node->getOrCreateStateSet(); + clearBoundTextures(stateset, boundTextures); + const bool wrapS = (texprop->mClamp >> 1) & 0x1; + const bool wrapT = texprop->mClamp & 0x1; if (!texprop->mTextureSet.empty()) - { - auto textureSet = texprop->mTextureSet.getPtr(); handleTextureSet( - textureSet, texprop->mClamp, node->getName(), stateset, imageManager, boundTextures); - } - handleTextureControllers(texprop, composite, imageManager, stateset, animflags); + texprop->mTextureSet.getPtr(), wrapS, wrapT, node->getName(), stateset, boundTextures); + handleTextureControllers(texprop, composite, stateset, animflags); if (texprop->refraction()) SceneUtil::setupDistortion(*node, texprop->mRefraction.mStrength); break; @@ -2383,34 +2504,20 @@ namespace NifOsg node->setUserValue("shaderPrefix", std::string(getBSShaderPrefix(texprop->mType))); node->setUserValue("shaderRequired", shaderRequired); osg::StateSet* stateset = node->getOrCreateStateSet(); + clearBoundTextures(stateset, boundTextures); if (!texprop->mFilename.empty()) { - if (!boundTextures.empty()) - { - for (unsigned int i = 0; i < boundTextures.size(); ++i) - stateset->setTextureMode(i, GL_TEXTURE_2D, osg::StateAttribute::OFF); - boundTextures.clear(); - } - std::string filename - = Misc::ResourceHelpers::correctTexturePath(texprop->mFilename, imageManager->getVFS()); - osg::ref_ptr image = imageManager->getImage(filename); - osg::ref_ptr texture2d = new osg::Texture2D(image); - texture2d->setName("diffuseMap"); - if (image) - texture2d->setTextureSize(image->s(), image->t()); - handleTextureWrapping(texture2d, texprop->wrapS(), texprop->wrapT()); - const unsigned int texUnit = 0; const unsigned int uvSet = 0; - stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); - boundTextures.push_back(uvSet); - if (mBethVersion >= 27) - { - useFalloff = true; - stateset->addUniform(new osg::Uniform("falloffParams", texprop->mFalloffParams)); - } + attachExternalTexture("diffuseMap", texprop->mFilename, texprop->wrapS(), texprop->wrapT(), + uvSet, stateset, boundTextures); + } + if (mBethVersion >= 27) + { + useFalloff = true; + stateset->addUniform(new osg::Uniform("falloffParams", texprop->mFalloffParams)); } stateset->addUniform(new osg::Uniform("useFalloff", useFalloff)); - handleTextureControllers(texprop, composite, imageManager, stateset, animflags); + handleTextureControllers(texprop, composite, stateset, animflags); handleDepthFlags(stateset, texprop->depthTest(), texprop->depthWrite()); break; } @@ -2421,10 +2528,18 @@ namespace NifOsg node->setUserValue("shaderPrefix", std::string(getBSLightingShaderPrefix(texprop->mType))); node->setUserValue("shaderRequired", shaderRequired); osg::StateSet* stateset = node->getOrCreateStateSet(); + clearBoundTextures(stateset, boundTextures); + if (Bgsm::MaterialFilePtr material = getShaderMaterial(texprop->mName, mMaterialManager)) + { + handleShaderMaterialNodeProperties(material, stateset, boundTextures); + break; + } + const bool wrapS = (texprop->mClamp >> 1) & 0x1; + const bool wrapT = texprop->mClamp & 0x1; if (!texprop->mTextureSet.empty()) - handleTextureSet(texprop->mTextureSet.getPtr(), texprop->mClamp, node->getName(), stateset, - imageManager, boundTextures); - handleTextureControllers(texprop, composite, imageManager, stateset, animflags); + handleTextureSet( + texprop->mTextureSet.getPtr(), wrapS, wrapT, node->getName(), stateset, boundTextures); + handleTextureControllers(texprop, composite, stateset, animflags); if (texprop->doubleSided()) stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); if (texprop->treeAnim()) @@ -2442,27 +2557,20 @@ namespace NifOsg node->setUserValue("shaderPrefix", std::string("bs/nolighting")); node->setUserValue("shaderRequired", shaderRequired); osg::StateSet* stateset = node->getOrCreateStateSet(); + clearBoundTextures(stateset, boundTextures); + if (Bgsm::MaterialFilePtr material = getShaderMaterial(texprop->mName, mMaterialManager)) + { + handleShaderMaterialNodeProperties(material, stateset, boundTextures); + break; + } if (!texprop->mSourceTexture.empty()) { - if (!boundTextures.empty()) - { - for (unsigned int i = 0; i < boundTextures.size(); ++i) - stateset->setTextureMode(i, GL_TEXTURE_2D, osg::StateAttribute::OFF); - boundTextures.clear(); - } - std::string filename = Misc::ResourceHelpers::correctTexturePath( - texprop->mSourceTexture, imageManager->getVFS()); - osg::ref_ptr image = imageManager->getImage(filename); - osg::ref_ptr texture2d = new osg::Texture2D(image); - texture2d->setName("diffuseMap"); - if (image) - texture2d->setTextureSize(image->s(), image->t()); - handleTextureWrapping(texture2d, (texprop->mClamp >> 1) & 0x1, texprop->mClamp & 0x1); - const unsigned int texUnit = 0; const unsigned int uvSet = 0; - stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); - boundTextures.push_back(uvSet); - + const bool wrapS = (texprop->mClamp >> 1) & 0x1; + const bool wrapT = texprop->mClamp & 0x1; + unsigned int texUnit = boundTextures.size(); + attachExternalTexture( + "diffuseMap", texprop->mSourceTexture, wrapS, wrapT, uvSet, stateset, boundTextures); { osg::ref_ptr texMat(new osg::TexMat); // This handles 20.2.0.7 UV settings like 4.0.0.2 UV settings (see NifOsg::UVController) @@ -2484,7 +2592,7 @@ namespace NifOsg stateset->addUniform(new osg::Uniform("useFalloff", useFalloff)); if (useFalloff) stateset->addUniform(new osg::Uniform("falloffParams", texprop->mFalloffParams)); - handleTextureControllers(texprop, composite, imageManager, stateset, animflags); + handleTextureControllers(texprop, composite, stateset, animflags); if (texprop->doubleSided()) stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); handleDepthFlags(stateset, texprop->depthTest(), texprop->depthWrite()); @@ -2566,12 +2674,9 @@ namespace NifOsg bool hasMatCtrl = false; bool hasSortAlpha = false; - osg::StateSet* blendFuncStateSet = nullptr; - auto setBin_Transparent = [](osg::StateSet* ss) { ss->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); }; auto setBin_BackToFront = [](osg::StateSet* ss) { ss->setRenderBinDetails(0, "SORT_BACK_TO_FRONT"); }; auto setBin_Traversal = [](osg::StateSet* ss) { ss->setRenderBinDetails(2, "TraversalOrderBin"); }; - auto setBin_Inherit = [](osg::StateSet* ss) { ss->setRenderBinToInherit(); }; auto lightmode = Nif::NiVertexColorProperty::LightMode::LightMode_EmiAmbDif; float emissiveMult = 1.f; @@ -2657,52 +2762,10 @@ namespace NifOsg case Nif::RC_NiAlphaProperty: { const Nif::NiAlphaProperty* alphaprop = static_cast(property); - if (alphaprop->useAlphaBlending()) - { - osg::ref_ptr blendFunc( - new osg::BlendFunc(getBlendMode(alphaprop->sourceBlendMode()), - getBlendMode(alphaprop->destinationBlendMode()))); - // on AMD hardware, alpha still seems to be stored with an RGBA framebuffer with OpenGL. - // This might be mandated by the OpenGL 2.1 specification section 2.14.9, or might be a bug. - // Either way, D3D8.1 doesn't do that, so adapt the destination factor. - if (blendFunc->getDestination() == GL_DST_ALPHA) - blendFunc->setDestination(GL_ONE); - blendFunc = shareAttribute(blendFunc); - node->getOrCreateStateSet()->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); - - if (!alphaprop->noSorter()) - { - hasSortAlpha = true; - if (!mPushedSorter) - setBin_Transparent(node->getStateSet()); - } - else - { - if (!mPushedSorter) - setBin_Inherit(node->getStateSet()); - } - } - else if (osg::StateSet* stateset = node->getStateSet()) - { - stateset->removeAttribute(osg::StateAttribute::BLENDFUNC); - stateset->removeMode(GL_BLEND); - blendFuncStateSet = stateset; - if (!mPushedSorter) - blendFuncStateSet->setRenderBinToInherit(); - } - - if (alphaprop->useAlphaTesting()) - { - osg::ref_ptr alphaFunc(new osg::AlphaFunc( - getTestMode(alphaprop->alphaTestMode()), alphaprop->mThreshold / 255.f)); - alphaFunc = shareAttribute(alphaFunc); - node->getOrCreateStateSet()->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); - } - else if (osg::StateSet* stateset = node->getStateSet()) - { - stateset->removeAttribute(osg::StateAttribute::ALPHAFUNC); - stateset->removeMode(GL_ALPHA_TEST); - } + handleAlphaBlending(alphaprop->useAlphaBlending(), alphaprop->sourceBlendMode(), + alphaprop->destinationBlendMode(), !alphaprop->noSorter(), hasSortAlpha, *node); + handleAlphaTesting(alphaprop->useAlphaTesting(), getTestMode(alphaprop->alphaTestMode()), + alphaprop->mThreshold, *node); break; } case Nif::RC_BSShaderPPLightingProperty: @@ -2714,6 +2777,18 @@ namespace NifOsg case Nif::RC_BSLightingShaderProperty: { auto shaderprop = static_cast(property); + if (Bgsm::MaterialFilePtr shaderMat = getShaderMaterial(shaderprop->mName, mMaterialManager)) + { + handleShaderMaterialDrawableProperties(shaderMat, mat, *node, hasSortAlpha); + if (shaderMat->mShaderType == Bgsm::ShaderType::Lighting) + { + auto bgsm = static_cast(shaderMat.get()); + specEnabled = false; // bgsm->mSpecularEnabled; TODO: PBR specular lighting + specStrength = 1.f; // bgsm->mSpecularMult; + emissiveMult = bgsm->mEmittanceMult; + } + break; + } 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)); @@ -2722,31 +2797,18 @@ namespace NifOsg emissiveMult = shaderprop->mEmissiveMult; specStrength = shaderprop->mSpecStrength; specEnabled = shaderprop->specular(); - if (shaderprop->decal()) - { - osg::StateSet* stateset = node->getOrCreateStateSet(); - if (!mPushedSorter && !hasSortAlpha) - stateset->setRenderBinDetails(1, "SORT_BACK_TO_FRONT"); - osg::ref_ptr polygonOffset(new osg::PolygonOffset); - polygonOffset->setUnits(SceneUtil::AutoDepth::isReversed() ? 1.f : -1.f); - polygonOffset->setFactor(SceneUtil::AutoDepth::isReversed() ? 0.65f : -0.65f); - stateset->setAttributeAndModes(polygonOffset, osg::StateAttribute::ON); - } + handleDecal(shaderprop->decal(), hasSortAlpha, *node); break; } case Nif::RC_BSEffectShaderProperty: { auto shaderprop = static_cast(property); - if (shaderprop->decal()) + if (Bgsm::MaterialFilePtr shaderMat = getShaderMaterial(shaderprop->mName, mMaterialManager)) { - osg::StateSet* stateset = node->getOrCreateStateSet(); - if (!mPushedSorter && !hasSortAlpha) - stateset->setRenderBinDetails(1, "SORT_BACK_TO_FRONT"); - osg::ref_ptr polygonOffset(new osg::PolygonOffset); - polygonOffset->setUnits(SceneUtil::AutoDepth::isReversed() ? 1.f : -1.f); - polygonOffset->setFactor(SceneUtil::AutoDepth::isReversed() ? 0.65f : -0.65f); - stateset->setAttributeAndModes(polygonOffset, osg::StateAttribute::ON); + handleShaderMaterialDrawableProperties(shaderMat, mat, *node, hasSortAlpha); + break; } + handleDecal(shaderprop->decal(), hasSortAlpha, *node); if (shaderprop->softEffect()) SceneUtil::setupSoftEffect( *node, shaderprop->mFalloffDepth, true, shaderprop->mFalloffDepth); @@ -2860,10 +2922,13 @@ namespace NifOsg } }; - osg::ref_ptr Loader::load(Nif::FileView file, Resource::ImageManager* imageManager) + osg::ref_ptr Loader::load( + Nif::FileView file, Resource::ImageManager* imageManager, Resource::BgsmFileManager* materialManager) { LoaderImpl impl(file.getFilename(), file.getVersion(), file.getUserVersion(), file.getBethVersion()); - return impl.load(file, imageManager); + impl.mMaterialManager = materialManager; + impl.mImageManager = imageManager; + return impl.load(file); } void Loader::loadKf(Nif::FileView kf, SceneUtil::KeyframeHolder& target) diff --git a/components/nifosg/nifloader.hpp b/components/nifosg/nifloader.hpp index 21e0ae097c..14f16088cc 100644 --- a/components/nifosg/nifloader.hpp +++ b/components/nifosg/nifloader.hpp @@ -18,6 +18,7 @@ namespace osg namespace Resource { class ImageManager; + class BgsmFileManager; } namespace NifOsg @@ -30,7 +31,8 @@ namespace NifOsg public: /// Create a scene graph for the given NIF. Auto-detects when skinning is used and wraps the graph in a Skeleton /// if so. - static osg::ref_ptr load(Nif::FileView file, Resource::ImageManager* imageManager); + static osg::ref_ptr load( + Nif::FileView file, Resource::ImageManager* imageManager, Resource::BgsmFileManager* materialManager); /// Load keyframe controllers from the given kf file. static void loadKf(Nif::FileView kf, SceneUtil::KeyframeHolder& target); diff --git a/components/resource/bgsmfilemanager.cpp b/components/resource/bgsmfilemanager.cpp new file mode 100644 index 0000000000..7d93608603 --- /dev/null +++ b/components/resource/bgsmfilemanager.cpp @@ -0,0 +1,55 @@ +#include "bgsmfilemanager.hpp" + +#include + +#include + +#include "objectcache.hpp" + +namespace Resource +{ + + class BgsmFileHolder : public osg::Object + { + public: + BgsmFileHolder(const Bgsm::MaterialFilePtr& file) + : mBgsmFile(file) + { + } + BgsmFileHolder(const BgsmFileHolder& copy, const osg::CopyOp& copyop) + : mBgsmFile(copy.mBgsmFile) + { + } + + BgsmFileHolder() = default; + + META_Object(Resource, BgsmFileHolder) + + Bgsm::MaterialFilePtr mBgsmFile; + }; + + BgsmFileManager::BgsmFileManager(const VFS::Manager* vfs, double expiryDelay) + : ResourceManager(vfs, expiryDelay) + { + } + + Bgsm::MaterialFilePtr BgsmFileManager::get(VFS::Path::NormalizedView name) + { + osg::ref_ptr obj = mCache->getRefFromObjectCache(name); + if (obj) + return static_cast(obj.get())->mBgsmFile; + else + { + Bgsm::MaterialFilePtr file = Bgsm::parse(mVFS->get(name)); + obj = new BgsmFileHolder(file); + mCache->addEntryToObjectCache(name.value(), obj); + return file; + } + } + + void BgsmFileManager::reportStats(unsigned int frameNumber, osg::Stats* stats) const + { + Resource::reportStats("BSShader Material", frameNumber, mCache->getStats(), *stats); + } + +} diff --git a/components/resource/bgsmfilemanager.hpp b/components/resource/bgsmfilemanager.hpp new file mode 100644 index 0000000000..3c77c2c665 --- /dev/null +++ b/components/resource/bgsmfilemanager.hpp @@ -0,0 +1,27 @@ +#ifndef OPENMW_COMPONENTS_RESOURCE_BGSMFILEMANAGER_H +#define OPENMW_COMPONENTS_RESOURCE_BGSMFILEMANAGER_H + +#include + +#include "resourcemanager.hpp" + +namespace Resource +{ + + /// @brief Handles caching of material files. + /// @note May be used from any thread. + class BgsmFileManager : public ResourceManager + { + public: + BgsmFileManager(const VFS::Manager* vfs, double expiryDelay); + ~BgsmFileManager() = default; + + /// Retrieve a material file from the cache or load it from the VFS if not cached yet. + Bgsm::MaterialFilePtr get(VFS::Path::NormalizedView name); + + void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; + }; + +} + +#endif diff --git a/components/resource/keyframemanager.cpp b/components/resource/keyframemanager.cpp index 0cbbe40d60..84e7a7e311 100644 --- a/components/resource/keyframemanager.cpp +++ b/components/resource/keyframemanager.cpp @@ -37,11 +37,11 @@ namespace Resource bool RetrieveAnimationsVisitor::belongsToLeftUpperExtremity(const std::string& name) { - static const std::array boneNames = { "bip01_l_clavicle", "left_clavicle", "bip01_l_upperarm", "left_upper_arm", - "bip01_l_forearm", "bip01_l_hand", "left_hand", "left_wrist", "shield_bone", "bip01_l_pinky1", - "bip01_l_pinky2", "bip01_l_pinky3", "bip01_l_ring1", "bip01_l_ring2", "bip01_l_ring3", "bip01_l_middle1", - "bip01_l_middle2", "bip01_l_middle3", "bip01_l_pointer1", "bip01_l_pointer2", "bip01_l_pointer3", - "bip01_l_thumb1", "bip01_l_thumb2", "bip01_l_thumb3", "left_forearm" }; + static const std::array boneNames = { "bip01 l clavicle", "left clavicle", "bip01 l upperarm", "left upper arm", + "bip01 l forearm", "bip01 l hand", "left hand", "left wrist", "shield bone", "bip01 l pinky1", + "bip01 l pinky2", "bip01 l pinky3", "bip01 l ring1", "bip01 l ring2", "bip01 l ring3", "bip01 l middle1", + "bip01 l middle2", "bip01 l middle3", "bip01 l pointer1", "bip01 l pointer2", "bip01 l pointer3", + "bip01 l thumb1", "bip01 l thumb2", "bip01 l thumb3", "left forearm" }; if (std::find(boneNames.begin(), boneNames.end(), name) != boneNames.end()) return true; @@ -51,11 +51,11 @@ namespace Resource bool RetrieveAnimationsVisitor::belongsToRightUpperExtremity(const std::string& name) { - static const std::array boneNames = { "bip01_r_clavicle", "right_clavicle", "bip01_r_upperarm", - "right_upper_arm", "bip01_r_forearm", "bip01_r_hand", "right_hand", "right_wrist", "bip01_r_thumb1", - "bip01_r_thumb2", "bip01_r_thumb3", "weapon_bone", "bip01_r_pinky1", "bip01_r_pinky2", "bip01_r_pinky3", - "bip01_r_ring1", "bip01_r_ring2", "bip01_r_ring3", "bip01_r_middle1", "bip01_r_middle2", "bip01_r_middle3", - "bip01_r_pointer1", "bip01_r_pointer2", "bip01_r_pointer3", "right_forearm" }; + static const std::array boneNames = { "bip01 r clavicle", "right clavicle", "bip01 r upperarm", + "right upper arm", "bip01 r forearm", "bip01 r hand", "right hand", "right wrist", "bip01 r thumb1", + "bip01 r thumb2", "bip01 r thumb3", "weapon bone", "bip01 r pinky1", "bip01 r pinky2", "bip01 r pinky3", + "bip01 r ring1", "bip01 r ring2", "bip01 r ring3", "bip01 r middle1", "bip01 r middle2", "bip01 r middle3", + "bip01 r pointer1", "bip01 r pointer2", "bip01 r pointer3", "right forearm" }; if (std::find(boneNames.begin(), boneNames.end(), name) != boneNames.end()) return true; @@ -66,7 +66,7 @@ namespace Resource bool RetrieveAnimationsVisitor::belongsToTorso(const std::string& name) { static const std::array boneNames - = { "bip01_spine1", "bip01_spine2", "bip01_neck", "bip01_head", "head", "neck", "chest", "groin" }; + = { "bip01 spine1", "bip01 spine2", "bip01 neck", "bip01 head", "head", "neck", "chest", "groin" }; if (std::find(boneNames.begin(), boneNames.end(), name) != boneNames.end()) return true; @@ -99,6 +99,9 @@ namespace Resource const osgAnimation::ChannelList& channels = animation->getChannels(); for (const auto& channel : channels) { + // Replace channel target name to match the renamed bones/transforms + channel->setTargetName(Misc::StringUtils::underscoresToSpaces(channel->getTargetName())); + if (name == "Bip01 R Clavicle") { if (!belongsToRightUpperExtremity(channel->getTargetName())) diff --git a/components/resource/resourcesystem.cpp b/components/resource/resourcesystem.cpp index 65a83a60ab..f012627efb 100644 --- a/components/resource/resourcesystem.cpp +++ b/components/resource/resourcesystem.cpp @@ -2,6 +2,7 @@ #include +#include "bgsmfilemanager.hpp" #include "imagemanager.hpp" #include "keyframemanager.hpp" #include "niffilemanager.hpp" @@ -15,11 +16,14 @@ namespace Resource : mVFS(vfs) { mNifFileManager = std::make_unique(vfs, encoder); + mBgsmFileManager = std::make_unique(vfs, expiryDelay); mImageManager = std::make_unique(vfs, expiryDelay); - mSceneManager = std::make_unique(vfs, mImageManager.get(), mNifFileManager.get(), expiryDelay); + mSceneManager = std::make_unique( + vfs, mImageManager.get(), mNifFileManager.get(), mBgsmFileManager.get(), expiryDelay); mKeyframeManager = std::make_unique(vfs, mSceneManager.get(), expiryDelay, encoder); addResourceManager(mNifFileManager.get()); + addResourceManager(mBgsmFileManager.get()); addResourceManager(mKeyframeManager.get()); // note, scene references images so add images afterwards for correct implementation of updateCache() addResourceManager(mSceneManager.get()); @@ -43,6 +47,11 @@ namespace Resource return mImageManager.get(); } + BgsmFileManager* ResourceSystem::getBgsmFileManager() + { + return mBgsmFileManager.get(); + } + NifFileManager* ResourceSystem::getNifFileManager() { return mNifFileManager.get(); diff --git a/components/resource/resourcesystem.hpp b/components/resource/resourcesystem.hpp index f7f09b9277..5609176a89 100644 --- a/components/resource/resourcesystem.hpp +++ b/components/resource/resourcesystem.hpp @@ -25,6 +25,7 @@ namespace Resource class SceneManager; class ImageManager; + class BgsmFileManager; class NifFileManager; class KeyframeManager; class BaseResourceManager; @@ -41,6 +42,7 @@ namespace Resource SceneManager* getSceneManager(); ImageManager* getImageManager(); + BgsmFileManager* getBgsmFileManager(); NifFileManager* getNifFileManager(); KeyframeManager* getKeyframeManager(); @@ -74,6 +76,7 @@ namespace Resource private: std::unique_ptr mSceneManager; std::unique_ptr mImageManager; + std::unique_ptr mBgsmFileManager; std::unique_ptr mNifFileManager; std::unique_ptr mKeyframeManager; diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index e4d0b4363d..fbc6acb3cf 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -9,7 +9,10 @@ #include #include +#include #include +#include +#include #include @@ -52,6 +55,7 @@ #include #include +#include "bgsmfilemanager.hpp" #include "errormarker.hpp" #include "imagemanager.hpp" #include "niffilemanager.hpp" @@ -268,6 +272,11 @@ namespace Resource void apply(osg::Node& node) override { + // If an osgAnimation bone/transform, ensure underscores in name are replaced with spaces + // this is for compatibility reasons + if (dynamic_cast(&node)) + node.setName(Misc::StringUtils::underscoresToSpaces(node.getName())); + if (osg::StateSet* stateset = node.getStateSet()) { if (stateset->getRenderingHint() == osg::StateSet::TRANSPARENT_BIN) @@ -353,8 +362,55 @@ namespace Resource std::vector> mRigGeometryHolders; }; + void updateVertexInfluenceMap(osgAnimation::RigGeometry& rig) + { + osgAnimation::VertexInfluenceMap* vertexInfluenceMap = rig.getInfluenceMap(); + if (!vertexInfluenceMap) + return; + + std::vector renameList; + for (const auto& [boneName, unused] : *vertexInfluenceMap) + { + if (boneName.find('_') != std::string::npos) + renameList.push_back(boneName); + } + + for (const std::string& oldName : renameList) + { + const std::string newName = Misc::StringUtils::underscoresToSpaces(oldName); + if (vertexInfluenceMap->find(newName) == vertexInfluenceMap->end()) + (*vertexInfluenceMap)[newName] = std::move((*vertexInfluenceMap)[oldName]); + vertexInfluenceMap->erase(oldName); + } + } + + class RenameAnimCallbacksVisitor : public osg::NodeVisitor + { + public: + RenameAnimCallbacksVisitor() + : osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN) + { + } + + void apply(osg::MatrixTransform& node) override + { + // osgAnimation update callback name must match bone name/channel targets + osg::Callback* cb = node.getUpdateCallback(); + while (cb) + { + auto animCb = dynamic_cast*>(cb); + if (animCb) + animCb->setName(Misc::StringUtils::underscoresToSpaces(animCb->getName())); + + cb = cb->getNestedCallback(); + } + + traverse(node); + } + }; + SceneManager::SceneManager(const VFS::Manager* vfs, Resource::ImageManager* imageManager, - Resource::NifFileManager* nifFileManager, double expiryDelay) + Resource::NifFileManager* nifFileManager, Resource::BgsmFileManager* bgsmFileManager, double expiryDelay) : ResourceManager(vfs, expiryDelay) , mShaderManager(new Shader::ShaderManager) , mForceShaders(false) @@ -369,6 +425,7 @@ namespace Resource , mSharedStateManager(new SharedStateManager) , mImageManager(imageManager) , mNifFileManager(nifFileManager) + , mBgsmFileManager(bgsmFileManager) , mMinFilter(osg::Texture::LINEAR_MIPMAP_LINEAR) , mMagFilter(osg::Texture::LINEAR) , mMaxAnisotropy(1) @@ -556,6 +613,7 @@ namespace Resource VFS::Path::NormalizedView normalizedFilename, std::istream& model, Resource::ImageManager* imageManager) { const std::string_view ext = Misc::getFileExtension(normalizedFilename.value()); + const bool isColladaFile = ext == "dae"; osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension(std::string(ext)); if (!reader) { @@ -571,7 +629,7 @@ namespace Resource // findFileCallback would be necessary. but findFileCallback does not support virtual files, so we can't // implement it. options->setReadFileCallback(new ImageReadCallback(imageManager)); - if (ext == "dae") + if (isColladaFile) options->setOptionString("daeUseSequencedTextureUnits"); const std::array fileHash = Files::getHash(normalizedFilename.value(), model); @@ -599,9 +657,13 @@ namespace Resource node->accept(rigFinder); for (osg::Node* foundRigNode : rigFinder.mFoundNodes) { - if (foundRigNode->libraryName() == std::string("osgAnimation")) + if (foundRigNode->libraryName() == std::string_view("osgAnimation")) { osgAnimation::RigGeometry* foundRigGeometry = static_cast(foundRigNode); + + if (isColladaFile) + Resource::updateVertexInfluenceMap(*foundRigGeometry); + osg::ref_ptr newRig = new SceneUtil::RigGeometryHolder(*foundRigGeometry, osg::CopyOp::DEEP_COPY_ALL); @@ -616,13 +678,18 @@ namespace Resource } } - if (ext == "dae") + if (isColladaFile) { Resource::ColladaDescriptionVisitor colladaDescriptionVisitor; node->accept(colladaDescriptionVisitor); if (colladaDescriptionVisitor.mSkeleton) { + // Collada bones may have underscores in place of spaces due to a collada limitation + // we should rename the bones and update callbacks here at load time + Resource::RenameAnimCallbacksVisitor renameBoneVisitor; + node->accept(renameBoneVisitor); + if (osg::Group* group = dynamic_cast(node)) { group->removeChildren(0, group->getNumChildren()); @@ -730,11 +797,12 @@ namespace Resource } osg::ref_ptr load(VFS::Path::NormalizedView normalizedFilename, const VFS::Manager* vfs, - Resource::ImageManager* imageManager, Resource::NifFileManager* nifFileManager) + Resource::ImageManager* imageManager, Resource::NifFileManager* nifFileManager, + Resource::BgsmFileManager* materialMgr) { const std::string_view ext = Misc::getFileExtension(normalizedFilename.value()); if (ext == "nif") - return NifOsg::Loader::load(*nifFileManager->get(normalizedFilename), imageManager); + return NifOsg::Loader::load(*nifFileManager->get(normalizedFilename), imageManager, materialMgr); else if (ext == "spt") { Log(Debug::Warning) << "Ignoring SpeedTree data file " << normalizedFilename; @@ -856,7 +924,7 @@ namespace Resource { path.changeExtension(meshType); if (mVFS->exists(path)) - return load(path, mVFS, mImageManager, mNifFileManager); + return load(path, mVFS, mImageManager, mNifFileManager, mBgsmFileManager); } } catch (const std::exception& e) @@ -865,7 +933,8 @@ namespace Resource << ", using embedded marker_error instead"; } Files::IMemStream file(ErrorMarker::sValue.data(), ErrorMarker::sValue.size()); - return loadNonNif("error_marker.osgt", file, mImageManager); + constexpr VFS::Path::NormalizedView errorMarker("error_marker.osgt"); + return loadNonNif(errorMarker, file, mImageManager); } osg::ref_ptr SceneManager::cloneErrorMarker() @@ -888,7 +957,7 @@ namespace Resource osg::ref_ptr loaded; try { - loaded = load(normalized, mVFS, mImageManager, mNifFileManager); + loaded = load(normalized, mVFS, mImageManager, mNifFileManager, mBgsmFileManager); SceneUtil::ProcessExtraDataVisitor extraDataVisitor(this); loaded->accept(extraDataVisitor); diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index 3ad8a24892..31ad51694c 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -32,6 +32,7 @@ namespace Resource { class ImageManager; class NifFileManager; + class BgsmFileManager; class SharedStateManager; } @@ -90,7 +91,7 @@ namespace Resource { public: explicit SceneManager(const VFS::Manager* vfs, Resource::ImageManager* imageManager, - Resource::NifFileManager* nifFileManager, double expiryDelay); + Resource::NifFileManager* nifFileManager, Resource::BgsmFileManager* bgsmFileManager, double expiryDelay); ~SceneManager(); Shader::ShaderManager& getShaderManager(); @@ -259,6 +260,7 @@ namespace Resource Resource::ImageManager* mImageManager; Resource::NifFileManager* mNifFileManager; + Resource::BgsmFileManager* mBgsmFileManager; osg::Texture::FilterMode mMinFilter; osg::Texture::FilterMode mMagFilter; diff --git a/components/resource/stats.cpp b/components/resource/stats.cpp index 9bb90635d1..6730ddb303 100644 --- a/components/resource/stats.cpp +++ b/components/resource/stats.cpp @@ -87,6 +87,7 @@ namespace Resource "Image", "Nif", "Keyframe", + "BSShader Material", "Groundcover Chunk", "Object Chunk", "Terrain Chunk", diff --git a/components/sceneutil/extradata.cpp b/components/sceneutil/extradata.cpp index bd82e9abba..5e91830bba 100644 --- a/components/sceneutil/extradata.cpp +++ b/components/sceneutil/extradata.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include diff --git a/components/sceneutil/osgacontroller.cpp b/components/sceneutil/osgacontroller.cpp index 06b693a6ca..d3268fef36 100644 --- a/components/sceneutil/osgacontroller.cpp +++ b/components/sceneutil/osgacontroller.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -9,6 +10,7 @@ #include #include +#include #include #include #include @@ -24,6 +26,10 @@ namespace SceneUtil void LinkVisitor::link(osgAnimation::UpdateMatrixTransform* umt) { + // If osgAnimation had underscores, we should update the umt name also + // otherwise the animation channel and updates wont be applied + umt->setName(Misc::StringUtils::underscoresToSpaces(umt->getName())); + const osgAnimation::ChannelList& channels = mAnimation->getChannels(); for (const auto& channel : channels) { @@ -85,9 +91,8 @@ namespace SceneUtil } } - osg::Vec3f OsgAnimationController::getTranslation(float time) const + osg::Matrixf OsgAnimationController::getTransformForNode(float time, const std::string_view name) const { - osg::Vec3f translationValue; std::string animationName; float newTime = time; @@ -98,10 +103,11 @@ namespace SceneUtil { newTime = time - emulatedAnimation.mStartTime; animationName = emulatedAnimation.mName; + break; } } - // Find the root transform track in animation + // Find the bone's transform track in animation for (const auto& mergedAnimationTrack : mMergedAnimationTracks) { if (mergedAnimationTrack->getName() != animationName) @@ -111,7 +117,7 @@ namespace SceneUtil for (const auto& channel : channels) { - if (channel->getTargetName() != "bip01" || channel->getName() != "transform") + if (!Misc::StringUtils::ciEqual(name, channel->getTargetName()) || channel->getName() != "transform") continue; if (osgAnimation::MatrixLinearSampler* templateSampler @@ -119,13 +125,17 @@ namespace SceneUtil { osg::Matrixf matrix; templateSampler->getValueAt(newTime, matrix); - translationValue = matrix.getTrans(); - return osg::Vec3f(translationValue[0], translationValue[1], translationValue[2]); + return matrix; } } } - return osg::Vec3f(); + return osg::Matrixf::identity(); + } + + osg::Vec3f OsgAnimationController::getTranslation(float time) const + { + return getTransformForNode(time, "bip01").getTrans(); } void OsgAnimationController::update(float time, const std::string& animationName) @@ -162,6 +172,12 @@ namespace SceneUtil update(time - emulatedAnimation.mStartTime, emulatedAnimation.mName); } } + + // Reset the transform of this node to whats in the animation + // we force this here because downstream some code relies on the bone having a non-modified transform + // as this is how the NIF controller behaves. RotationController is a good example of this. + // Without this here, it causes osgAnimation skeletons to spin wildly + static_cast(node)->setMatrix(getTransformForNode(time, node->getName())); } traverse(node, nv); diff --git a/components/sceneutil/osgacontroller.hpp b/components/sceneutil/osgacontroller.hpp index 8739d68b99..bb9a621760 100644 --- a/components/sceneutil/osgacontroller.hpp +++ b/components/sceneutil/osgacontroller.hpp @@ -59,6 +59,9 @@ namespace SceneUtil /// @brief Handles the location of the instance osg::Vec3f getTranslation(float time) const override; + /// @brief Handles finding bone position in the animation + osg::Matrixf getTransformForNode(float time, const std::string_view name) const; + /// @brief Calls animation track update() void update(float time, const std::string& animationName); diff --git a/components/sceneutil/visitor.cpp b/components/sceneutil/visitor.cpp index ffcf12b167..8d1bf46322 100644 --- a/components/sceneutil/visitor.cpp +++ b/components/sceneutil/visitor.cpp @@ -5,6 +5,8 @@ #include +#include + #include #include @@ -13,7 +15,6 @@ namespace SceneUtil { - bool FindByNameVisitor::checkGroup(osg::Group& group) { if (Misc::StringUtils::ciEqual(group.getName(), mNameToFind)) @@ -22,35 +23,13 @@ namespace SceneUtil return true; } - // FIXME: can the nodes/bones be renamed at loading stage rather than each time? - // Convert underscores to whitespaces as a workaround for Collada (OpenMW's animation system uses - // whitespace-separated names) - std::string nodeName = group.getName(); - std::replace(nodeName.begin(), nodeName.end(), '_', ' '); - if (Misc::StringUtils::ciEqual(nodeName, mNameToFind)) - { - mFoundNode = &group; - return true; - } return false; } void FindByClassVisitor::apply(osg::Node& node) { if (Misc::StringUtils::ciEqual(node.className(), mNameToFind)) - { mFoundNodes.push_back(&node); - } - else - { - // FIXME: can the nodes/bones be renamed at loading stage rather than each time? - // Convert underscores to whitespaces as a workaround for Collada (OpenMW's animation system uses - // whitespace-separated names) - std::string nodeName = node.className(); - std::replace(nodeName.begin(), nodeName.end(), '_', ' '); - if (Misc::StringUtils::ciEqual(nodeName, mNameToFind)) - mFoundNodes.push_back(&node); - } traverse(node); } @@ -69,23 +48,19 @@ namespace SceneUtil void FindByNameVisitor::apply(osg::Geometry&) {} + void NodeMapVisitorBoneOnly::apply(osg::MatrixTransform& trans) + { + // Choose first found bone in file + if (dynamic_cast(&trans) != nullptr) + mMap.emplace(trans.getName(), &trans); + + traverse(trans); + } + void NodeMapVisitor::apply(osg::MatrixTransform& trans) { // Choose first found node in file - - if (trans.libraryName() == std::string_view("osgAnimation")) - { - std::string nodeName = trans.getName(); - - // FIXME: can the nodes/bones be renamed at loading stage rather than each time? - // Convert underscores to whitespaces as a workaround for Collada (OpenMW's animation system uses - // whitespace-separated names) - std::replace(nodeName.begin(), nodeName.end(), '_', ' '); - mMap.emplace(nodeName, &trans); - } - else - mMap.emplace(trans.getName(), &trans); - + mMap.emplace(trans.getName(), &trans); traverse(trans); } diff --git a/components/sceneutil/visitor.hpp b/components/sceneutil/visitor.hpp index 3e3df4d4b3..a9a943423c 100644 --- a/components/sceneutil/visitor.hpp +++ b/components/sceneutil/visitor.hpp @@ -50,14 +50,14 @@ namespace SceneUtil std::vector mFoundNodes; }; + typedef std::unordered_map, Misc::StringUtils::CiHash, + Misc::StringUtils::CiEqual> + NodeMap; + /// Maps names to nodes class NodeMapVisitor : public osg::NodeVisitor { public: - typedef std::unordered_map, Misc::StringUtils::CiHash, - Misc::StringUtils::CiEqual> - NodeMap; - NodeMapVisitor(NodeMap& map) : osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN) , mMap(map) @@ -70,6 +70,22 @@ namespace SceneUtil NodeMap& mMap; }; + /// Maps names to bone nodes + class NodeMapVisitorBoneOnly : public osg::NodeVisitor + { + public: + NodeMapVisitorBoneOnly(NodeMap& map) + : osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN) + , mMap(map) + { + } + + void apply(osg::MatrixTransform& trans) override; + + private: + NodeMap& mMap; + }; + /// @brief Base class for visitors that remove nodes from a scene graph. /// Subclasses need to fill the mToRemove vector. /// To use, node->accept(removeVisitor); removeVisitor.remove(); diff --git a/components/vfs/manager.cpp b/components/vfs/manager.cpp index 936dd64679..2d22a7a034 100644 --- a/components/vfs/manager.cpp +++ b/components/vfs/manager.cpp @@ -99,4 +99,21 @@ namespace VFS ++normalized.back(); return { it, mIndex.lower_bound(normalized) }; } + + RecursiveDirectoryRange Manager::getRecursiveDirectoryIterator(VFS::Path::NormalizedView path) const + { + if (path.value().empty()) + return { mIndex.begin(), mIndex.end() }; + const auto it = mIndex.lower_bound(path); + if (it == mIndex.end() || !it->first.view().starts_with(path.value())) + return { it, it }; + std::string copy(path.value()); + ++copy.back(); + return { it, mIndex.lower_bound(copy) }; + } + + RecursiveDirectoryRange Manager::getRecursiveDirectoryIterator() const + { + return { mIndex.begin(), mIndex.end() }; + } } diff --git a/components/vfs/manager.hpp b/components/vfs/manager.hpp index 59211602de..d64be1d1d1 100644 --- a/components/vfs/manager.hpp +++ b/components/vfs/manager.hpp @@ -65,6 +65,10 @@ namespace VFS /// @note May be called from any thread once the index has been built. RecursiveDirectoryRange getRecursiveDirectoryIterator(std::string_view path) const; + RecursiveDirectoryRange getRecursiveDirectoryIterator(VFS::Path::NormalizedView path) const; + + RecursiveDirectoryRange getRecursiveDirectoryIterator() const; + /// Retrieve the absolute path to the file /// @note Throws an exception if the file can not be found. /// @note May be called from any thread once the index has been built. diff --git a/components/vfs/pathutil.hpp b/components/vfs/pathutil.hpp index 07e73acfa9..50f16d1804 100644 --- a/components/vfs/pathutil.hpp +++ b/components/vfs/pathutil.hpp @@ -79,7 +79,7 @@ namespace VFS::Path public: constexpr NormalizedView() noexcept = default; - constexpr NormalizedView(const char* value) + constexpr explicit NormalizedView(const char* value) : mValue(value) { if (!isNormalized(mValue)) @@ -179,6 +179,12 @@ namespace VFS::Path return true; } + Normalized& operator=(NormalizedView value) + { + mValue = value.value(); + return *this; + } + Normalized& operator/=(NormalizedView value) { mValue.reserve(mValue.size() + value.value().size() + 1); @@ -258,6 +264,22 @@ namespace VFS::Path result /= rhs; return result; } + + struct Hash + { + using is_transparent = void; + + [[nodiscard]] std::size_t operator()(std::string_view sv) const { return std::hash{}(sv); } + + [[nodiscard]] std::size_t operator()(const std::string& s) const { return std::hash{}(s); } + + [[nodiscard]] std::size_t operator()(const Normalized& s) const { return std::hash{}(s.value()); } + + [[nodiscard]] std::size_t operator()(NormalizedView s) const + { + return std::hash{}(s.value()); + } + }; } #endif diff --git a/files/lang/components_sv.ts b/files/lang/components_sv.ts new file mode 100644 index 0000000000..8682a569bd --- /dev/null +++ b/files/lang/components_sv.ts @@ -0,0 +1,95 @@ + + + + + ContentSelector + + Select language used by ESM/ESP content files to allow OpenMW to detect their encoding. + Välj språk som används av ESM/ESP-innehållsfiler så att OpenMW kan hitta deras kodning. + + + + ContentSelectorModel::ContentModel + + Unable to find dependent file: %1 + Kunde inte hitta beroende fil: %1 + + + Dependent file needs to be active: %1 + Beroende fil måste vara aktiv: %1 + + + This file needs to load after %1 + Denna fil måste laddas efter %1 + + + + ContentSelectorModel::EsmFile + + <b>Author:</b> %1<br/><b>Format version:</b> %2<br/><b>Modified:</b> %3<br/><b>Path:</b><br/>%4<br/><br/><b>Description:</b><br/>%5<br/><br/><b>Dependencies: </b>%6<br/> + <b>Skapare:</b> %1<br/><b>Formatversion:</b> %2<br/><b>Ändrad:</b> %3<br/><b>Sökväg:</b><br/>%4<br/><br/><b>Beskrivning:</b><br/>%5<br/><br/><b>Beroenden: </b>%6<br/> + + + <br/><b>This content file cannot be disabled because it is part of OpenMW.</b><br/> + <br/><b>Denna innehållsfil kan inte inaktiveras då den är en del av OpenMW.</b><br/> + + + <br/><b>This content file cannot be disabled because it is enabled in a config file other than the user one.</b><br/> + <br/><b>Denna innehållsfil kan inte inaktiveras då den är en aktiverad i en annan konfigurationsfil än användarens.</b><br/> + + + + ContentSelectorView::ContentSelector + + &Check Selected + &Bocka i markerade + + + &Uncheck Selected + &Bocka ur markerade + + + &Copy Path(s) to Clipboard + &Kopiera sökväg(ar) till klippbordet + + + <No game file> + <Ingen spelfil> + + + + Process::ProcessInvoker + + Error starting executable + Kunde inte starta körbara filen + + + Error running executable + Fel när körbara filen skulle köras + + + +Arguments: + + +Argument: + + + + <html><head/><body><p><b>Could not find %1</b></p><p>The application is not found.</p><p>Please make sure OpenMW is installed correctly and try again.</p></body></html> + <html><head/><body><p><b>Kunde inte hitta %1</b></p><p>Applikationen hittas inte.</p><p>Se till att OpenMW är korrekt installerat och försök igen.</p></body></html> + + + <html><head/><body><p><b>Could not start %1</b></p><p>The application is not executable.</p><p>Please make sure you have the right permissions and try again.</p></body></html> + <html><head/><body><p><b>Kunde inte starta %1</b></p><p>Applikationen är inte körbar.</p><p>Se till att du har rätt behörigheter och försök igen.</p></body></html> + + + <html><head/><body><p><b>Could not start %1</b></p><p>An error occurred while starting %1.</p><p>Press "Show Details..." for more information.</p></body></html> + <html><head/><body><p><b>Kunde inte starta %1</b></p><p>Ett fel uppstod när %1 skulle startas.</p><p>Tryck på "Visa detaljer..." för mer information.</p></body></html> + + + <html><head/><body><p><b>Executable %1 returned an error</b></p><p>An error occurred while running %1.</p><p>Press "Show Details..." for more information.</p></body></html> + <html><head/><body><p><b>Körbara filen %1 gav ett felmeddelande</b></p><p>Ett fel uppstod när %1 kördes.</p><p>Tryck på "Visa detaljer..." för mer information.</p></body></html> + + + diff --git a/files/lang/launcher_sv.ts b/files/lang/launcher_sv.ts new file mode 100644 index 0000000000..4cd75487b2 --- /dev/null +++ b/files/lang/launcher_sv.ts @@ -0,0 +1,1494 @@ + + + + + DataFilesPage + + Content Files + Innehållsfiler + + + Data Directories + Datakataloger + + + Scan directories for likely data directories and append them at the end of the list. + Skanna kataloger för troliga datakataloger och lägg till dem i slutet på listan. + + + Append + Lägg till + + + Scan directories for likely data directories and insert them above the selected position + Skanna kataloger för troliga datakataloger och lägg till dem ovanför den markerade positionen + + + Insert Above + Lägg till ovanför + + + Move selected directory one position up + Flytta markerad katalog en position upp + + + Move Up + Flytta upp + + + Move selected directory one position down + Flytta markerad katalog en position ner + + + Move Down + Flytta ner + + + Remove selected directory + Ta bort markerad katalog + + + Remove + Ta bort + + + Archive Files + Arkivfiler + + + Move selected archive one position up + Flytta markerat arkiv en position upp + + + Move selected archive one position down + Flytta markerat arkiv en position ner + + + Navigation Mesh Cache + Cache för navigeringsmesh + + + Generate navigation mesh cache for all content. Will be used by the engine to make cell loading faster. + Generera navigeringsmesh för allt innehåll. Kommer användas av motorn för att ladda celler snabbare. + + + Update + Uppdatera + + + Cancel navigation mesh generation. Already processed data will be saved. + Avbryt generering av navigationsmesh. Redan processad data kommer sparas. + + + Cancel + Avbryt + + + MiB + MiB + + + Content List + Innehållslista + + + Select a content list + Välj en innehållslista + + + New Content List + Ny innehållslista + + + &New Content List + &Ny innehållslista + + + Clone Content List + Klona innehållslista + + + Delete Content List + Radera innehållslista + + + Ctrl+N + Ctrl+N + + + Ctrl+G + Ctrl+G + + + Ctrl+D + Ctrl+D + + + Check Selection + Kryssa markering + + + Uncheck Selection + Avkryssa markering + + + Refresh Data Files + Uppdatera datafiler + + + Ctrl+R + Ctrl+R + + + <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> + <html><head/><body><p>Notera: innehållsfiler som inte är del av aktuell innehållslista är <span style=" font-style:italic;font-weight: bold">markerade</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> + <html><head/><body><p>Notera: kataloger som inte är del av aktuell innehållslista är <span style=" font-style:italic;font-weight: bold">markerade</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> + <html><head/><body><p>Notera: arkiv som inte är del av aktuell innehållslista är <span style=" font-style:italic;font-weight: bold">markerade</span></p></body></html> + + + Remove Unused Tiles + Ta bort oanvända celler + + + Max Size + Maximal storlek + + + + GraphicsPage + + 0 + 0 + + + 2 + 2 + + + 4 + 4 + + + 8 + 8 + + + 16 + 16 + + + Custom: + Egen: + + + Standard: + Standard: + + + Fullscreen + Helskärm + + + Windowed Fullscreen + Helskärm i fönsterläge + + + Windowed + Fönster + + + Disabled + Inaktiverad + + + Enabled + Aktiverad + + + Adaptive + Adaptiv + + + FPS + FPS + + + × + × + + + Screen + Skärm + + + Resolution + Upplösning + + + Window Mode + Fönsterläge + + + Framerate Limit + Gränsvärde bilduppdateringsfrekvens + + + Window Border + Fönster, ram + + + Anti-Aliasing + Kantutjämning + + + Vertical Synchronization + Vertikal synkronisering + + + + ImportPage + + Form + Can be translated in different ways depending on context. Will return to later. + + + + Morrowind Installation Wizard + Installationsguide för Morrowind + + + Run &Installation Wizard + Kör &Installationsguide + + + Morrowind Settings Importer + Inställningsimporterare för Morrowind + + + Browse... + Bläddra... + + + Fonts shipped with the original engine are blurry with UI scaling and support only a small amount of characters, +so OpenMW provides another set of fonts to avoid these issues. These fonts use TrueType technology and are quite similar +to default Morrowind fonts. Check this box if you still prefer original fonts over OpenMW ones or if you use custom bitmap fonts. + Fonter som kommer med ordinarie spelmotor är suddiga med gränssnittsuppskalning och stödjer bara ett litet antal tecken, +så OpenMW tillhandahåller andra fonter för att undvika dessa problem. Dessa fonter använder TrueType-teknologi och är ganska lika +de ordinarie fonterna i Morrowind. Bocka denna ruta om du ändå föredrar ordinarie fonter istället för OpenMWs, alternativt om du använder egna bitmapfonter. + + + Run &Settings Importer + Kör &Inställningsimporterare + + + File to Import Settings From: + Fil att importera inställningar från: + + + Import Bitmap Fonts + Importera bitmapfonter + + + Import Add-on and Plugin Selection + Importera tillägg- och pluginmarkeringar + + + + Launcher::DataFilesPage + + English + Engelska + + + French + Franska + + + German + Tyska + + + Italian + Italienska + + + Polish + Polska + + + Russian + Ryska + + + Spanish + Spanska + + + New Content List + Ny innehållslista + + + Content List name: + Namn på innehållslista: + + + Clone Content List + Klona innehållslista + + + Select Directory + Välj katalog + + + Delete Content List + Radera innehållslista + + + Are you sure you want to delete <b>%1</b>? + Är du säker på att du vill radera <b>%1</b>? + + + Delete + Radera + + + Contains content file(s) + Innehåller innehållsfil(er) + + + Will be added to the current profile + Kommer läggas till nuvarande profil + + + &Check Selected + &Kryssa markerade + + + &Uncheck Selected + &Avkryssa markerade + + + Resolved as %1 + Löst som %1 + + + This is the data-local directory and cannot be disabled + Det här är den data-lokala katalogen och kan inte inaktiveras + + + This directory is part of OpenMW and cannot be disabled + Denna katalog är en del av OpenMW och kan inte inaktiveras + + + This directory is enabled in an openmw.cfg other than the user one + Denna katalog är aktiverad i en annan openmw.cfg än användarens + + + This archive is enabled in an openmw.cfg other than the user one + Detta arkiv är aktiverat i en annan openmw.cfg än användarens + + + + Launcher::GraphicsPage + + Error receiving number of screens + Det gick inte att ta emot antalet skärmar + + + <br><b>SDL_GetNumVideoDisplays failed:</b><br><br> + <br><b>SDL_GetNumVideoDisplays misslyckades:</b><br><br> + + + Screen + Skärm + + + Error receiving resolutions + Det gick inte att ta emot upplösningar + + + <br><b>SDL_GetNumDisplayModes failed:</b><br><br> + <br><b>SDL_GetNumDisplayModes misslyckades:</b><br><br> + + + <br><b>SDL_GetDisplayMode failed:</b><br><br> + <br><b>SDL_GetDisplayMode misslyckades:</b><br><br> + + + + Launcher::ImportPage + + Error writing OpenMW configuration file + Det gick inte att skriva en OpenMW-konfigurationsfil + + + Morrowind configuration file (*.ini) + Morrowind konfigurationsfil (*.ini) + + + Importer finished + Importeraren klar + + + Failed to import settings from INI file. + Misslyckades att importera inställningar från INI-fil. + + + <html><head/><body><p><b>Could not open or create %1 for writing </b></p><p>Please make sure you have the right permissions and try again.</p></body></html> + <html><head/><body><p><b>Kunde inte öppna eller skapa %1 för att skriva </b></p><p>Kontrollera att du har rätt behörigheter och försök igen.</p></body></html> + + + + Launcher::MainDialog + + Close + Stäng + + + Launch OpenMW + Starta OpenMW + + + Help + Hjälp + + + Error opening OpenMW configuration file + Fel när OpenMW-konfigurationsfil skulle öppnas + + + First run + Första körning + + + Run &Installation Wizard + Kör &Installationsguide + + + Skip + Hoppa över + + + OpenMW %1 release + OpenMW version %1 + + + OpenMW development (%1) + OpenMW utvecklarversion (%1) + + + Compiled on %1 %2 + Kompilerad den %1 %2 + + + Error detecting Morrowind installation + Kunde inte hitta Morrowindinstallation + + + Run &Installation Wizard... + Kör &Installationsguide... + + + Error reading OpenMW configuration files + Fel när OpenMW-konfigurationsfil skulle läsas + + + Error writing OpenMW configuration file + Fel när OpenMW-konfigurationsfil skulle skrivas> + + + Error writing user settings file + Fel när användarinställningsfil skulle skrivas + + + Error writing Launcher configuration file + Fel när Startarens-konfigurationsfil skulle skrivas + + + No game file selected + Ingen spelfil vald + + + <html><head/><body><p><b>Welcome to OpenMW!</b></p><p>It is recommended to run the Installation Wizard.</p><p>The Wizard will let you select an existing Morrowind installation, or install Morrowind for OpenMW to use.</p></body></html> + <html><head/><body><p><b>Välkommen till OpenMW!</b></p><p>Det är rekommenderat att du kör Installationsguiden.</p><p>Installationsguiden låter dig välja en befintlig Morrowindinstallation eller installera Morrowind för OpenMW.</p></body></html> + + + <br><b>Could not open %0 for reading:</b><br><br>%1<br><br>Please make sure you have the right permissions and try again.<br> + <br><b>Kunde inte öppna %0 för läsning:</b><br><br>%1<br><br>Se till att du har rätt behörigheter och försök igen.<br> + + + <br><b>Could not open %0 for reading</b><br><br>Please make sure you have the right permissions and try again.<br> + <br><b>Kunde inte öppna %0 för läsning</b><br><br>Se till att du har rätt behörigheter och försök igen.<br> + + + <br><b>Could not find the Data Files location</b><br><br>The directory containing the data files was not found. + <br><b>Kunde inte hitta Data Files-platsen</b><br><br>Katalogen som innehåller datafilerna hittades inte. + + + <br>The problem may be due to an incomplete installation of OpenMW.<br>Reinstalling OpenMW may resolve the problem.<br> + <br>Problemet kan bero på en ofullständig installation av OpenMW.<br>Ominstallation av OpenMW kan lösa problemet.<br> + + + <br><b>Could not open or create %0 for writing</b><br><br>Please make sure you have the right permissions and try again.<br> + <br><b>Kunde inte öppna eller skapa %0 för att skriva</b><br><br>Se till att du har rätt behörigheter och försök igen.<br> + + + <br><b>You do not have a game file selected.</b><br><br>OpenMW will not start without a game file selected.<br> + <br><b>Du har ingen spelfil markerad.</b><br><br>OpenMW kan inte starta utan en spelfil markerad.<br> + + + Error creating OpenMW configuration directory: code %0 + Kunde inte skapa konfigurationskatalog för OpenMW: kod %0 + + + <br><b>Could not create directory %0</b><br><br>%1<br> + <br><b>Kunde inte skapa katalog %0</b><br><br>%1<br> + + + + Launcher::SettingsPage + + Text file (*.txt) + Textfil (*.txt) + + + + MainWindow + + OpenMW Launcher + OpenMW Startare + + + OpenMW version + OpenMW version + + + toolBar + toolBar + + + Data Files + Datafiler + + + Allows to setup data files and directories + Låter dig ställa in datafiler och kataloger + + + Settings + Inställningar + + + Allows to tweak engine settings + Låter dig justera spelmotorinställningar + + + Import + Importera + + + Allows to import data from original engine + Låter dig importera data från originalmotorn + + + Display + Bild + + + Allows to change display settings + Låter dig ändra bildinställningar + + + + QObject + + Select configuration file + Välj konfigurationsfil + + + Select script file + Välj skriptfil + + + + SelectSubdirs + + Select directories you wish to add + Välj kataloger du vill lägga till + + + + SettingsPage + + <html><head/><body><p>Make disposition change of merchants caused by trading permanent.</p></body></html> + <html><head/><body><p>Gör dispositionsändring av handlare orsakad av handel permanent.</p></body></html> + + + <html><head/><body><p>Make player followers and escorters start combat with enemies who have started combat with them or the player. Otherwise they wait for the enemies or the player to do an attack first.</p></body></html> + <html><head/><body><p>Gör att följare och eskorter till spelaren själva påbörjar strid med fiender som påbörjat strid med dem eller spelaren. Annars kommer de vänta tills fienden eller spelaren har attackerat dem först.</p></body></html> + + + <html><head/><body><p>Make Damage Fatigue magic effect uncapped like Drain Fatigue effect.</p><p>This means that unlike Morrowind you will be able to knock down actors using this effect.</p></body></html> + <html><head/><body><p>Gör att "Damage Fatigue"-magieffekten blir obegränsad såsom "Drain Fatigue"-effekten.</p><p>Det innebär att du, till skillnad från i Morrowind, kommer kunna slå ner figurer till marken med denna effekt.</p></body></html> + + + Uncapped Damage Fatigue + Obegränsad "Damage Fatigue" + + + <html><head/><body><p>Stops combat with NPCs affected by Calm spells every frame -- like in Morrowind without the MCP.</p></body></html> + <html><head/><body><p>Avbryter strid med icke-spelbara figurer påverkade av "Calm"-besvjärjelser varje bildruta – såsom i Morrowind utan MCP.</p></body></html> + + + <html><head/><body><p>Make the value of filled soul gems dependent only on soul magnitude.</p></body></html> + <html><head/><body><p>Gör värderingen av fyllda "Soulgems" endast baserad på själens magnitud.</p></body></html> + + + <html><head/><body><p>Makes player swim a bit upward from the line of sight. Applies only in third person mode. Intended to make simpler swimming without diving.</p></body></html> + <html><head/><body><p>Får spelaren att simma något uppåt från siktlinjen. Endast applicerat på tredjepersonsperspektivet. Avsett att göra simning enklare.</p></body></html> + + + <html><head/><body><p>Make stealing items from NPCs that were knocked down possible during combat.</p></body></html> + <html><head/><body><p>Gör det möjligt att stjäla föremål från icke-spelbara figurer som är avsvimmade.</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> + <html><head/><body><p><a name="docs-internal-guid-f375b85a-7fff-02ff-a5af-c5cff63923c0"/>Om aktiv kommer en en navigeringsmesh av världsgeometrin byggas i bakgrunden som används för pathfinding. Om inaktiv kommer endast pathgrid användas för att bygga vägar. Enkelkärnade processorer kan få kraftigt försämrad prestanda. Kan påverka prestandan på flerkärniga processorer något. Flerkärniga processorer kan ha olika fördröjning för att uppdatera navigeringsmesh. Förflyttning mellan externa världar och att gå in eller ut ur en plats producerar en uppdatering av navigeringsmesh. Icke-spelbara figurer kan inte hitta vägar innan navigeringsmesh har skapats runt om dem. Testa att inaktivera denna funktion om du vill ha en mer gammaldags AI som inte vet var den ska gå när du står bakom den där stenen och skjuter ett eldklot.</p></body></html> + + + <html><head/><body><p>If enabled NPCs apply evasion maneuver to avoid collisions with others.</p></body></html> + <html><head/><body><p>Om aktiverat kommer icke-spelbara figurer göra undanmanövrar för att undvika kollisioner med andra.</p></body></html> + + + <html><head/><body><p>Don't use race weight in NPC movement speed calculations.</p></body></html> + <html><head/><body><p>Ta inte med varelsers vikt i beräkningen av hastighet.</p></body></html> + + + <html><head/><body><p>If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor's script instantly.</p><p>If this setting is false, player has to wait until end of death animation in all cases. Makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation.</p></body></html> + <html><head/><body><p>Om denna inställning är aktiv tillåts spelaren plundra figurer (exempelvis tillkallade varelser) under deras dödsanimation, om de inte är i strid.</p><p>Om inställningen är inaktiv måste spelaren vänta tills dödsanimationen är slut. Detta gör det mycket svårare att exploatera tillkallade varelser (exempelvis plundra Draemoror eller Golden Saints för att få dyrbara vapen). Inställningen är i konflikt med skyltdocks-moddar som använder SkipAnim för att förhindra avslutning av dödsanimationer.</p></body></html> + + + <html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html> + <html><head/><body><p>Gör att obeväpnade varelseattacker kan reducera rustningars skick, precis som attacker från icke-spelbara figurer och beväpnade varelser.</p></body></html> + + + Off + Av + + + <html><head/><body><p>How many threads will be spawned to compute physics update in the background. A value of 0 means that the update will be performed in the main thread.</p><p>A value greater than 1 requires the Bullet library be compiled with multithreading support.</p></body></html> + <html><head/><body><p>Antalet exekveringstrådar som kommer användas för att beräkna fysikuppdateringar i bakgrunden.</p><p>Ett värde högre än 1 kräver att Bullet-biblioteket är kompilerat med multithreading-stöd.</p></body></html> + + + Cylinder + Cylinder + + + Visuals + Visuellt + + + Animations + Animationer + + + <html><head/><body><p>Use casting animations for magic items, just as for spells.</p></body></html> + <html><head/><body><p>Använd animationer för magiska föremål, precis som för besvärjelser.</p></body></html> + + + <html><head/><body><p>Makes NPCs and player movement more smooth. Recommended to use with "turn to movement direction" enabled.</p></body></html> + <html><head/><body><p>Gör spelarens och icke-spelbara figurers rörelser mjukare. Rekommenderas att användas tillsammans med "vänd mot rörelseriktningen" aktiverad.</p></body></html> + + + <html><head/><body><p>Load per-group KF-files and skeleton files from Animations folder</p></body></html> + <html><head/><body><p>Ladda per-grupp KF-filer och skelettfiler från Animations-katalogen</p></body></html> + + + <html><head/><body><p>Affects side and diagonal movement. Enabling this setting makes movement more realistic.</p><p>If disabled then the whole character's body is pointed to the direction of view. Diagonal movement has no special animation and causes sliding.</p><p>If enabled then the character turns lower body to the direction of movement. Upper body is turned partially. Head is always pointed to the direction of view. In combat mode it works only for diagonal movement. In non-combat mode it changes straight right and straight left movement as well. Also turns the whole body up or down when swimming according to the movement direction.</p></body></html> + <html><head/><body><p>Påverkar sido- och diagonalförflyttningar. Förflyttningar blir mer realistiska om aktiv.</p><p>Om aktiv kommer spelarrollfiguren vända underkroppen i rikting mot förflyttningen. Överkroppen vänds delvis. Huvudet pekar alltid dit kameran ser. I stidsläge fungerar det bara för diagonal förflyttning. Utanför strid ändrar inställningen förflyttningar rakt höger och vänster också. Inställningen vänder också hela kroppen upp eller ner vid simning enligt förflyttningsriktningen.</p></body></html> + + + <html><head/><body><p>Render holstered weapons (with quivers and scabbards), requires modded assets.</p></body></html> + <html><head/><body><p>Rendera hölstrade vapen (med koger och vapenslidor), kräver moddat innehåll.</p></body></html> + + + <html><head/><body><p>Render holstered shield, requires modded assets.</p></body></html> + <html><head/><body><p>Rendera hölstrade sköldar, kräver moddat innehåll.</p></body></html> + + + <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> + <html><head/><body><p>I tredjepersonsperspektiv kommer kameran svänga efter spelarens förflyttningsanimationer. Detta var det förvalda beteendet i OpenMW 0.48.0 och tidigare.</p></body></html> + + + Shaders + Shader + + + <html><head/><body><p>If this option is enabled, normal maps are automatically recognized and used if they are named appropriately + (see 'normal map pattern', e.g. for a base texture foo.dds, the normal map texture would have to be named foo_n.dds). + If this option is disabled, normal maps are only used if they are explicitly listed within the mesh file (.nif or .osg file). Affects objects.</p></body></html> + <html><head/><body><p>Om funktionen är aktiv kommer normalkartor (normal maps) att hittas och användas automatiskt om de har korrekt namn + (se 'normal map pattern', t.ex. för en bastextur foo.dds ska normalkartan heta foo_n.dds). + Om funktionen är inaktiverad kommer normalkartor bara användas om texturerna är explicit listade i 3D-modell-filen (.nif eller .osg fil). Påverkar objekt.</p></body></html> + + + <html><head/><body><p>See 'auto use object normal maps'. Affects terrain.</p></body></html> + <html><head/><body><p>Se 'autoanvänd normalkartor (normal maps) på objekt'. Påverkar terräng.</p></body></html> + + + <html><head/><body><p>If this option is enabled, specular maps are automatically recognized and used if they are named appropriately + (see 'specular map pattern', e.g. for a base texture foo.dds, + the specular map texture would have to be named foo_spec.dds). + If this option is disabled, normal maps are only used if they are explicitly listed within the mesh file + (.osg file, not supported in .nif files). Affects objects.</p></body></html> + <html><head/><body><p>Om den här funktionen är aktiverad kommer spekularitetskartor (specular maps) att hittas och användas + (see 'specular map pattern', t.ex. för en bastextur foo.dds, + ska spekularitetskartan heta foo_spec.dds). + Om funktionen är inaktiverad kommer normalkartor bara användas om texturerna är explicit listade i 3D-modell-filen + (.nif eller .osg fil). Påverkar objekt.</p></body></html> + + + <html><head/><body><p>If a file with pattern 'terrain specular map pattern' exists, use that file as a 'diffuse specular' map. The texture must contain the layer colour in the RGB channel (as usual), and a specular multiplier in the alpha channel.</p></body></html> + <html><head/><body><p>Om en fil med mönstret 'terrain specular map pattern' finns, använd den filen som en 'diffuse specular'-karta. Texturen måste innehålla färglagren i RGB-kanalerna (som vanligt) och spekularitet i alfakanalen.</p></body></html> + + + <html><head/><body><p>Normally environment map reflections aren't affected by lighting, which makes environment-mapped (and thus bump-mapped objects) glow in the dark. + Morrowind Code Patch includes an option to remedy that by doing environment-mapping before applying lighting, this is the equivalent of that option. + Affected objects will use shaders. + </p></body></html> + <html><head/><body><p>Normalt sett påverkas omgivningskartors reflektioner inte av ljus, vilket gör att omgivningskartor (och således så kallade bump mappade objekt) glöder i mörkret. + Morrowind Code Patch inkluderar en inställning att kringå detta genom att lägga omgivningskartor före ljussättningen. Det här är motsvarigheten till den inställningen. + Påverkade objekt kommer använda shaders. + </p></body></html> + + + <html><head/><body><p>Allows MSAA to work with alpha-tested meshes, producing better-looking edges without pixelation. Can negatively impact performance.</p></body></html> + <html><head/><body><p>Gör att MSAA fungerar med alfa-testade modeller, vilket ger snyggare kanter utan synliga pixlar. Kan påverka prestandan negativt.</p></body></html> + + + <html><head/><body><p>Enables soft particles for particle effects. This technique softens the intersection between individual particles and other opaque geometry by blending between them.</p></body></html> + <html><head/><body><p>Aktiverar mjuka partiklar på partikeleffekter. Denna teknik mjukar upp övergången mellan individuella partiklar och annan ogenomskinlig geometri genom att smälta samman dem.</p></body></html> + + + <html><head/><body><p>Simulate coverage-preserving mipmaps to prevent alpha-tested meshes shrinking as they get further away. Will cause meshes whose textures have coverage-preserving mipmaps to grow, though, so refer to mod installation instructions for how to set this.</p></body></html> + <html><head/><body><p>Simulerar mip maps med coverage-preserving för att förhindra att alfa-testade modeller krymper när de kommer längre bort. Kommer göra att dessa modeller växer istället; se instruktioner i modinstallationen för hur detta ska ställas in.</p></body></html> + + + <html><head/><body><p>EXPERIMENTAL: Stop rain and snow from falling through overhangs and roofs.</p></body></html> + <html><head/><body><p>EXPERIMENTELLT: Förhindra att regn och snö faller genom tak och överhäng.</p></body></html> + + + Fog + Dimma + + + <html><head/><body><p>By default, the fog becomes thicker proportionally to your distance from the clipping plane set at the clipping distance, which causes distortion at the edges of the screen. + This setting makes the fog use the actual eye point distance (or so called Euclidean distance) to calculate the fog, which makes the fog look less artificial, especially if you have a wide FOV.</p></body></html> + <html><head/><body><p>Som standard blir dimman tätare proportionellt med ditt avstånd till avklippningsdistansen, vilket ger distortion vid skärmens kanter. + Denna inställning gör att dimman använder det faktiska ögonpunktsavståndet (så kallat euklidiskt avstånd) för att beräkna dimman, vilket får dimman att se mindre artificiell ut, särskilt vid hög FoV.</p></body></html> + + + <html><head/><body><p>Use exponential fog formula. By default, linear fog is used.</p></body></html> + <html><head/><body><p>Använd exponentiell formel för dimma. Som standard används linjär dimma.</p></body></html> + + + <html><head/><body><p>Reduce visibility of clipping plane by blending objects with the sky.</p></body></html> + <html><head/><body><p>Reducera synligheten av avklippsplanet genom att smälta samman objekt med himmelen.</p></body></html> + + + <html><head/><body><p>The fraction of the maximum distance at which blending with the sky starts.</p></body></html> + <html><head/><body><p>Fraktionen av det maximala avståndet där utsmetning mellan himmel och horisont påbörjas.</p></body></html> + + + Terrain + Terräng + + + <html><head/><body><p>Controls how large an object must be to be visible in the scene. The object’s size is divided by its distance to the camera and the result of the division is compared with this value. The smaller this value is, the more objects you will see in the scene.</p></body></html> + <html><head/><body><p>Bestämmer hur stort ett objekt måste vara för att vara synligt på skärmen. Objektets storlek divideras med sitt avstånd till kameran. Resultatet av denna division jämförs med detta värde. Ju mindre värdet är, desto fler objekt kommer du se i scenen.</p></body></html> + + + <html><head/><body><p>If true, use paging and LOD algorithms to display the entire terrain. If false, only display terrain of the loaded cells.</p></body></html> + <html><head/><body><p>Om aktiverat används paging och LOD-algoritmer för att rita upp all terräng. Om inaktiverat visas endast terrängen i den laddade cellen.</p></body></html> + + + <html><head/><body><p>Use object paging for active cells grid.</p></body></html> + <html><head/><body><p>Använd objekt-paging för aktiva cellers rutnät.</p></body></html> + + + <html><head/><body><p>If this setting is true, supporting models will make use of day night switch nodes.</p></body></html> + <html><head/><body><p>Om den här inställningen är aktiv kommer modeller med stöd för det att använda dag- och natt-bytesnoder.</p></body></html> + + + Post Processing + Efterbehandling + + + <html><head/><body><p>If this setting is true, post processing will be enabled.</p></body></html> + <html><head/><body><p>Om denna inställning är på kommer efterbehandling (post processing) att vara aktiverat.</p></body></html> + + + <html><head/><body><p>Re-render transparent objects with forced alpha clipping.</p></body></html> + <html><head/><body><p>Återrendera transparenta objekt med forcerad alpha clipping.</p></body></html> + + + <html><head/><body><p>Controls how much eye adaptation can change from frame to frame. Smaller values makes for slower transitions.</p></body></html> + <html><head/><body><p>Bestämmer hur mycket ögonanpassningen kan förändras från bildruta till bildruta. Lägre värden ger långsammare övergångar.</p></body></html> + + + Audio + Ljud + + + Select your preferred audio device. + Välj din föredragna ljudenhet. + + + Default + Förvalt + + + This setting controls HRTF, which simulates 3D sound on stereo systems. + Denna inställning kontrollerar HRTF, vilket simulerar 3D-ljud på stereosystem. + + + HRTF + HRTF + + + Automatic + Automatisk + + + On + + + + Select your preferred HRTF profile. + Välj din föredragna HRTF-profil. + + + Interface + Gränssnitt + + + <html><head/><body><p>This setting scales GUI windows. A value of 1.0 results in the normal scale.</p></body></html> + <html><head/><body><p>Denna inställning skalar grafiska fönster i gränssnittet. Ett värde på 1.0 ger den normala skalan.</p></body></html> + + + <html><head/><body><p>Show the remaining duration of magic effects and lights if this setting is true. The remaining duration is displayed in the tooltip by hovering over the magical effect. </p><p>The default value is false.</p></body></html> + <html><head/><body><p>Visar den återstående tiden för magiska effekter och ljus om denna inställning är på. Den återstående tiden visas som en inforuta när muspekaren befinner sig över den magiska effekten. </p><p>Förvalt är av.</p></body></html> + + + <html><head/><body><p>If this setting is true, dialogue topics will have a different color if the topic is specific to the NPC you're talking to or the topic was previously seen. Color can be changed in settings.cfg.</p><p>The default value is false.</p></body></html> + <html><head/><body><p>Om denna inställning är på kommer dialogämnen ha en annan färg om ämnet är specifikt till den icke-spelbara figur du pratar med eller om ämnet redan har setts. Färger kan ändras i settings.cfg.</p><p>Förvalt är av.</p></body></html> + + + Size of characters in game texts. + Storlek på tecken i speltext. + + + <html><head/><body><p>Enable zooming on local and global maps.</p></body></html> + <html><head/><body><p>Aktivera zoomning på lokala och globala kartor.</p></body></html> + + + <html><head/><body><p>If this setting is true, containers supporting graphic herbalism will do so instead of opening the menu.</p></body></html> + <html><head/><body><p>Om denna inställning är på kommer behållare som stödjer grafisk örtplockning (graphic herbalism) använda den funktionen istället för att öppna menyn.</p></body></html> + + + <html><head/><body><p>If this setting is true, damage bonus of arrows and bolts will be shown on item tooltip.</p><p>The default value is false.</p></body></html> + <html><head/><body><p>Om denna inställning är på kommer skadebonus från pilar visas på föremålens inforuta.</p><p>Förvalt är av.</p></body></html> + + + <html><head/><body><p>If this setting is true, melee weapons reach and speed will be shown on item tooltip.</p><p>The default value is false.</p></body></html> + <html><head/><body><p>Om denna inställning är på kommer närstridsvapens räckvidd och hastighet att visas på föremåls inforuta.</p><p>Förvalt är av.</p></body></html> + + + <html><head/><body><p>Stretch menus, load screens, etc. to the window aspect ratio.</p></body></html> + <html><head/><body><p>Sträck ut menyer, laddskärmar o.s.v. till fönstrets aspektratio.</p></body></html> + + + <html><head/><body><p>Whether or not the chance of success will be displayed in the enchanting menu.</p><p>The default value is false.</p></body></html> + <html><head/><body><p>Huruvida chansen att lyckas kommer visas i förtrollningsmenyn.</p><p>Förvalt är av.</p></body></html> + + + <html><head/><body><p>Prevents merchants from equipping items that are sold to them.</p></body></html> + <html><head/><body><p>Förhindrar att handlare tar på sig föremål som säljs till dem.</p></body></html> + + + <html><head/><body><p>Trainers now only choose which skills to train using their base skill points, allowing mercantile improving effects to be used without making mercantile an offered skill.</p></body></html> + <html><head/><body><p>Tränare väljer nu endast de färdigheter som kan tränas genom att använda deras basfärdighetsvärde, vilket tillåter förbättring av merkantil utan att göra merkantil till en erbjuden färdighet.</p></body></html> + + + Miscellaneous + Diverse + + + Saves + Sparfiler + + + <html><head/><body><p>This setting determines whether the amount of the time the player has spent playing will be displayed for each saved game in the Load menu.</p></body></html> + <html><head/><body><p>Denna inställning avgör huruvida mängden tid spelaren har spenderat i spelet kommer visas för varje sparat spel i Ladda spel-menyn.</p></body></html> + + + JPG + JPG + + + PNG + PNG + + + TGA + TGA + + + Testing + Testning + + + These settings are intended for testing mods and will cause issues if used for normal gameplay. + Dessa inställningar är avsedda för att testa moddar och kommer orsaka problem vid normalt spelande. + + + <html><head/><body><p>OpenMW will capture control of the cursor if this setting is true.</p><p>In “look mode”, OpenMW will center the cursor regardless of the value of this setting (since the cursor/crosshair is always centered in the OpenMW window). However, in GUI mode, this setting determines the behavior when the cursor is moved outside the OpenMW window. If true, the cursor movement stops at the edge of the window preventing access to other applications. If false, the cursor is allowed to move freely on the desktop.</p><p>This setting does not apply to the screen where escape has been pressed, where the cursor is never captured. Regardless of this setting “Alt-Tab” or some other operating system dependent key sequence can be used to allow the operating system to regain control of the mouse cursor. This setting interacts with the minimize on focus loss setting by affecting what counts as a focus loss. Specifically on a two-screen configuration it may be more convenient to access the second screen with setting disabled.</p><p>Note for developers: it’s desirable to have this setting disabled when running the game in a debugger, to prevent the mouse cursor from becoming unusable when the game pauses on a breakpoint.</p></body></html> + <html><head/><body><p>OpenMW kommer ta kontroll av muspekaren om denna inställning är aktiverad.</p><p>I ”tittläge” kommer OpenMW centrera muspekaren oavsett värdet på denna inställning (eftersom muspekaren/hårkorset alltid är centrerat i OpenMW-fönstret). I gränssnittsläge däremot kommer denna inställning bedöma beteendet när muspekaren flyttas utanför OpenMW-fönstret. Om på kommer muspekarrörelsen stanna vid kanten av fönstret, vilket förhindrar tillgång till andra applikationer. Om av tillåts muspekaren att röras fritt över skrivbordet.</p><p>Denna inställning appliceras inte på skärmen där Escape har blivit tryckt, då muspekaren aldrig tas över. Oavsett denna inställning kan ”Alt-Tab” eller annan operativsystemberoende knappsekvens användas för att ge operativsystemet åter tillgång till muspekaren. Denna inställning interagerar med minimera vid fokusförlust-inställningen genom att påverka vad som räknas som en fokusförlust. Specifikt på en tvåskärmskonfiguration kan det vara mer smidigt att få tillgång till den andra skärmen med inställningen inaktiverad.</p><p>Notis för utvecklare: det är önskvärt att ha denna inställning inaktiverad när OpenMW körs i debug-läge för att förhindra att musen blir oanvändbar när spelet pausar vid en brytpunkt.</p></body></html> + + + Browse… + Bläddra… + + + Collision is used for both physics simulation and navigation mesh generation for pathfinding. Cylinder gives the best consistency between available navigation paths and ability to move by them. Changing this value affects navigation mesh generation therefore navigation mesh disk cache generated for one value will not be useful with another. + Kollision används för både fysiksimulering och navigationsmeshgenerering för pathfinding. Cylinder ger bäst förenlighet mellan tillgängliga navigeringsvägar och möjlighet att förflytta förbi dem. Ändring av detta värde påverkar navigeringsmeshgenereringen – därför kommer inte navigeringsmesh disk cache för ett värde vara användbart för ett annat. + + + Gameplay + Spelmekanik + + + <html><head/><body><p>If enabled, a magical ammunition is required to bypass normal weapon resistance or weakness. If disabled, a magical ranged weapon or a magical ammunition is required.</p></body></html> + <html><head/><body><p>Om aktiverad kommer magisk ammunition krävas för att förbigå normal vapenmotståndskraft eller -svaghet. Om inaktiverad krävs ett magiskt avståndsvapen eller en magisk ammunition.</p></body></html> + + + <html><head/><body><p>Make enchanted weapons without Magical flag bypass normal weapons resistance, like in Morrowind.</p></body></html> + <html><head/><body><p>Få förtrollade vapen utan Magisk-flagga att förbigå normal vapenmotståndskraft, såsom i Morrowind.</p></body></html> + + + cells + celler + + + Shadows + Skuggor + + + <html><head/><body><p>Type of "compute scene bounds" computation method to be used. Bounds (default) for good balance between performance and shadow quality, primitives for better looking shadows or none for no computation.</p></body></html> + <html><head/><body><p>Typ av "compute scene bounds" beräkningsmetod att använda. Bounds (förvalt) för en bra balans mellan prestanda och skuggkvalitet, primitives för snyggare skuggor eller none för ingen beräkning alls.</p></body></html> + + + <html><head/><body><p>64 game units is 1 real life yard or about 0.9 m</p></body></html> + <html><head/><body><p>64 spelenheter är 1 yard eller ungefär 0,9 meter i verkligheten</p></body></html> + + + unit(s) + enhet(er) + + + <html><head/><body><p>Enable shadows for NPCs and creatures besides the player character. May have a minor performance impact.</p></body></html> + <html><head/><body><p>Aktiverar skuggor för icke-spelbara figurer och varelser bortsett från spelarrollfiguren. Kan ha en liten negativ prestandapåverkan.</p></body></html> + + + 512 + 512 + + + 1024 + 1024 + + + 2048 + 2048 + + + 4096 + 4096 + + + <html><head/><body><p>The fraction of the limit above at which shadows begin to gradually fade away.</p></body></html> + <html><head/><body><p>Den fraktion av gränsen ovan vid vilken skuggor gradvis börjar blekna bort.</p></body></html> + + + <html><head/><body><p>Enable shadows exclusively for the player character. May have a very minor performance impact.</p></body></html> + <html><head/><body><p>Aktivera skuggor exklusivt för spelarrollfiguren. Kan ha en mycket liten negativ prestandapåverkan.</p></body></html> + + + <html><head/><body><p>The resolution of each individual shadow map. Increasing it significantly improves shadow quality but may have a minor performance impact.</p></body></html> + <html><head/><body><p>Upplösningen för varje individuell skuggkarta. Ökning av den ökar skuggkvalitén signifikant, men kan ha en lättare negativ prestandapåverkan.</p></body></html> + + + <html><head/><body><p>The distance from the camera at which shadows completely disappear.</p></body></html> + <html><head/><body><p>Avståndet från kameran vid vilken skuggor helt försvinner.</p></body></html> + + + <html><head/><body><p>Enable shadows for primarily inanimate objects. May have a significant performance impact.</p></body></html> + <html><head/><body><p>Aktivera skuggor för huvudsakligen icke-rörliga objekt. Kan ha en signifikant negativ prestandapåverkan.</p></body></html> + + + <html><head/><body><p>Due to limitations with Morrowind's data, only actors can cast shadows indoors, which some might feel is distracting.</p><p>Has no effect if actor/player shadows are not enabled.</p></body></html> + <html><head/><body><p>På grund av begränsningar med Morrowinds data kan endast figurer ge ifrån sig skuggor inomhus, vilket vissa kan tycka vara distraherande.</p><p>Har ingen effekt om figur/spelarskuggor inte är aktiverat.</p></body></html> + + + <html><head/><body><p>Enable shadows for the terrain including distant terrain. May have a significant performance and shadow quality impact.</p></body></html> + <html><head/><body><p>Aktivera skuggor för terräng, inklusive avlägsen terräng. Kan ha en signifikant negativ påverkan på prestanda och skuggkvalité.</p></body></html> + + + Lighting + Ljussättning + + + Tooltip + Inforuta + + + Crosshair + Hårkors + + + Screenshots + Skärmdumpar + + + <html><head/><body><p>Give actors an ability to swim over the water surface when they follow other actor independently from their ability to swim. Has effect only when nav mesh building is enabled.</p></body></html> + <html><head/><body><p>Ge figurer en möjlighet att simma över vattenytan när de följer andra figurer, oberoende av deras förmåga att simma. Har endast effekt när navigeringsmesh är aktiverat.</p></body></html> + + + <html><head/><body><p>Effects of reflected Absorb spells are not mirrored - like in Morrowind.</p></body></html> + <html><head/><body><p>Effekter av reflekterade "Absorb"-besvärjelser speglas inte – såsom i Morrowind.</p></body></html> + + + <html><head/><body><p>Maximum distance at which lights will appear (measured in units).</p><p>Set this to 0 to use an unlimited distance.</p></body></html> + <html><head/><body><p>Maximala avståndet där ljuskällor syns (mätt i enheter).</p><p>Värdet 0 ger oändligt avstånd.</p></body></html> + + + <html><head/><body><p>Maximum number of lights per object.</p><p>A low number near default will cause light popping similar to what you would see with legacy lighting.</p></body></html> + <html><head/><body><p>Maximalt antal ljuskällor per objekt.</p><p>Ett lågt tal nära det förvalda kommer orsaka att ljuskällor poppar upp som vid ljussättningsmetoden Gammaldags.</p></body></html> + + + <html><head/><body><p>Fraction of maximum distance at which lights will start to fade.</p><p>Set this to a low value for slower transitions or a high value for quicker transitions.</p></body></html> + <html><head/><body><p>Fraktion av det maximala avståndet från vilket ljuskällor börjar blekna.</p><p>Välj ett lågt värde för långsammare övergång eller högre värde för snabbare övergång.</p></body></html> + + + <html><head/><body><p>Set the internal handling of light sources.</p> +<p> "Legacy" always uses 8 lights per object and provides a lighting closest to an original game.</p> +<p>"Shaders (compatibility)" removes the 8 light limit. This mode also enables lighting on groundcover and a configurable light fade. It is recommended to use this with older hardware and a light limit closer to 8.</p> +<p> "Shaders" carries all of the benefits that "Shaders (compatibility)" does, but uses a modern approach that allows for a higher max lights count with little to no performance penalties on modern hardware.</p></body></html> + <html><head/><body><p>Välj intern hantering av ljuskällor.</p> +<p> "Gammaldags" använder alltid max 8 ljuskällor per objekt och ger ljussättning likt ett gammaldags spel.</p> +<p>"Shader (kompatibilitet)" tar bort begränsningen med max 8 ljuskällor per objekt. Detta läge aktiverar också ljus på marktäckning och ett konfigurerbart ljusbleknande. Rekommenderas för äldre hårdvara tillsammans med en ljusbegränsning nära 8.</p> +<p> "Shader" har alla fördelar som "Shader (kompatibilitet)" har, men med ett modernt förhållningssätt som möjliggör fler maximalt antal ljuskällor med liten eller ingen prestandaförlust på modern hårdvara.</p></body></html> + + + Legacy + Gammaldags + + + Shaders (compatibility) + Shader (kompatibilitet) + + + <html><head/><body><p>Multipler for bounding sphere of lights.</p><p>Higher numbers allows for smooth falloff but require an increase in number of max lights.</p><p>Does not effect the illumination or strength of lights.</p></body></html> + <html><head/><body><p>Multiplikator för ljusens gränssfär.</p><p>Högre värden ger mjukare minskning av gränssfären, men kräver högre värde i Max antal ljuskällor.</p><p>Påverkar inte ljusstyrkan.</p></body></html> + + + <html><head/><body><p>Minimum ambient interior brightness.</p><p>Increase this if you feel interiors are too dark.</p></body></html> + <html><head/><body><p>Minsta omgivande ljusstyrka i interiörer.</p><p>Öka värdet om du anser att interiörer är för mörka.</p></body></html> + + + In third-person view, use the camera as the sound listener instead of the player character. + Använd kameran som ljudlyssnare istället för spelarrollfiguren i tredjepersonsperspektivet. + + + Permanent Barter Disposition Changes + Permanenta handelsförändringar + + + Classic Calm Spells Behavior + Klassiskt beteende för "Calm"-besvärjelser + + + NPCs Avoid Collisions + Icke-spelbara figurer undviker kollisioner + + + Soulgem Values Rebalance + Ombalansering av "Soulgems" värdering + + + Day Night Switch Nodes + Dag/natt-bytesnoder + + + Followers Defend Immediately + Följare försvarar omedelbart + + + Only Magical Ammo Bypass Resistance + Bara magisk ammunition förbigår motstånd + + + Graphic Herbalism + Grafisk örtplockning + + + Swim Upward Correction + Simma uppåt korrigering + + + Enchanted Weapons Are Magical + Förtrollade vapen är magiska + + + Merchant Equipping Fix + Handlare klär inte på sig + + + Can Loot During Death Animation + Kan plundra under dödsanimation + + + Classic Reflected Absorb Spells Behavior + Klassiskt beteende för reflekterade "Absorb"-besvärjelser + + + Unarmed Creature Attacks Damage Armor + Obeväpnad attack från varelser skadar rustning + + + Affect Werewolves + Påverka varulvar + + + Do Not Affect Werewolves + Påverka inte varulvar + + + Background Physics Threads + Fysiktrådar i bakgrunden + + + Actor Collision Shape Type + Figurers kollisionsformtyp + + + Axis-Aligned Bounding Box + Axeljusterad begränsningslåda + + + Rotating Box + Roterande låda + + + Smooth Movement + Mjuka rörelser + + + Use Additional Animation Sources + Använd extra animationskällor + + + Weapon Sheathing + Vapenhölstring + + + Shield Sheathing + Sköldhölstring + + + Player Movement Ignores Animation + Spelarförflyttningar ignorerar animation + + + Use Magic Item Animation + Animationer för magiska föremål + + + Auto Use Object Normal Maps + Använd automatiskt normalkartor på objekt + + + Soft Particles + Mjuka partiklar + + + Auto Use Object Specular Maps + Använd automatiskt spekularitetskartor på objekt + + + Auto Use Terrain Normal Maps + Använd automatiskt normalkartor på terräng + + + Auto Use Terrain Specular Maps + Använd automatiskt spekularitetskartor på terräng + + + Use Anti-Aliased Alpha Testing + Använd kantutjämnad alfa-testning + + + Bump/Reflect Map Local Lighting + Bump/Reflektionskartor lokalt ljus + + + Weather Particle Occlusion + Regn/snö blockeras av tak + + + Exponential Fog + Exponentiell dimma + + + Radial Fog + Radiell dimma + + + Sky Blending Start + Smeta ut horisont/himmel, start + + + Sky Blending + Smeta ut horisont/himmel + + + Object Paging Min Size + Object paging needs a better translation + Minsta storlek för objektpaging + + + Viewing Distance + Siktavstånd + + + Distant Land + Avlägsen terräng + + + Active Grid Object Paging + Object paging needs a better translation + Objektpaging i aktivt rutnät + + + Transparent Postpass + Will return to later + + + + Auto Exposure Speed + Autoexponeringshastighet + + + Enable Post Processing + Aktivera efterbehandling (post processing) + + + Shadow Planes Computation Method + Skuggplaner beräkningsmetod + + + Enable Actor Shadows + Aktivera rollfigurskuggor + + + Fade Start Multiplier + Blekningsstartmultiplikator + + + Enable Player Shadows + Aktivera spelarskuggor + + + Shadow Map Resolution + Skuggkartsupplösning + + + Shadow Distance Limit: + Skuggavståndsgräns: + + + Enable Object Shadows + Aktivera objektskuggor + + + Enable Indoor Shadows + Aktivera interiöra skuggor + + + Enable Terrain Shadows + Aktivera terrängskuggor + + + Maximum Light Distance + Maximalt ljusavstånd + + + Max Lights + Max antal ljuskällor + + + Lighting Method + Ljussättningsmetod + + + Bounding Sphere Multiplier + Gränssfärsmultiplikator + + + Minimum Interior Brightness + Minsta ljusstyrka i interiörer + + + Audio Device + Ljudenhet + + + HRTF Profile + HRTF-profil + + + Tooltip and Crosshair + Inforuta och hårkors + + + GUI Scaling Factor + Skalningsfaktor för gränssnitt + + + Show Effect Duration + Visa effektvaraktighet + + + Change Dialogue Topic Color + Ändra färg på dialogämnen + + + Font Size + Fontstorlek + + + Show Projectile Damage + Visa projektilskada + + + Show Melee Info + Visa närstridsinfo + + + Stretch Menu Background + Sträck ut menybakgrund + + + Show Owned Objects + Visa ägda objekt + + + Show Enchant Chance + Visa chans för "Enchant" + + + Maximum Quicksaves + Max snabbsparfiler + + + Screenshot Format + Skärmdumpformat + + + Grab Cursor + Ta över muspekaren + + + Default Cell + Förinställd cell + + + Bounds + Bounds + + + Primitives + Primitives + + + None + None + + + Always Allow Actors to Follow over Water + Tillåt alltid figurer att följa över vatten + + + Racial Variation in Speed Fix + Fix för varelsers hastighetsvariation + + + Use Navigation Mesh for Pathfinding + Använd navigeringsmesh för pathfinding + + + Trainers Choose Offered Skills by Base Value + Tränare väljer erbjudna färdigheter efter grundvärde + + + Steal from Knocked out Actors in Combat + Stjäl från avsvimmade rollfigurer i strid + + + Factor Strength into Hand-to-Hand Combat + Räkna in styrka i obeväpnad strid + + + Turn to Movement Direction + Vänd mot rörelseriktningen + + + Adjust Coverage for Alpha Test + Justera täckning för alfa-testning + + + Use the Camera as the Sound Listener + Använd kameran som ljudlyssnaren + + + Can Zoom on Maps + Kan zooma på kartor + + + Add "Time Played" to Saves + Lägg till spelad tid i sparfiler + + + Notify on Saved Screenshot + Ge notis vid sparad skärmdump + + + Skip Menu and Generate Default Character + Hoppa över meny och generera förinställd rollfigur + + + Start Default Character at + Starta förinställd rollfigur vid + + + Run Script After Startup: + Kör skript efter uppstart: + + + diff --git a/files/lang/wizard_de.ts b/files/lang/wizard_de.ts index 33c7a5fb53..c3ef55b04a 100644 --- a/files/lang/wizard_de.ts +++ b/files/lang/wizard_de.ts @@ -128,10 +128,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Where should Morrowind be installed? - - <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> - - Morrowind will be installed to the following location. @@ -170,10 +166,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov What is the language of the Morrowind installation? - - <html><head/><body><p><img src=":/icons/tango/48x48/preferences-desktop-locale.png"/></p></body></html> - - Select the language of the Morrowind installation. @@ -197,10 +189,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Retail CD/DVD - - <html><head/><body><p><img src=":/icons/tango/48x48/system-installer.png"/></p></body></html> - - Install from a retail disc to a new location. @@ -209,10 +197,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Existing Installation - - <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> - - Select an existing installation. @@ -221,10 +205,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Don't have a copy? - - <html><head/><body><p><img src=":/icons/tango/48x48/dollar.png"/></p></body></html> - - Buy the game diff --git a/files/lang/wizard_fr.ts b/files/lang/wizard_fr.ts index 6c655885d2..010883d0c4 100644 --- a/files/lang/wizard_fr.ts +++ b/files/lang/wizard_fr.ts @@ -128,10 +128,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Where should Morrowind be installed? À quel emplacement Morrowind doit-il être installé ? - - <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> - <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> - Morrowind will be installed to the following location. Morrowind sera installé à l'emplacement suivant : @@ -170,10 +166,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov What is the language of the Morrowind installation? Dans quelle langue est cette installation de Morrowind ? - - <html><head/><body><p><img src=":/icons/tango/48x48/preferences-desktop-locale.png"/></p></body></html> - <html><head/><body><p><img src=":/icons/tango/48x48/preferences-desktop-locale.png"/></p></body></html> - Select the language of the Morrowind installation. Sélectionnez la langue de cette installation de Morrowind. @@ -197,10 +189,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Retail CD/DVD Copie CD/DVD physique - - <html><head/><body><p><img src=":/icons/tango/48x48/system-installer.png"/></p></body></html> - <html><head/><body><p><img src=":/icons/tango/48x48/system-installer.png"/></p></body></html> - Install from a retail disc to a new location. Installer depuis un CD/DVD et choisir la destination sur l'ordinateur. @@ -209,10 +197,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Existing Installation Installation existante - - <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> - <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> - Select an existing installation. Sélectionnez une installation existante. @@ -221,10 +205,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Don't have a copy? Vous n'avez pas de copie du jeu ? - - <html><head/><body><p><img src=":/icons/tango/48x48/dollar.png"/></p></body></html> - <html><head/><body><p><img src=":/icons/tango/48x48/dollar.png"/></p></body></html> - Buy the game Achetez le jeu. diff --git a/files/lang/wizard_ru.ts b/files/lang/wizard_ru.ts index 5784b11eac..ba9815f634 100644 --- a/files/lang/wizard_ru.ts +++ b/files/lang/wizard_ru.ts @@ -130,10 +130,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Where should Morrowind be installed? Куда нужно установить Morrowind? - - <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> - <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> - Morrowind will be installed to the following location. Morrowind будет установлен в следующее место. @@ -172,10 +168,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov What is the language of the Morrowind installation? Какой язык использует ваша копия Morrowind? - - <html><head/><body><p><img src=":/icons/tango/48x48/preferences-desktop-locale.png"/></p></body></html> - ><html><head/><body><p><img src=":/icons/tango/48x48/preferences-desktop-locale.png"/></p></body></html> - Select the language of the Morrowind installation. Выберите язык, используемый вашей копией Morrowind. @@ -199,10 +191,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Retail CD/DVD CD/DVD-диск - - <html><head/><body><p><img src=":/icons/tango/48x48/system-installer.png"/></p></body></html> - <html><head/><body><p><img src=":/icons/tango/48x48/system-installer.png"/></p></body></html> - Install from a retail disc to a new location. Установить игру с диска. @@ -211,10 +199,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Existing Installation Установленная копия игры - - <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> - <html><head/><body><p><img src=":/icons/tango/48x48/folder.png"/></p></body></html> - Select an existing installation. Выбрать установленную копию игры. @@ -223,10 +207,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Don't have a copy? Нет копии игры? - - <html><head/><body><p><img src=":/icons/tango/48x48/dollar.png"/></p></body></html> - <html><head/><body><p><img src=":/icons/tango/48x48/dollar.png"/></p></body></html> - Buy the game Купить игру diff --git a/files/lang/wizard_sv.ts b/files/lang/wizard_sv.ts new file mode 100644 index 0000000000..099a95d128 --- /dev/null +++ b/files/lang/wizard_sv.ts @@ -0,0 +1,658 @@ + + + + + ComponentSelectionPage + + WizardPage + WizardPage + + + Select Components + Välj komponenter + + + Which components should be installed? + Vilka komponenter ska installeras? + + + <html><head/><body><p>Select which official Morrowind expansions should be installed. For best results, it is recommended to have both expansions installed.</p><p><span style=" font-weight:bold;">Note:</span> It is possible to install expansions later by re-running this Wizard.<br/></p></body></html> + <html><head/><body><p>Välj vilka officiella Morrowindexpansioner som ska installeras. För bäst resultat rekommenderas att båda expansionerna installeras.</p><p><span style=" font-weight:bold;">Note:</span> Det är möjligt att installera expansioner senare genom att köra denna guide igen.<br/></p></body></html> + + + Selected components: + Valda komponenter: + + + + ConclusionPage + + WizardPage + WizardPage + + + Completing the OpenMW Wizard + Färdigställer OpenMWs installationsguide + + + Placeholder + Placeholder + + + + ExistingInstallationPage + + WizardPage + WizardPage + + + Select Existing Installation + Välj befintlig installation + + + Select an existing installation for OpenMW to use or modify. + Välj en befintlig installation som OpenMW kan använda eller modifiera. + + + Detected installations: + Hittade installationer: + + + Browse... + Bläddra... + + + + ImportPage + + WizardPage + WizardPage + + + Import Settings + Importera inställningar + + + Import settings from the Morrowind installation. + Importera inställningar från Morrowindinstallationen. + + + <html><head/><body><p>OpenMW needs to import settings from the Morrowind configuration file in order to function properly.</p><p><span style=" font-weight:bold;">Note:</span> It is possible to import settings later by re-running this Wizard.</p><p/></body></html> + <html><head/><body><p>OpenMW behöver importera inställningar från Morrowinds konfigurationsfil för att fungera korrekt.</p><p><span style=" font-weight:bold;">Notera:</span> Det är möjligt att importera inställningarna senare genom att köra denna guide igen.</p><p/></body></html> + + + Import Settings From Morrowind.ini + Importera inställningar från Morrowind.ini + + + Import Add-on and Plugin Selection + Importera tillägg- och pluginmarkering + + + Import Bitmap Fonts Setup From Morrowind.ini + Importera bitmapfonter från Morrowind.ini + + + Fonts shipped with the original engine are blurry with UI scaling and support only a small amount of characters, +so OpenMW provides another set of fonts to avoid these issues. These fonts use TrueType technology and are quite similar +to default Morrowind fonts. Check this box if you still prefer original fonts over OpenMW ones or if you use custom bitmap fonts. + Fonter som kommer med ordinarie spelmotor är suddiga med gränssnittsuppskalning och stödjer bara ett litet antal tecken, +så OpenMW tillhandahåller därför andra fonter för att undvika dessa problem. Dessa fonter använder TrueType-teknologi och är ganska lika +de ordinarie fonterna i Morrowind. Bocka i denna ruta om du ändå föredrar ordinarie fonter istället för OpenMWs, alternativt om du använder egna bitmapfonter. + + + + InstallationPage + + WizardPage + WizardPage + + + Installing + Installerar + + + Please wait while Morrowind is installed on your computer. + Vänta medan Morrowind installeras på din dator. + + + + InstallationTargetPage + + WizardPage + WizardPage + + + Select Installation Destination + Välj installationsplats + + + Where should Morrowind be installed? + Var ska Morrowind installeras? + + + Morrowind will be installed to the following location. + Morrowind kommer installeras på följande plats. + + + Browse... + Bläddra... + + + + IntroPage + + WizardPage + WizardPage + + + Welcome to the OpenMW Wizard + Välkommen till OpenMWs installationsguide + + + This Wizard will help you install Morrowind and its add-ons for OpenMW to use. + Denna guide hjälper dig att installera Morrowind och expansionerna för OpenMW. + + + + LanguageSelectionPage + + WizardPage + WizardPage + + + Select Morrowind Language + Välj Morrowinds språk + + + What is the language of the Morrowind installation? + Vad är språket på Morrowindinstallationen? + + + Select the language of the Morrowind installation. + Välj språket på Morrowindinstallationen. + + + + MethodSelectionPage + + WizardPage + WizardPage + + + Select Installation Method + Välj installationsmetod + + + <html><head/><body><p>Select how you would like to install <i>The Elder Scrolls III: Morrowind</i>.</p></body></html> + <html><head/><body><p>Välj hur du vill installera <i>The Elder Scrolls III: Morrowind</i>.</p></body></html> + + + Retail CD/DVD + Köpt CD/DVD + + + Install from a retail disc to a new location. + Installera från en köpt skiva till en ny plats. + + + Existing Installation + Befintlig installation + + + Select an existing installation. + Välj en befintlig installation. + + + Don't have a copy? + Äger du inte spelet? + + + Buy the game + Köp spelet + + + + QObject + + <br><b>Could not find Morrowind.ini</b><br><br>The Wizard needs to update settings in this file.<br><br>Press "Browse..." to specify the location manually.<br> + <br><b>Kunde inte hitta Morrowind.ini</b><br><br>Guiden behöver uppdatera inställningarna i denna fil.<br><br>Tryck på "Bläddra..." för att specificera filens plats manuellt.<br> + + + B&rowse... + B&läddra... + + + Select configuration file + Välj konfigurationsfil + + + <b>Morrowind.bsa</b> is missing!<br>Make sure your Morrowind installation is complete. + <b>Morrowind.bsa</b> saknas!<br>Se till att din Morrowindinstallation är komplett. + + + <br><b>There may be a more recent version of Morrowind available.</b><br><br>Do you wish to continue anyway?<br> + <br><b>Det kan finnas nyare version av Morrowind tillgänglig.</b><br><br>Vill du fortsätta ändå?<br> + + + Most recent Morrowind not detected + Senaste versionen av Morrowind hittades inte + + + Select a valid %1 installation media.<br><b>Hint</b>: make sure that it contains at least one <b>.cab</b> file. + Välj ett giltigt %1 installationsmedium.<br><b>Tips</b>: säkerställ att det finns åtminstone en <b>.cab</b>-fil. + + + There may be a more recent version of Morrowind available.<br><br>Do you wish to continue anyway? + Det kan finnas en mer uppdaterad version av Morrowind tillgänglig.<br><br>Vill du fortsätta ändå? + + + + Wizard::ComponentSelectionPage + + &Install + &Installera + + + &Skip + &Hoppa över + + + Morrowind (installed) + Morrowind (installerat) + + + Morrowind + Morrowind + + + Tribunal (installed) + Tribunal (installerat) + + + Tribunal + Tribunal + + + Bloodmoon (installed) + Bloodmoon (installerat) + + + Bloodmoon + Bloodmoon + + + About to install Tribunal after Bloodmoon + På väg att installera Tribunal efter Bloodmoon + + + <html><head/><body><p><b>You are about to install Tribunal</b></p><p>Bloodmoon is already installed on your computer.</p><p>However, it is recommended that you install Tribunal before Bloodmoon.</p><p>Would you like to re-install Bloodmoon?</p></body></html> + <html><head/><body><p><b>Du håller på att installera Tribunal</b></p><p>Bloodmoon finns redan installerat på din dator.</p><p>Det är dock rekommenderat att du installerar Tribunal före Bloodmoon.</p><p>Vill du ominstallera Bloodmoon?</p></body></html> + + + Re-install &Bloodmoon + Ominstallera &Bloodmoon + + + + Wizard::ConclusionPage + + <html><head/><body><p>The OpenMW Wizard successfully installed Morrowind on your computer.</p></body></html> + <html><head/><body><p>OpenMWs Installationsguide har installerat Morrowind på din dator.</p></body></html> + + + <html><head/><body><p>The OpenMW Wizard successfully modified your existing Morrowind installation.</body></html> + <html><head/><body><p>OpenMWs installationsguide har justerat din befintliga Morrowindinstallation.</body></html> + + + <html><head/><body><p>The OpenMW Wizard failed to install Morrowind on your computer.</p><p>Please report any bugs you might have encountered to our <a href="https://gitlab.com/OpenMW/openmw/issues">bug tracker</a>.<br/>Make sure to include the installation log.</p><br/></body></html> + <html><head/><body><p>OpenMWs installationsguide misslyckades med att installera Morrowind på din dator.</p><p>Vänligen rapportera eventuella buggar på vår <a href="https://gitlab.com/OpenMW/openmw/issues">bug tracker</a>.<br/>Se till att du inkluderar installationsloggen.</p><br/></body></html> + + + + Wizard::ExistingInstallationPage + + No existing installations detected + Inga befintliga installationer hittades + + + Error detecting Morrowind configuration + Kunde inte hitta Morrowind-konfigurationsfil + + + Morrowind configuration file (*.ini) + Morrowind konfigurationsfil (*.ini) + + + Select Morrowind.esm (located in Data Files) + Markera Morrowind.esm (hittas i Data Files) + + + Morrowind master file (Morrowind.esm) + Morrowind masterfil (Morrowind.esm) + + + Error detecting Morrowind files + Kunde inte hitta Morrowindfiler + + + + Wizard::InstallationPage + + <p>Attempting to install component %1.</p> + <p>Försöker installera komponent %1.</p> + + + Attempting to install component %1. + Försöker installera komponent %1. + + + %1 Installation + %1 Installation + + + Select %1 installation media + Välj %1 installationsmedia + + + <p><br/><span style="color:red;"><b>Error: The installation was aborted by the user</b></span></p> + <p><br/><span style="color:red;"><b>Fel: Installationen avbröts av användaren</b></span></p> + + + <p>Detected old version of component Morrowind.</p> + <p>Hittade gammal version av Morrowind.</p> + + + Detected old version of component Morrowind. + Hittade gammal version av komponenten Morrowind. + + + Morrowind Installation + Installation av Morrowind + + + Installation finished + Installationen klar + + + Installation completed successfully! + Installationen slutfördes! + + + Installation failed! + Installationen misslyckades! + + + <p><br/><span style="color:red;"><b>Error: %1</b></p> + <p><br/><span style="color:red;"><b>Fel: %1</b></p> + + + <p><span style="color:red;"><b>%1</b></p> + <p><span style="color:red;"><b>%1</b></p> + + + <html><head/><body><p><b>The Wizard has encountered an error</b></p><p>The error reported was:</p><p>%1</p><p>Press &quot;Show Details...&quot; for more information.</p></body></html> + <html><head/><body><p><b>Guiden har stött på ett fel</b></p><p>Felet som rapporterades var:</p><p>%1</p><p>Tryck på &quot;Visa detaljer...&quot; för mer information.</p></body></html> + + + An error occurred + Ett fel uppstod + + + + Wizard::InstallationTargetPage + + Error creating destination + Fel när målkatalogen skulle skapas + + + <html><head/><body><p><b>Could not create the destination directory</b></p><p>Please make sure you have the right permissions and try again, or specify a different location.</p></body></html> + <html><head/><body><p><b>Kunde inte skapa målkatalogen</b></p><p>Se till att du har rätt behörigheter och försök igen, eller specificera en annan katalog.</p></body></html> + + + <html><head/><body><p><b>Could not write to the destination directory</b></p><p>Please make sure you have the right permissions and try again, or specify a different location.</p></body></html> + <html><head/><body><p><b>Kunde inte skriva till målkatalogen</b></p><p>Se till att du har rätt behörigheter och försök igen, eller specificera en annan katalog.</p></body></html> + + + <html><head/><body><p><b>The destination directory is not empty</b></p><p>An existing Morrowind installation is present in the specified location.</p><p>Please specify a different location, or go back and select the location as an existing installation.</p></body></html> + <html><head/><body><p><b>Målkatalogen är inte tom</b></p><p>Det finns en befintlig Morrowindinstallation i den specificerade katalogen.</p><p>Välj en annan katalog eller gå tillbaka och välj katalogen som en befintlig installation.</p></body></html> + + + Insufficient permissions + Otillräckliga behörigheter + + + Destination not empty + Platsen är inte tom + + + Select where to install Morrowind + Välj där Morrowind ska installeras + + + + Wizard::LanguageSelectionPage + + English + Engelska + + + French + Franska + + + German + Tyska + + + Italian + Italienska + + + Polish + Polska + + + Russian + Ryska + + + Spanish + Spanska + + + + Wizard::MainWizard + + OpenMW Wizard + OpenMW installationsguide + + + Error opening Wizard log file + Kunde inte öppna guidens loggfil + + + <html><head/><body><p><b>Could not open %1 for writing</b></p><p>Please make sure you have the right permissions and try again.</p></body></html> + <html><head/><body><p><b>Kunde inte öppna %1 för att skriva</b></p><p>Se till att du har rätt behörigheter och försök igen.</p></body></html> + + + <html><head/><body><p><b>Could not open %1 for reading</b></p><p>Please make sure you have the right permissions and try again.</p></body></html> + <html><head/><body><p><b>Kunde inte öppna %1 för läsning</b></p><p>Se till att du har rätt behörigheter och försök igen.</p></body></html> + + + Error opening OpenMW configuration file + Fel när OpenMW-konfigurationsfil skulle öppnas + + + Quit Wizard + Avsluta guiden + + + Are you sure you want to exit the Wizard? + Är du säker på att du vill avsluta guiden? + + + Error creating OpenMW configuration directory + Fel vid skapande av OpenMW-konfigurationskatalog + + + <html><head/><body><p><b>Could not create %1</b></p><p>Please make sure you have the right permissions and try again.</p></body></html> + <html><head/><body><p><b>Kunde inte skapa %1</b></p><p>Se till att du har rätt behörigheter och försök igen.</p></body></html> + + + Error writing OpenMW configuration file + Kunde inte skriva OpenMW-konfigurationsfil + + + + Wizard::UnshieldWorker + + Failed to open Morrowind configuration file! + Kunde inte öppna en Morrowind-konfigurationsfil! + + + Opening %1 failed: %2. + Öppnar %1 misslyckad: %2. + + + Failed to write Morrowind configuration file! + Kunde inte skriva en Morrowind-konfigurationsfil! + + + Writing to %1 failed: %2. + Skriver till %1 misslyckad: %2. + + + Installing: %1 + Installerar: %1 + + + Installing: %1 directory + Installerar: %1 katalog + + + Installation finished! + Installationen slutförd! + + + Component parameter is invalid! + Komponentparametern är ogiltig! + + + An invalid component parameter was supplied. + En ogiltig komponentparameter angavs. + + + Failed to find a valid archive containing %1.bsa! Retrying. + Misslyckades att hitta ett giltigt arkiv som innehåller %1.bsa! Försöker igen. + + + Installing %1 + Installerar %1 + + + Installation media path not set! + Sökväg till installationsmedia inte inställd! + + + The source path for %1 was not set. + Källsökvägen för %1 ställdes inte in. + + + Cannot create temporary directory! + Kan inte skapa temporär katalog! + + + Failed to create %1. + Kunde inte skapa %1. + + + Cannot move into temporary directory! + Kan inte flytta till temporär katalog! + + + Failed to move into %1. + Misslyckades att flytta till %1. + + + Moving installation files + Flyttar installationsfiler + + + Could not install directory! + Kunde inte installera katalog! + + + Installing %1 to %2 failed. + Installation %1 till %2 misslyckades. + + + Could not install translation file! + Kunde inte installera översättningsfil! + + + Failed to install *%1 files. + Kunde inte installera *%1 filer. + + + Could not install Morrowind data file! + Kunde inte installera Morrowind-datafil! + + + Failed to install %1. + Misslyckades att installera %1. + + + Could not install Morrowind configuration file! + Kunde inte installera Morrowind-konfigurationsfil! + + + Installing: Sound directory + Installerar: Ljudkatalog + + + Could not find Tribunal data file! + Tribunal-datafil hittades inte! + + + Failed to find %1. + Misslyckades att hitta %1. + + + Could not find Tribunal patch file! + Tribunal-patchfil hittades inte! + + + Could not find Bloodmoon data file! + Bloodmoon-datafil hittades inte! + + + Updating Morrowind configuration file + Uppdaterar Morrowind-konfigurationsfil + + + %1 installation finished! + %1 installation klar! + + + Extracting: %1 + Extraherar: %1 + + + Failed to open InstallShield Cabinet File. + Misslyckades att öppna en InstallShield Cabinet-fil. + + + Opening %1 failed. + Misslyckades att öppna %1. + + + Failed to extract %1. + Misslyckades att extrahera %1. + + + Complete path: %1 + Färdigställ sökväg: %1 + + + diff --git a/files/opencs/configure.svg b/files/opencs/configure.svg new file mode 100644 index 0000000000..91a5efca5c --- /dev/null +++ b/files/opencs/configure.svg @@ -0,0 +1,501 @@ + + + +image/svg+xml diff --git a/files/opencs/scalable/editor-icons.svg b/files/opencs/editor-icons.svg similarity index 100% rename from files/opencs/scalable/editor-icons.svg rename to files/opencs/editor-icons.svg diff --git a/files/opencs/menu-reload.svg b/files/opencs/menu-reload.svg index aef27dd104..32cefa7548 100644 --- a/files/opencs/menu-reload.svg +++ b/files/opencs/menu-reload.svg @@ -89,23 +89,82 @@ x2="76.100182" y2="249" gradientUnits="userSpaceOnUse" /> + + + + + + + + + + - + d="m 52.8211,61.122306 -2.004035,-0.0036 0.217103,-0.201119 c 0.158097,-0.150209 -0.07905,-0.375524 -0.237144,-0.225315 l -0.50101,0.476016 c -0.06587,0.06207 -0.06587,0.163247 0,0.225315 l 0.50101,0.476016 c 0.06533,0.06258 0.171817,0.06258 0.237144,0 0.06587,-0.06207 0.06585,-0.163246 -1.8e-5,-0.225315 L 50.817065,61.383321 H 52.8211 c 0.143894,-0.0027 0.357404,0.162378 0.360144,0.299094 v 0.391921 c 0,0.211563 0.264583,0.211563 0.264583,0 v -0.391921 c -0.0028,-0.311974 -0.296373,-0.566316 -0.624727,-0.563677 z m 0.07432,1.891352 c -0.158096,-0.149435 -0.394425,0.07511 -0.237143,0.225315 l 0.2171,0.261015 h -2.004033 c -0.143895,0.0027 -0.333193,-0.162379 -0.335933,-0.299097 v -0.39192 c -5.38e-4,-0.21105 -0.264046,-0.21105 -0.264584,0 v 0.39192 c 0.0028,0.311975 0.272162,0.56632 0.600517,0.56368 h 2.004035 l -0.217101,0.20112 c -0.06587,0.06207 -0.06587,0.163245 0,0.225314 0.06533,0.06258 0.171816,0.06258 0.237144,0 l 0.501007,-0.476016 c 0.06587,-0.06207 0.06587,-0.163245 0,-0.225314 z" + id="path1" + style="display:inline;overflow:hidden;fill:#4d4d4d;fill-opacity:1;stroke-width:0.00381524" /> diff --git a/files/opencs/object.svg b/files/opencs/object.svg index 717269b9d6..8f9261fd18 100644 --- a/files/opencs/object.svg +++ b/files/opencs/object.svg @@ -108,11 +108,11 @@ style="display:inline" transform="matrix(3.7795278,0,0,3.77117,-148,-187.58427)"> + cy="51.863018" + rx="1.7196451" + ry="1.7237496" /> diff --git a/files/opencs/raster/startup/big/configure.png b/files/opencs/raster/startup/big/configure.png deleted file mode 100644 index f0be888a15..0000000000 Binary files a/files/opencs/raster/startup/big/configure.png and /dev/null differ diff --git a/files/opencs/raster/startup/small/configure.png b/files/opencs/raster/startup/small/configure.png deleted file mode 100644 index e91b7f7731..0000000000 Binary files a/files/opencs/raster/startup/small/configure.png and /dev/null differ diff --git a/files/opencs/raster/startup/small/create-addon.png b/files/opencs/raster/startup/small/create-addon.png deleted file mode 100644 index 64fd138be5..0000000000 Binary files a/files/opencs/raster/startup/small/create-addon.png and /dev/null differ diff --git a/files/opencs/raster/startup/small/edit-content.png b/files/opencs/raster/startup/small/edit-content.png deleted file mode 100644 index 6297f1169c..0000000000 Binary files a/files/opencs/raster/startup/small/edit-content.png and /dev/null differ diff --git a/files/opencs/raster/startup/small/new-game.png b/files/opencs/raster/startup/small/new-game.png deleted file mode 100644 index 0d7d14c558..0000000000 Binary files a/files/opencs/raster/startup/small/new-game.png and /dev/null differ diff --git a/files/opencs/record-edit.svg b/files/opencs/record-edit.svg index 2db10e9c78..5e7a058782 100644 --- a/files/opencs/record-edit.svg +++ b/files/opencs/record-edit.svg @@ -116,7 +116,7 @@ gradientTransform="matrix(0.2267857,0,0,0.24804718,6.7279833,5.1097051)" /> + + id="rect2091" + style="font-variation-settings:normal;display:inline;vector-effect:none;fill:url(#linearGradient2178);fill-opacity:1;stroke:none;stroke-width:0.89642px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;paint-order:fill markers stroke;stop-color:#000000" + d="M 2,1 V 16.000002 H 13.999997 V 1 Z M 2.8571402,1.9374994 H 13.142856 V 15.062503 H 2.8571402 Z" + transform="matrix(0.26458331,0,0,0.26458402,33.866672,49.74166)" /> + cx="37.306255" + cy="51.329163" + rx="0.79374993" + ry="0.79375207" /> diff --git a/files/opencs/resources.qrc b/files/opencs/resources.qrc index 786040623c..3b5a17c730 100644 --- a/files/opencs/resources.qrc +++ b/files/opencs/resources.qrc @@ -102,7 +102,7 @@ raster/startup/big/create-addon.png raster/startup/big/new-game.png raster/startup/big/edit-content.png - raster/startup/small/configure.png + configure.svg lighting-moon.svg diff --git a/files/opencs/run-log.svg b/files/opencs/run-log.svg index 404f74a6d4..96e0d037b7 100644 --- a/files/opencs/run-log.svg +++ b/files/opencs/run-log.svg @@ -67,14 +67,13 @@ gradientTransform="matrix(0.26458338,0,0,0.26459185,23.018732,7.1420691)" /> + d="m 57.904031,57.222619 -1.429163,0.842578 a 0.07915963,0.08113535 0 0 1 -0.118629,-0.07046 l 0.0027,-1.689872 a 0.07915963,0.08113535 0 0 1 0.11885,-0.07007 l 1.426507,0.847294 a 0.07915963,0.08113535 0 0 1 -2.21e-4,0.140531 z" /> + y2="30.691668" + gradientTransform="matrix(1.5999994,0,0,1,-10.794989,8.2675378e-8)" /> + gradientTransform="matrix(1.5,0,0,1.6,6.0324979,-18.415001)" /> + style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:url(#linearGradient3131);fill-opacity:1;stroke:none;stroke-width:0.334674px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1" + d="m 18.838333,28.575001 v 4.233334 h 0.423332 v -4.233334 z m 0.846662,0.846667 v 2.540001 h 0.423336 v -2.540001 z m 1.693335,0 v 2.540001 h 0.423334 v -2.540001 z m -3.386665,0.423334 v 1.693333 h 0.423333 v -1.693333 z m 2.539997,0 v 1.693333 h 0.423335 v -1.693333 z m 1.693334,0 v 1.693333 h 0.423336 v -1.693333 z" /> + style="font-variation-settings:normal;display:inline;vector-effect:none;fill:url(#linearGradient2264);fill-opacity:1;stroke:none;stroke-width:0.40989px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000" + d="m 17.144998,27.305001 v 6.773334 h 6.35 v -6.773334 z m 0.396874,0.423334 h 5.55625 v 5.926666 h -5.55625 z" /> diff --git a/files/shaders/compatibility/bs/default.frag b/files/shaders/compatibility/bs/default.frag index d2c8de0b22..3c665752d1 100644 --- a/files/shaders/compatibility/bs/default.frag +++ b/files/shaders/compatibility/bs/default.frag @@ -77,10 +77,11 @@ void main() vec3 specularColor = getSpecularColor().xyz; #if @normalMap vec4 normalTex = texture2D(normalMap, normalMapUV); + vec3 normal = normalTex.xyz * 2.0 - 1.0; #if @reconstructNormalZ - normalTex.z = sqrt(1.0 - dot(normalTex.xy, normalTex.xy)); + normal.z = sqrt(1.0 - dot(normal.xy, normal.xy)); #endif - vec3 viewNormal = normalToView(normalTex.xyz * 2.0 - 1.0); + vec3 viewNormal = normalToView(normal); specularColor *= normalTex.a; #else vec3 viewNormal = normalize(gl_NormalMatrix * passNormal); diff --git a/files/shaders/compatibility/groundcover.frag b/files/shaders/compatibility/groundcover.frag index aab37d465d..96a79c8793 100644 --- a/files/shaders/compatibility/groundcover.frag +++ b/files/shaders/compatibility/groundcover.frag @@ -60,10 +60,11 @@ void main() #if @normalMap vec4 normalTex = texture2D(normalMap, normalMapUV); + vec3 normal = normalTex.xyz * 2.0 - 1.0; #if @reconstructNormalZ - normalTex.z = sqrt(1.0 - dot(normalTex.xy, normalTex.xy)); + normal.z = sqrt(1.0 - dot(normal.xy, normal.xy)); #endif - vec3 viewNormal = normalToView(normalTex.xyz * 2.0 - 1.0); + vec3 viewNormal = normalToView(normal); #else vec3 viewNormal = normalToView(normalize(passNormal)); #endif diff --git a/files/shaders/compatibility/objects.frag b/files/shaders/compatibility/objects.frag index eb5b79a0c2..7f163580cc 100644 --- a/files/shaders/compatibility/objects.frag +++ b/files/shaders/compatibility/objects.frag @@ -168,10 +168,11 @@ vec2 screenCoords = gl_FragCoord.xy / screenRes; #if @normalMap vec4 normalTex = texture2D(normalMap, normalMapUV + offset); + vec3 normal = normalTex.xyz * 2.0 - 1.0; #if @reconstructNormalZ - normalTex.z = sqrt(1.0 - dot(normalTex.xy, normalTex.xy)); + normal.z = sqrt(1.0 - dot(normal.xy, normal.xy)); #endif - vec3 viewNormal = normalToView(normalTex.xyz * 2.0 - 1.0); + vec3 viewNormal = normalToView(normal); #else vec3 viewNormal = normalize(gl_NormalMatrix * passNormal); #endif diff --git a/files/shaders/compatibility/terrain.frag b/files/shaders/compatibility/terrain.frag index f45f1f024e..9d89217f35 100644 --- a/files/shaders/compatibility/terrain.frag +++ b/files/shaders/compatibility/terrain.frag @@ -64,10 +64,11 @@ void main() #if @normalMap vec4 normalTex = texture2D(normalMap, adjustedUV); + vec3 normal = normalTex.xyz * 2.0 - 1.0; #if @reconstructNormalZ - normalTex.z = sqrt(1.0 - dot(normalTex.xy, normalTex.xy)); + normal.z = sqrt(1.0 - dot(normal.xy, normal.xy)); #endif - vec3 viewNormal = normalToView(normalTex.xyz * 2.0 - 1.0); + vec3 viewNormal = normalToView(normal); #else vec3 viewNormal = normalize(gl_NormalMatrix * passNormal); #endif diff --git a/files/shaders/compatibility/water.frag b/files/shaders/compatibility/water.frag index d1324e01bd..749dcf27cd 100644 --- a/files/shaders/compatibility/water.frag +++ b/files/shaders/compatibility/water.frag @@ -94,7 +94,6 @@ uniform vec2 screenRes; void main(void) { vec2 UV = worldPos.xy / (8192.0*5.0) * 3.0; - UV.y *= -1.0; float shadow = unshadowedLightRatio(linearDepth); diff --git a/files/windows/openmw-cs.exe.manifest b/files/windows/openmw-cs.exe.manifest new file mode 100644 index 0000000000..3107f64706 --- /dev/null +++ b/files/windows/openmw-cs.exe.manifest @@ -0,0 +1,9 @@ + + + + + true + PerMonitorV2 + + + \ No newline at end of file diff --git a/files/wizard/icons/dollar.svg b/files/wizard/icons/dollar.svg new file mode 100644 index 0000000000..60c5a4b23c --- /dev/null +++ b/files/wizard/icons/dollar.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/wizard/icons/folder.svg b/files/wizard/icons/folder.svg new file mode 100644 index 0000000000..a976bad226 --- /dev/null +++ b/files/wizard/icons/folder.svg @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + 2009-01-13, 2020-12-18 + + + Jakub Steiner, Daniel S. Fowler + + + + https://tekeye.uk/free_resources/tango_desktop_project/index + + + folder + directory + icon + + + + + Public Domain + + + + + https://tekeye.uk/ + + + https://tekeye.uk/free_resources/tango_desktop_project/images/folder.svg + computing + A representation of a folder for file storage. + en-GB + https://tekeye.uk/free_resources/tango_desktop_project/images/folder-remote.svg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/wizard/icons/preferences-desktop-locale.svg b/files/wizard/icons/preferences-desktop-locale.svg new file mode 100644 index 0000000000..51098cfa13 --- /dev/null +++ b/files/wizard/icons/preferences-desktop-locale.svg @@ -0,0 +1,307 @@ + + + Locale Preferences Icon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner, Daniel S. Fowler + + + https://tekeye.uk/free_resources/tango_desktop_project/index + + Locale Preferences Icon + + + locale + settings + preferences + location + flag + computer + icon + + + 2021-01-02 + + + Public Domain + + + + + https://tekeye.uk/ + + + https://tekeye.uk/free_resources/tango_desktop_project/images/svg/apps/preferences-desktop-locale.svg + https://tekeye.uk/free_resources/tango_desktop_project/images/svg/apps/preferences-desktop-screensaver.svg + en-GB + computing + An icon for locale settings. + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/wizard/icons/system-installer.svg b/files/wizard/icons/system-installer.svg new file mode 100644 index 0000000000..7d95eea080 --- /dev/null +++ b/files/wizard/icons/system-installer.svg @@ -0,0 +1,456 @@ + + + System Installer Icon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + System Installer Icon + + + Jakub Steiner, Daniel S. Fowler + + + https://tekeye.uk/free_resources/tango_desktop_project/index + + 2021-01-02 + + + Public Domain + + + + + https://tekeye.uk/ + + + https://tekeye.uk/free_resources/tango_desktop_project/images/svg/apps/system-installer.svg + https://tekeye.uk/free_resources/tango_desktop_project/images/svg/apps/preferences-desktop-screensaver.svg + en-GB + + + install + setup + packages + manager + settings + preferences + cd + dvd + computer + icon + + + computing + An icon for system setup and installation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/wizard/icons/tango/48x48/dollar.png b/files/wizard/icons/tango/48x48/dollar.png deleted file mode 100644 index a14ba2505d..0000000000 Binary files a/files/wizard/icons/tango/48x48/dollar.png and /dev/null differ diff --git a/files/wizard/icons/tango/48x48/folder.png b/files/wizard/icons/tango/48x48/folder.png deleted file mode 100644 index e93d7cf8f8..0000000000 Binary files a/files/wizard/icons/tango/48x48/folder.png and /dev/null differ diff --git a/files/wizard/icons/tango/48x48/preferences-desktop-locale.png b/files/wizard/icons/tango/48x48/preferences-desktop-locale.png deleted file mode 100644 index f56497bd23..0000000000 Binary files a/files/wizard/icons/tango/48x48/preferences-desktop-locale.png and /dev/null differ diff --git a/files/wizard/icons/tango/48x48/system-installer.png b/files/wizard/icons/tango/48x48/system-installer.png deleted file mode 100644 index dbd08f7e2f..0000000000 Binary files a/files/wizard/icons/tango/48x48/system-installer.png and /dev/null differ diff --git a/files/wizard/icons/tango/index.theme b/files/wizard/icons/tango/index.theme deleted file mode 100644 index 3ffebebc9a..0000000000 --- a/files/wizard/icons/tango/index.theme +++ /dev/null @@ -1,8 +0,0 @@ -[Icon Theme] -Name=Tango -Comment=Tango Theme -Inherits=default -Directories=48x48 - -[48x48] -Size=48 \ No newline at end of file diff --git a/files/wizard/wizard.qrc b/files/wizard/wizard.qrc index 7dbb8fe080..6f7bf69eb0 100644 --- a/files/wizard/wizard.qrc +++ b/files/wizard/wizard.qrc @@ -1,10 +1,9 @@ - - icons/tango/48x48/preferences-desktop-locale.png - icons/tango/index.theme - icons/tango/48x48/folder.png - icons/tango/48x48/system-installer.png - icons/tango/48x48/dollar.png + + icons/preferences-desktop-locale.svg + icons/folder.svg + icons/system-installer.svg + icons/dollar.svg images/intropage-background.png diff --git a/scripts/osg_stats.py b/scripts/osg_stats.py index 20fae2cac8..81739c1b57 100755 --- a/scripts/osg_stats.py +++ b/scripts/osg_stats.py @@ -159,13 +159,7 @@ def collect_per_frame(sources, keys, begin_frame, end_frame, frame_number_name): if key in frame: result[name][key][index] = frame[key] for key in keys: - prev = 0.0 values = result[name][key][:max_index + 1] - 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 @@ -187,7 +181,7 @@ def draw_timeseries(sources, keys, add_sum, begin_frame, end_frame): y = frames[key] ax.plot(x[:len(y)], y, label=f'{key}:{name}') if add_sum: - y = numpy.sum(list(frames[k] for k in keys), axis=0) + y = sum_arrays_with_none([frames[k] for k in keys]) ax.plot(x[:len(y)], y, label=f'sum:{name}', linestyle='--') ax.grid(True) ax.legend() @@ -199,10 +193,10 @@ def draw_cumulative_timeseries(sources, keys, add_sum, begin_frame, end_frame): x = numpy.array(range(begin_frame, end_frame)) for name, frames in sources.items(): for key in keys: - y = numpy.cumsum(frames[key]) + y = cumsum_with_none(frames[key]) ax.plot(x[:len(y)], y, label=f'{key}:{name}') if add_sum: - y = numpy.cumsum(numpy.sum(list(frames[k] for k in keys), axis=0)) + y = sum_arrays_with_none([cumsum_with_none(frames[k]) for k in keys]) ax.plot(x[:len(y)], y, label=f'sum:{name}', linestyle='--') ax.grid(True) ax.legend() @@ -214,10 +208,10 @@ def draw_timeseries_delta(sources, keys, add_sum, begin_frame, end_frame): x = numpy.array(range(begin_frame + 1, end_frame)) for name, frames in sources.items(): for key in keys: - y = numpy.diff(frames[key]) - ax.plot(x[:len(y)], numpy.diff(frames[key]), label=f'{key}:{name}') + y = diff_with_none(frames[key]) + ax.plot(x[:len(y)], y, label=f'{key}:{name}') if add_sum: - y = numpy.diff(numpy.sum(list(frames[k] for k in keys), axis=0)) + y = sum_arrays_with_none([diff_with_none(frames[k]) for k in keys]) ax.plot(x[:len(y)], y, label=f'sum:{name}', linestyle='--') ax.grid(True) ax.legend() @@ -367,13 +361,13 @@ def make_stats(source, key, values, precision): source=source, key=key, number=len(values), - min=fixed_float(min(values), precision), - max=fixed_float(max(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(float(v) for v in values), precision), - q95=fixed_float(numpy.quantile(values, 0.95), precision), + min=fixed_float(min(values), precision) if values else '-', + max=fixed_float(max(values), precision) if values else '-', + sum=fixed_float(sum(values), precision) if values else '-', + mean=fixed_float(statistics.mean(values), precision) if values else '-', + median=fixed_float(statistics.median(values), precision) if values else '-', + stdev=fixed_float(statistics.stdev(float(v) for v in values), precision) if values else '-', + q95=fixed_float(numpy.quantile(values, 0.95), precision) if values else '-', ) @@ -384,5 +378,49 @@ def to_number(value): return float(value) +def cumsum_with_none(values): + cumsum = None + result = list() + for v in values: + if v is None: + result.append(None) + elif cumsum is None: + cumsum = v + result.append(cumsum) + else: + cumsum += v + result.append(cumsum) + return numpy.array(result) + + +def diff_with_none(values): + if len(values) < 2: + return numpy.array([]) + prev = values[0] + result = list() + for v in values[1:]: + if prev is None: + result.append(v) + prev = v + elif v is None: + result.append(v) + else: + result.append(v - prev) + prev = v + return numpy.array(result) + + +def sum_arrays_with_none(arrays): + size = max(len(v) for v in arrays) + result = list() + for i in range(size): + not_none_values = [v[i] for v in arrays if v[i] is not None] + if not_none_values: + result.append(sum(not_none_values)) + else: + result.append(None) + return numpy.array(result) + + if __name__ == '__main__': main()