1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-01-27 03:35:27 +00:00

Merge branch openmw:master into mwdialogue-bindings

This commit is contained in:
trav 2024-04-24 21:06:58 +00:00
commit 0986b103c5
129 changed files with 5796 additions and 838 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -12,6 +12,7 @@
#include <components/files/multidircollection.hpp>
#include <components/misc/strings/conversion.hpp>
#include <components/platform/platform.hpp>
#include <components/resource/bgsmfilemanager.hpp>
#include <components/resource/bulletshape.hpp>
#include <components/resource/bulletshapemanager.hpp>
#include <components/resource/foreachbulletobject.hpp>
@ -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(

View File

@ -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)

View File

@ -19,6 +19,7 @@
#include <components/files/conversion.hpp>
#include <components/files/multidircollection.hpp>
#include <components/platform/platform.hpp>
#include <components/resource/bgsmfilemanager.hpp>
#include <components/resource/bulletshapemanager.hpp>
#include <components/resource/imagemanager.hpp>
#include <components/resource/niffilemanager.hpp>
@ -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();

View File

@ -8,6 +8,7 @@
#include <utility>
#include <vector>
#include <components/bgsm/file.hpp>
#include <components/files/configurationmanager.hpp>
#include <components/files/constrainedfilestream.hpp>
#include <components/files/conversion.hpp>
@ -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<VFS::Archive> 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<VFS::Archive>&& 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<VFS::Archive>&& 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 <nif files, kf files, BSA/BA2 files, or directories>
niftest <nif files, kf files, bgem/bgsm files, BSA/BA2 files, or directories>
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;
}
}

View File

@ -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)

View File

@ -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" },

View File

@ -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,

View File

@ -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(

View File

@ -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] != '/')

View File

@ -10,7 +10,7 @@
#include <QScreen>
#include <QVBoxLayout>
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)

View File

@ -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();

View File

@ -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();

View File

@ -62,7 +62,7 @@ namespace CSVRender
osg::ref_ptr<osg::PositionAttitudeTransform> mBaseNode;
SceneUtil::Skeleton* mSkeleton;
SceneUtil::NodeMapVisitor::NodeMap mNodeMap;
SceneUtil::NodeMap mNodeMap;
};
}

View File

@ -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"
"<ul><li>Use {scene-edit-primary} to move instances around freely</li>"
"<li>Use {scene-edit-secondary} to move instances around within the grid</li>"

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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));

View File

@ -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);
}

View File

@ -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)

View File

@ -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";

View File

@ -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

View File

@ -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)

View File

@ -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);

View File

@ -6,6 +6,7 @@
#include <components/settings/values.hpp>
#include <components/vfs/manager.hpp>
#include <components/vfs/pathutil.hpp>
#include <components/widgets/imagebutton.hpp>
#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();
}

View File

@ -402,7 +402,8 @@ namespace MWGui
std::vector<std::string> 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")
{

View File

@ -7,7 +7,6 @@
#include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp"
#include <components/settings/values.hpp>
#include <components/widgets/imagebutton.hpp>
#include "draganddrop.hpp"
@ -80,12 +79,12 @@ void WindowBase::center()
void WindowBase::clampWindowCoordinates(MyGUI::Window* window)
{
auto minSize = window->getMinSize();
minSize.height = static_cast<int>(minSize.height * Settings::gui().mScalingFactor);
minSize.width = static_cast<int>(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;

View File

@ -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));

View File

@ -141,7 +141,7 @@ namespace MWLua
api["streamMusic"] = [](std::string_view fileName, const sol::optional<sol::table>& 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"]

View File

@ -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
{

View File

@ -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;
}
}

View File

@ -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<AnimationTime>();
@ -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);

View File

@ -246,6 +246,7 @@ namespace MWRender
osg::ref_ptr<SceneUtil::LightListCallback> mLightListCallback;
bool mPlayScriptedOnly;
bool mRequiresBoneMap;
const NodeMap& getNodeMap() const;

View File

@ -1,6 +1,7 @@
#include "rotatecontroller.hpp"
#include <osg/MatrixTransform>
#include <osgAnimation/Bone>
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<osgAnimation::Bone*>(node);
if (b)
{
osgAnimation::Bone* parent = b->getBoneParent();
if (parent)
matrix *= parent->getMatrixInSkeletonSpace();
b->setMatrixInSkeletonSpace(matrix);
}
traverse(node, nv);
}

View File

@ -715,8 +715,6 @@ namespace MWRender
osg::ref_ptr<osg::Texture2D> 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);

View File

@ -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)
{

View File

@ -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(

View File

@ -0,0 +1,15 @@
#ifndef OPENMW_APPS_OPENMW_MWSOUND_CONSTANTS_H
#define OPENMW_APPS_OPENMW_MWSOUND_CONSTANTS_H
#include <components/vfs/pathutil.hpp>
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

View File

@ -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));

View File

@ -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;

View File

@ -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<MWSoundDecoderBridge> 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();

View File

@ -1,6 +1,8 @@
#ifndef GAME_SOUND_SOUND_DECODER_H
#define GAME_SOUND_SOUND_DECODER_H
#include <components/vfs/pathutil.hpp>
#include <string>
#include <vector>
@ -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;

View File

@ -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<std::string>& 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<VFS::Path::Normalized>& 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<std::string> filelist;
auto playlistPath = Misc::ResourceHelpers::correctMusicPath(playlist) + '/';
for (const auto& name : mVFS->getRecursiveDirectoryIterator(playlistPath))
std::vector<VFS::Path::Normalized> 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);

View File

@ -9,6 +9,7 @@
#include <components/fallback/fallback.hpp>
#include <components/misc/objectpool.hpp>
#include <components/misc/strings/algorithm.hpp>
#include <components/settings/settings.hpp>
#include "../mwbase/soundmanager.hpp"
@ -52,9 +53,10 @@ namespace MWSound
std::unique_ptr<Sound_Output> mOutput;
// Caches available music tracks by <playlist name, (sound files) >
std::unordered_map<std::string, std::vector<std::string>> mMusicFiles;
std::unordered_map<VFS::Path::Normalized, std::vector<VFS::Path::Normalized>, VFS::Path::Hash, std::equal_to<>>
mMusicFiles;
std::unordered_map<std::string, std::vector<int>> 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

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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})

View File

@ -8,19 +8,22 @@ namespace
TEST(CorrectSoundPath, wav_files_not_overridden_with_mp3_in_vfs_are_not_corrected)
{
std::unique_ptr<VFS::Manager> 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<VFS::Manager> 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<VFS::Manager> 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

View File

@ -3,6 +3,7 @@
#include <components/nif/node.hpp>
#include <components/nif/property.hpp>
#include <components/nifosg/nifloader.hpp>
#include <components/resource/bgsmfilemanager.hpp>
#include <components/resource/imagemanager.hpp>
#include <components/sceneutil/serialize.hpp>
#include <components/vfs/manager.hpp>
@ -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<osgDB::Options> 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<Nif::NiProperty>(&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<Nif::NiProperty>(&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));
}

View File

@ -0,0 +1,131 @@
#include <components/sceneutil/osgacontroller.hpp>
#include <gtest/gtest.h>
#include <osgAnimation/Channel>
#include <filesystem>
#include <fstream>
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<Resource::Animation> createMergedAnimationTrack(std::string name, osg::Matrixf startTransform,
osg::Matrixf endTransform, float startTime = 0.0f, float endTime = 1.0f)
{
osg::ref_ptr<Resource::Animation> 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<osgAnimation::MatrixLinearChannel> 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<EmulatedAnimation> 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<EmulatedAnimation> 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<EmulatedAnimation> 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<EmulatedAnimation> 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<EmulatedAnimation> 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
}
}

View File

@ -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");
}

View File

@ -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})

View File

@ -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);
}

View File

@ -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");
}

View File

@ -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);

View File

@ -30,9 +30,6 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/folder.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>

View File

@ -30,9 +30,6 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/preferences-desktop-locale.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>

View File

@ -59,9 +59,6 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/system-installer.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
@ -124,9 +121,6 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/folder.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
@ -191,9 +185,6 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/dollar.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>

View File

@ -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
)

236
components/bgsm/file.cpp Normal file
View File

@ -0,0 +1,236 @@
#include "file.hpp"
#include <array>
#include <stdexcept>
#include "stream.hpp"
namespace Bgsm
{
MaterialFilePtr parse(Files::IStreamPtr&& inputStream)
{
std::shared_ptr<Bgsm::MaterialFile> file;
BGSMStream stream(std::move(inputStream));
std::array<char, 4> signature;
stream.readArray(signature);
std::string shaderType(signature.data(), 4);
if (shaderType == "BGEM")
{
file = std::make_shared<BGEMFile>();
file->mShaderType = Bgsm::ShaderType::Effect;
}
else if (shaderType == "BGSM")
{
file = std::make_shared<BGSMFile>();
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);
}
}

167
components/bgsm/file.hpp Normal file
View File

@ -0,0 +1,167 @@
#ifndef OPENMW_COMPONENTS_BGSM_FILE_HPP
#define OPENMW_COMPONENTS_BGSM_FILE_HPP
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <osg/Vec2f>
#include <osg/Vec3f>
#include <osg/Vec4f>
#include <components/files/istreamptr.hpp>
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<const Bgsm::MaterialFile>;
MaterialFilePtr parse(Files::IStreamPtr&& stream);
}
#endif

View File

@ -0,0 +1,39 @@
#include "stream.hpp"
namespace Bgsm
{
template <>
void BGSMStream::read<osg::Vec2f>(osg::Vec2f& vec)
{
readBufferOfType(mStream, vec._v);
}
template <>
void BGSMStream::read<osg::Vec3f>(osg::Vec3f& vec)
{
readBufferOfType(mStream, vec._v);
}
template <>
void BGSMStream::read<osg::Vec4f>(osg::Vec4f& vec)
{
readBufferOfType(mStream, vec._v);
}
template <>
void BGSMStream::read<std::string>(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);
}
}

View File

@ -0,0 +1,77 @@
#ifndef OPENMW_COMPONENTS_BGSM_STREAM_HPP
#define OPENMW_COMPONENTS_BGSM_STREAM_HPP
#include <array>
#include <cassert>
#include <cstdint>
#include <istream>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <components/files/istreamptr.hpp>
#include <components/misc/endianness.hpp>
#include <osg/Vec2f>
#include <osg/Vec3f>
#include <osg/Vec4f>
namespace Bgsm
{
template <std::size_t numInstances, typename T>
inline void readBufferOfType(Files::IStreamPtr& pIStream, T* dest)
{
static_assert(std::is_arithmetic_v<T>, "Buffer element type is not arithmetic");
pIStream->read(reinterpret_cast<char*>(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 <std::size_t numInstances, typename T>
inline void readBufferOfType(Files::IStreamPtr& pIStream, T (&dest)[numInstances])
{
readBufferOfType<numInstances>(pIStream, static_cast<T*>(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 <class T>
void read(T& data)
{
readBufferOfType<1>(mStream, &data);
}
/// Read multiple instances of type into an array
template <class T, size_t size>
void readArray(std::array<T, size>& arr)
{
readBufferOfType<size>(mStream, arr.data());
}
};
template <>
void BGSMStream::read<osg::Vec2f>(osg::Vec2f& vec);
template <>
void BGSMStream::read<osg::Vec3f>(osg::Vec3f& vec);
template <>
void BGSMStream::read<osg::Vec4f>(osg::Vec4f& vec);
template <>
void BGSMStream::read<std::string>(std::string& str);
}
#endif

View File

@ -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);
}

View File

@ -1,11 +0,0 @@
#ifndef NATURALSORT_H
#define NATURALSORT_H
#include <QString>
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

View File

@ -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);
}

View File

@ -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)

View File

@ -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);

View File

@ -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());

View File

@ -21,6 +21,7 @@
#include <components/misc/strings/algorithm.hpp>
#include <components/misc/strings/lower.hpp>
#include <components/nif/parent.hpp>
#include <components/resource/bgsmfilemanager.hpp>
#include <components/resource/imagemanager.hpp>
// particle
@ -42,6 +43,7 @@
#include <osg/TexEnvCombine>
#include <osg/Texture2D>
#include <components/bgsm/file.hpp>
#include <components/nif/effect.hpp>
#include <components/nif/exception.hpp>
#include <components/nif/extra.hpp>
@ -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<unsigned int> mBoundTextures = {};
int mAnimFlags = 0;
@ -349,7 +352,7 @@ namespace NifOsg
osg::Node* mRootNode = nullptr;
};
osg::ref_ptr<osg::Node> load(Nif::FileView nif, Resource::ImageManager* imageManager)
osg::ref_ptr<osg::Node> load(Nif::FileView nif)
{
const size_t numRoots = nif.numRoots();
std::vector<const Nif::NiAVObject*> 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<unsigned int>& boundTextures, int animflags)
SceneUtil::CompositeStateSetUpdater* composite, std::vector<unsigned int>& 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<const Nif::BSTriShape*>(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<osg::Image> handleSourceTexture(
const Nif::NiSourceTexture* st, Resource::ImageManager* imageManager)
osg::ref_ptr<osg::Image> handleSourceTexture(const Nif::NiSourceTexture* st) const
{
if (!st)
return nullptr;
if (st)
{
if (st->mExternal)
return getTextureImage(st->mFile);
osg::ref_ptr<osg::Image> 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<osg::Image> image(handleSourceTexture(textureEffect->mTexture.getPtr(), imageManager));
osg::ref_ptr<osg::Texture2D> 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<unsigned int> 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<SceneUtil::CompositeStateSetUpdater> 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<osg::StateSet> 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<osg::Image> 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<osg::Texture2D> attachTexture(const std::string& name, osg::ref_ptr<osg::Image> image, bool wrapS,
bool wrapT, unsigned int uvSet, osg::StateSet* stateset, std::vector<unsigned int>& boundTextures) const
{
osg::ref_ptr<osg::Texture2D> 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<osg::Texture2D> attachExternalTexture(const std::string& name, const std::string& path, bool wrapS,
bool wrapT, unsigned int uvSet, osg::StateSet* stateset, std::vector<unsigned int>& boundTextures) const
{
return attachTexture(name, getTextureImage(path), wrapS, wrapT, uvSet, stateset, boundTextures);
}
osg::ref_ptr<osg::Texture2D> attachNiSourceTexture(const std::string& name, const Nif::NiSourceTexture* st,
bool wrapS, bool wrapT, unsigned int uvSet, osg::StateSet* stateset,
std::vector<unsigned int>& boundTextures) const
{
return attachTexture(name, handleSourceTexture(st), wrapS, wrapT, uvSet, stateset, boundTextures);
}
static void clearBoundTextures(osg::StateSet* stateset, std::vector<unsigned int>& 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<unsigned int> boundTextures; // Dummy list for attachTexture
for (const auto& source : flipctrl->mSources)
{
if (source.empty())
continue;
osg::ref_ptr<osg::Image> image(handleSourceTexture(source.getPtr(), imageManager));
osg::ref_ptr<osg::Texture2D> 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<osg::Texture2D> texture
= attachNiSourceTexture({}, source.getPtr(), wrapS, wrapT, uvSet, nullptr, boundTextures);
textures.push_back(texture);
}
osg::ref_ptr<FlipController> callback(new FlipController(flipctrl, textures));
@ -1811,7 +1844,7 @@ namespace NifOsg
}
}
osg::ref_ptr<osg::Image> handleInternalTexture(const Nif::NiPixelData* pixelData)
osg::ref_ptr<osg::Image> handleInternalTexture(const Nif::NiPixelData* pixelData) const
{
if (pixelData->mMipmaps.empty())
return nullptr;
@ -1946,7 +1979,7 @@ namespace NifOsg
return image;
}
osg::ref_ptr<osg::TexEnvCombine> createEmissiveTexEnv()
static osg::ref_ptr<osg::TexEnvCombine> createEmissiveTexEnv()
{
osg::ref_ptr<osg::TexEnvCombine> 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<unsigned int>& boundTextures, int animflags)
std::vector<unsigned int>& 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<osg::Texture2D> 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<osg::Image> 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<unsigned int>& 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<unsigned int>& 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<const Bgsm::BGSMFile*>(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<const Bgsm::BGEMFile*>(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<osg::StateSet> stateset = node.getOrCreateStateSet();
osg::ref_ptr<osg::PolygonOffset> 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<osg::AlphaFunc> 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<osg::StateSet> stateset = node.getOrCreateStateSet();
osg::ref_ptr<osg::BlendFunc> 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<osg::StateSet> stateset = node.getStateSet())
{
stateset->removeAttribute(osg::StateAttribute::BLENDFUNC);
stateset->removeMode(GL_BLEND);
if (!mPushedSorter)
stateset->setRenderBinToInherit();
}
}
void handleShaderMaterialDrawableProperties(
Bgsm::MaterialFilePtr shaderMat, osg::ref_ptr<osg::Material> 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<const Bgsm::BGSMFile*>(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<const Bgsm::BGEMFile*>(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<unsigned int>& 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<Nif::BSShaderTextureSet::TextureType>(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<osg::Image> image = imageManager->getImage(filename);
osg::ref_ptr<osg::Texture2D> 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<Nif::BSShaderTextureSet::TextureType>(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<unsigned int>& boundTextures, int animflags, bool hasStencilProperty)
SceneUtil::CompositeStateSetUpdater* composite, std::vector<unsigned int>& boundTextures, int animflags,
bool hasStencilProperty)
{
switch (property->recType)
{
@ -2352,8 +2474,7 @@ namespace NifOsg
{
const Nif::NiTexturingProperty* texprop = static_cast<const Nif::NiTexturingProperty*>(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<int>(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<osg::Image> image = imageManager->getImage(filename);
osg::ref_ptr<osg::Texture2D> 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<osg::Image> image = imageManager->getImage(filename);
osg::ref_ptr<osg::Texture2D> 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<osg::TexMat> 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<const Nif::NiAlphaProperty*>(property);
if (alphaprop->useAlphaBlending())
{
osg::ref_ptr<osg::BlendFunc> 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<osg::AlphaFunc> 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<const Nif::BSLightingShaderProperty*>(property);
if (Bgsm::MaterialFilePtr shaderMat = getShaderMaterial(shaderprop->mName, mMaterialManager))
{
handleShaderMaterialDrawableProperties(shaderMat, mat, *node, hasSortAlpha);
if (shaderMat->mShaderType == Bgsm::ShaderType::Lighting)
{
auto bgsm = static_cast<const Bgsm::BGSMFile*>(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<osg::PolygonOffset> 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<const Nif::BSEffectShaderProperty*>(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<osg::PolygonOffset> 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<osg::Node> Loader::load(Nif::FileView file, Resource::ImageManager* imageManager)
osg::ref_ptr<osg::Node> 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)

View File

@ -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<osg::Node> load(Nif::FileView file, Resource::ImageManager* imageManager);
static osg::ref_ptr<osg::Node> 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);

View File

@ -0,0 +1,55 @@
#include "bgsmfilemanager.hpp"
#include <osg/Object>
#include <components/vfs/manager.hpp>
#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<osg::Object> obj = mCache->getRefFromObjectCache(name);
if (obj)
return static_cast<BgsmFileHolder*>(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);
}
}

View File

@ -0,0 +1,27 @@
#ifndef OPENMW_COMPONENTS_RESOURCE_BGSMFILEMANAGER_H
#define OPENMW_COMPONENTS_RESOURCE_BGSMFILEMANAGER_H
#include <components/bgsm/file.hpp>
#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

View File

@ -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()))

View File

@ -2,6 +2,7 @@
#include <algorithm>
#include "bgsmfilemanager.hpp"
#include "imagemanager.hpp"
#include "keyframemanager.hpp"
#include "niffilemanager.hpp"
@ -15,11 +16,14 @@ namespace Resource
: mVFS(vfs)
{
mNifFileManager = std::make_unique<NifFileManager>(vfs, encoder);
mBgsmFileManager = std::make_unique<BgsmFileManager>(vfs, expiryDelay);
mImageManager = std::make_unique<ImageManager>(vfs, expiryDelay);
mSceneManager = std::make_unique<SceneManager>(vfs, mImageManager.get(), mNifFileManager.get(), expiryDelay);
mSceneManager = std::make_unique<SceneManager>(
vfs, mImageManager.get(), mNifFileManager.get(), mBgsmFileManager.get(), expiryDelay);
mKeyframeManager = std::make_unique<KeyframeManager>(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();

View File

@ -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<SceneManager> mSceneManager;
std::unique_ptr<ImageManager> mImageManager;
std::unique_ptr<BgsmFileManager> mBgsmFileManager;
std::unique_ptr<NifFileManager> mNifFileManager;
std::unique_ptr<KeyframeManager> mKeyframeManager;

View File

@ -9,7 +9,10 @@
#include <osg/Node>
#include <osg/UserDataContainer>
#include <osgAnimation/Bone>
#include <osgAnimation/RigGeometry>
#include <osgAnimation/Skeleton>
#include <osgAnimation/UpdateBone>
#include <osgParticle/ParticleSystem>
@ -52,6 +55,7 @@
#include <components/files/hash.hpp>
#include <components/files/memorystream.hpp>
#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<osgAnimation::Bone*>(&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<osg::ref_ptr<SceneUtil::RigGeometryHolder>> mRigGeometryHolders;
};
void updateVertexInfluenceMap(osgAnimation::RigGeometry& rig)
{
osgAnimation::VertexInfluenceMap* vertexInfluenceMap = rig.getInfluenceMap();
if (!vertexInfluenceMap)
return;
std::vector<std::string> 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<osgAnimation::AnimationUpdateCallback<osg::NodeCallback>*>(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<std::uint64_t, 2> 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<osgAnimation::RigGeometry*>(foundRigNode);
if (isColladaFile)
Resource::updateVertexInfluenceMap(*foundRigGeometry);
osg::ref_ptr<SceneUtil::RigGeometryHolder> 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<osg::Group*>(node))
{
group->removeChildren(0, group->getNumChildren());
@ -730,11 +797,12 @@ namespace Resource
}
osg::ref_ptr<osg::Node> 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<osg::Node> SceneManager::cloneErrorMarker()
@ -888,7 +957,7 @@ namespace Resource
osg::ref_ptr<osg::Node> loaded;
try
{
loaded = load(normalized, mVFS, mImageManager, mNifFileManager);
loaded = load(normalized, mVFS, mImageManager, mNifFileManager, mBgsmFileManager);
SceneUtil::ProcessExtraDataVisitor extraDataVisitor(this);
loaded->accept(extraDataVisitor);

View File

@ -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;

View File

@ -87,6 +87,7 @@ namespace Resource
"Image",
"Nif",
"Keyframe",
"BSShader Material",
"Groundcover Chunk",
"Object Chunk",
"Terrain Chunk",

View File

@ -9,6 +9,7 @@
#include <yaml-cpp/yaml.h>
#include <components/misc/osguservalues.hpp>
#include <components/misc/strings/algorithm.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/sceneutil/depth.hpp>
#include <components/shader/shadermanager.hpp>

View File

@ -1,5 +1,6 @@
#include <components/sceneutil/osgacontroller.hpp>
#include <osg/MatrixTransform>
#include <osg/Node>
#include <osg/NodeVisitor>
#include <osg/ref_ptr>
@ -9,6 +10,7 @@
#include <osgAnimation/Sampler>
#include <osgAnimation/UpdateMatrixTransform>
#include <components/misc/strings/algorithm.hpp>
#include <components/misc/strings/lower.hpp>
#include <components/resource/animation.hpp>
#include <components/sceneutil/controller.hpp>
@ -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<osg::MatrixTransform*>(node)->setMatrix(getTransformForNode(time, node->getName()));
}
traverse(node, nv);

View File

@ -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);

View File

@ -5,6 +5,8 @@
#include <osgParticle/ParticleSystem>
#include <osgAnimation/Bone>
#include <components/debug/debuglog.hpp>
#include <components/misc/strings/algorithm.hpp>
@ -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<osgAnimation::Bone*>(&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);
}

View File

@ -50,14 +50,14 @@ namespace SceneUtil
std::vector<osg::Node*> mFoundNodes;
};
typedef std::unordered_map<std::string, osg::ref_ptr<osg::MatrixTransform>, Misc::StringUtils::CiHash,
Misc::StringUtils::CiEqual>
NodeMap;
/// Maps names to nodes
class NodeMapVisitor : public osg::NodeVisitor
{
public:
typedef std::unordered_map<std::string, osg::ref_ptr<osg::MatrixTransform>, 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();

View File

@ -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() };
}
}

View File

@ -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.

View File

@ -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<std::string_view>{}(sv); }
[[nodiscard]] std::size_t operator()(const std::string& s) const { return std::hash<std::string>{}(s); }
[[nodiscard]] std::size_t operator()(const Normalized& s) const { return std::hash<std::string>{}(s.value()); }
[[nodiscard]] std::size_t operator()(NormalizedView s) const
{
return std::hash<std::string_view>{}(s.value());
}
};
}
#endif

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="sv_SE">
<context>
<name>ContentSelector</name>
<message>
<source>Select language used by ESM/ESP content files to allow OpenMW to detect their encoding. </source>
<translation>Välj språk som används av ESM/ESP-innehållsfiler att OpenMW kan hitta deras kodning. </translation>
</message>
</context>
<context>
<name>ContentSelectorModel::ContentModel</name>
<message>
<source>Unable to find dependent file: %1</source>
<translation>Kunde inte hitta beroende fil: %1</translation>
</message>
<message>
<source>Dependent file needs to be active: %1</source>
<translation>Beroende fil måste vara aktiv: %1</translation>
</message>
<message>
<source>This file needs to load after %1</source>
<translation>Denna fil måste laddas efter %1</translation>
</message>
</context>
<context>
<name>ContentSelectorModel::EsmFile</name>
<message>
<source>&lt;b&gt;Author:&lt;/b&gt; %1&lt;br/&gt;&lt;b&gt;Format version:&lt;/b&gt; %2&lt;br/&gt;&lt;b&gt;Modified:&lt;/b&gt; %3&lt;br/&gt;&lt;b&gt;Path:&lt;/b&gt;&lt;br/&gt;%4&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Description:&lt;/b&gt;&lt;br/&gt;%5&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Dependencies: &lt;/b&gt;%6&lt;br/&gt;</source>
<translation>&lt;b&gt;Skapare:&lt;/b&gt; %1&lt;br/&gt;&lt;b&gt;Formatversion:&lt;/b&gt; %2&lt;br/&gt;&lt;b&gt;Ändrad:&lt;/b&gt; %3&lt;br/&gt;&lt;b&gt;Sökväg:&lt;/b&gt;&lt;br/&gt;%4&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Beskrivning:&lt;/b&gt;&lt;br/&gt;%5&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Beroenden: &lt;/b&gt;%6&lt;br/&gt;</translation>
</message>
<message>
<source>&lt;br/&gt;&lt;b&gt;This content file cannot be disabled because it is part of OpenMW.&lt;/b&gt;&lt;br/&gt;</source>
<translation>&lt;br/&gt;&lt;b&gt;Denna innehållsfil kan inte inaktiveras den är en del av OpenMW.&lt;/b&gt;&lt;br/&gt;</translation>
</message>
<message>
<source>&lt;br/&gt;&lt;b&gt;This content file cannot be disabled because it is enabled in a config file other than the user one.&lt;/b&gt;&lt;br/&gt;</source>
<translation>&lt;br/&gt;&lt;b&gt;Denna innehållsfil kan inte inaktiveras den är en aktiverad i en annan konfigurationsfil än användarens.&lt;/b&gt;&lt;br/&gt;</translation>
</message>
</context>
<context>
<name>ContentSelectorView::ContentSelector</name>
<message>
<source>&amp;Check Selected</source>
<translation>&amp;Bocka i markerade</translation>
</message>
<message>
<source>&amp;Uncheck Selected</source>
<translation>&amp;Bocka ur markerade</translation>
</message>
<message>
<source>&amp;Copy Path(s) to Clipboard</source>
<translation>&amp;Kopiera sökväg(ar) till klippbordet</translation>
</message>
<message>
<source>&lt;No game file&gt;</source>
<translation>&lt;Ingen spelfil&gt;</translation>
</message>
</context>
<context>
<name>Process::ProcessInvoker</name>
<message>
<source>Error starting executable</source>
<translation>Kunde inte starta körbara filen</translation>
</message>
<message>
<source>Error running executable</source>
<translation>Fel när körbara filen skulle köras</translation>
</message>
<message>
<source>
Arguments:
</source>
<translation>
Argument:
</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Could not find %1&lt;/b&gt;&lt;/p&gt;&lt;p&gt;The application is not found.&lt;/p&gt;&lt;p&gt;Please make sure OpenMW is installed correctly and try again.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Kunde inte hitta %1&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Applikationen hittas inte.&lt;/p&gt;&lt;p&gt;Se till att OpenMW är korrekt installerat och försök igen.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Could not start %1&lt;/b&gt;&lt;/p&gt;&lt;p&gt;The application is not executable.&lt;/p&gt;&lt;p&gt;Please make sure you have the right permissions and try again.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Kunde inte starta %1&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Applikationen är inte körbar.&lt;/p&gt;&lt;p&gt;Se till att du har rätt behörigheter och försök igen.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Could not start %1&lt;/b&gt;&lt;/p&gt;&lt;p&gt;An error occurred while starting %1.&lt;/p&gt;&lt;p&gt;Press &quot;Show Details...&quot; for more information.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Kunde inte starta %1&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Ett fel uppstod när %1 skulle startas.&lt;/p&gt;&lt;p&gt;Tryck på &quot;Visa detaljer...&quot; för mer information.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Executable %1 returned an error&lt;/b&gt;&lt;/p&gt;&lt;p&gt;An error occurred while running %1.&lt;/p&gt;&lt;p&gt;Press &quot;Show Details...&quot; for more information.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Körbara filen %1 gav ett felmeddelande&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Ett fel uppstod när %1 kördes.&lt;/p&gt;&lt;p&gt;Tryck på &quot;Visa detaljer...&quot; för mer information.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
</context>
</TS>

1494
files/lang/launcher_sv.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -128,10 +128,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Where should Morrowind be installed?</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/folder.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Morrowind will be installed to the following location. </source>
<translation type="unfinished"></translation>
@ -170,10 +166,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>What is the language of the Morrowind installation?</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/preferences-desktop-locale.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Select the language of the Morrowind installation.</source>
<translation type="unfinished"></translation>
@ -197,10 +189,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Retail CD/DVD</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/system-installer.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Install from a retail disc to a new location.</source>
<translation type="unfinished"></translation>
@ -209,10 +197,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Existing Installation</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/folder.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Select an existing installation.</source>
<translation type="unfinished"></translation>
@ -221,10 +205,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Don&apos;t have a copy?</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/dollar.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Buy the game</source>
<translation type="unfinished"></translation>

View File

@ -128,10 +128,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Where should Morrowind be installed?</source>
<translation>À quel emplacement Morrowind doit-il être installé ?</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/folder.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/folder.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Morrowind will be installed to the following location. </source>
<translation>Morrowind sera installé à l&apos;emplacement suivant :</translation>
@ -170,10 +166,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>What is the language of the Morrowind installation?</source>
<translation>Dans quelle langue est cette installation de Morrowind ?</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/preferences-desktop-locale.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/preferences-desktop-locale.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Select the language of the Morrowind installation.</source>
<translation>Sélectionnez la langue de cette installation de Morrowind.</translation>
@ -197,10 +189,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Retail CD/DVD</source>
<translation>Copie CD/DVD physique</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/system-installer.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/system-installer.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Install from a retail disc to a new location.</source>
<translation>Installer depuis un CD/DVD et choisir la destination sur l&apos;ordinateur.</translation>
@ -209,10 +197,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Existing Installation</source>
<translation>Installation existante</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/folder.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/folder.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Select an existing installation.</source>
<translation>Sélectionnez une installation existante.</translation>
@ -221,10 +205,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Don&apos;t have a copy?</source>
<translation>Vous n&apos;avez pas de copie du jeu ?</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/dollar.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/dollar.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Buy the game</source>
<translation>Achetez le jeu.</translation>

View File

@ -130,10 +130,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Where should Morrowind be installed?</source>
<translation>Куда нужно установить Morrowind?</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/folder.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/folder.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Morrowind will be installed to the following location. </source>
<translation>Morrowind будет установлен в следующее место. </translation>
@ -172,10 +168,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>What is the language of the Morrowind installation?</source>
<translation>Какой язык использует ваша копия Morrowind?</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/preferences-desktop-locale.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&gt;&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/preferences-desktop-locale.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Select the language of the Morrowind installation.</source>
<translation>Выберите язык, используемый вашей копией Morrowind.</translation>
@ -199,10 +191,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Retail CD/DVD</source>
<translation>CD/DVD-диск</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/system-installer.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/system-installer.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Install from a retail disc to a new location.</source>
<translation>Установить игру с диска.</translation>
@ -211,10 +199,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Existing Installation</source>
<translation>Установленная копия игры</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/folder.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/folder.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Select an existing installation.</source>
<translation>Выбрать установленную копию игры.</translation>
@ -223,10 +207,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Don&apos;t have a copy?</source>
<translation>Нет копии игры?</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/dollar.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/tango/48x48/dollar.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Buy the game</source>
<translation>Купить игру</translation>

658
files/lang/wizard_sv.ts Normal file
View File

@ -0,0 +1,658 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="sv_SE">
<context>
<name>ComponentSelectionPage</name>
<message>
<source>WizardPage</source>
<translation>WizardPage</translation>
</message>
<message>
<source>Select Components</source>
<translation>Välj komponenter</translation>
</message>
<message>
<source>Which components should be installed?</source>
<translation>Vilka komponenter ska installeras?</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select which official Morrowind expansions should be installed. For best results, it is recommended to have both expansions installed.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:bold;&quot;&gt;Note:&lt;/span&gt; It is possible to install expansions later by re-running this Wizard.&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Välj vilka officiella Morrowindexpansioner som ska installeras. För bäst resultat rekommenderas att båda expansionerna installeras.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:bold;&quot;&gt;Note:&lt;/span&gt; Det är möjligt att installera expansioner senare genom att köra denna guide igen.&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Selected components:</source>
<translation>Valda komponenter:</translation>
</message>
</context>
<context>
<name>ConclusionPage</name>
<message>
<source>WizardPage</source>
<translation>WizardPage</translation>
</message>
<message>
<source>Completing the OpenMW Wizard</source>
<translation>Färdigställer OpenMWs installationsguide</translation>
</message>
<message>
<source>Placeholder</source>
<translation>Placeholder</translation>
</message>
</context>
<context>
<name>ExistingInstallationPage</name>
<message>
<source>WizardPage</source>
<translation>WizardPage</translation>
</message>
<message>
<source>Select Existing Installation</source>
<translation>Välj befintlig installation</translation>
</message>
<message>
<source>Select an existing installation for OpenMW to use or modify.</source>
<translation>Välj en befintlig installation som OpenMW kan använda eller modifiera.</translation>
</message>
<message>
<source>Detected installations:</source>
<translation>Hittade installationer:</translation>
</message>
<message>
<source>Browse...</source>
<translation>Bläddra...</translation>
</message>
</context>
<context>
<name>ImportPage</name>
<message>
<source>WizardPage</source>
<translation>WizardPage</translation>
</message>
<message>
<source>Import Settings</source>
<translation>Importera inställningar</translation>
</message>
<message>
<source>Import settings from the Morrowind installation.</source>
<translation>Importera inställningar från Morrowindinstallationen.</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;OpenMW needs to import settings from the Morrowind configuration file in order to function properly.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:bold;&quot;&gt;Note:&lt;/span&gt; It is possible to import settings later by re-running this Wizard.&lt;/p&gt;&lt;p/&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;OpenMW behöver importera inställningar från Morrowinds konfigurationsfil för att fungera korrekt.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:bold;&quot;&gt;Notera:&lt;/span&gt; Det är möjligt att importera inställningarna senare genom att köra denna guide igen.&lt;/p&gt;&lt;p/&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Import Settings From Morrowind.ini</source>
<translation>Importera inställningar från Morrowind.ini</translation>
</message>
<message>
<source>Import Add-on and Plugin Selection</source>
<translation>Importera tillägg- och pluginmarkering</translation>
</message>
<message>
<source>Import Bitmap Fonts Setup From Morrowind.ini</source>
<translation>Importera bitmapfonter från Morrowind.ini</translation>
</message>
<message>
<source>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.</source>
<translation>Fonter som kommer med ordinarie spelmotor är suddiga med gränssnittsuppskalning och stödjer bara ett litet antal tecken,
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.</translation>
</message>
</context>
<context>
<name>InstallationPage</name>
<message>
<source>WizardPage</source>
<translation>WizardPage</translation>
</message>
<message>
<source>Installing</source>
<translation>Installerar</translation>
</message>
<message>
<source>Please wait while Morrowind is installed on your computer.</source>
<translation>Vänta medan Morrowind installeras din dator.</translation>
</message>
</context>
<context>
<name>InstallationTargetPage</name>
<message>
<source>WizardPage</source>
<translation>WizardPage</translation>
</message>
<message>
<source>Select Installation Destination</source>
<translation>Välj installationsplats</translation>
</message>
<message>
<source>Where should Morrowind be installed?</source>
<translation>Var ska Morrowind installeras?</translation>
</message>
<message>
<source>Morrowind will be installed to the following location. </source>
<translation>Morrowind kommer installeras följande plats. </translation>
</message>
<message>
<source>Browse...</source>
<translation>Bläddra...</translation>
</message>
</context>
<context>
<name>IntroPage</name>
<message>
<source>WizardPage</source>
<translation>WizardPage</translation>
</message>
<message>
<source>Welcome to the OpenMW Wizard</source>
<translation>Välkommen till OpenMWs installationsguide</translation>
</message>
<message>
<source>This Wizard will help you install Morrowind and its add-ons for OpenMW to use.</source>
<translation>Denna guide hjälper dig att installera Morrowind och expansionerna för OpenMW.</translation>
</message>
</context>
<context>
<name>LanguageSelectionPage</name>
<message>
<source>WizardPage</source>
<translation>WizardPage</translation>
</message>
<message>
<source>Select Morrowind Language</source>
<translation>Välj Morrowinds språk</translation>
</message>
<message>
<source>What is the language of the Morrowind installation?</source>
<translation>Vad är språket Morrowindinstallationen?</translation>
</message>
<message>
<source>Select the language of the Morrowind installation.</source>
<translation>Välj språket Morrowindinstallationen.</translation>
</message>
</context>
<context>
<name>MethodSelectionPage</name>
<message>
<source>WizardPage</source>
<translation>WizardPage</translation>
</message>
<message>
<source>Select Installation Method</source>
<translation>Välj installationsmetod</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select how you would like to install &lt;i&gt;The Elder Scrolls III: Morrowind&lt;/i&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Välj hur du vill installera &lt;i&gt;The Elder Scrolls III: Morrowind&lt;/i&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Retail CD/DVD</source>
<translation>Köpt CD/DVD</translation>
</message>
<message>
<source>Install from a retail disc to a new location.</source>
<translation>Installera från en köpt skiva till en ny plats.</translation>
</message>
<message>
<source>Existing Installation</source>
<translation>Befintlig installation</translation>
</message>
<message>
<source>Select an existing installation.</source>
<translation>Välj en befintlig installation.</translation>
</message>
<message>
<source>Don&apos;t have a copy?</source>
<translation>Äger du inte spelet?</translation>
</message>
<message>
<source>Buy the game</source>
<translation>Köp spelet</translation>
</message>
</context>
<context>
<name>QObject</name>
<message>
<source>&lt;br&gt;&lt;b&gt;Could not find Morrowind.ini&lt;/b&gt;&lt;br&gt;&lt;br&gt;The Wizard needs to update settings in this file.&lt;br&gt;&lt;br&gt;Press &quot;Browse...&quot; to specify the location manually.&lt;br&gt;</source>
<translation>&lt;br&gt;&lt;b&gt;Kunde inte hitta Morrowind.ini&lt;/b&gt;&lt;br&gt;&lt;br&gt;Guiden behöver uppdatera inställningarna i denna fil.&lt;br&gt;&lt;br&gt;Tryck på &quot;Bläddra...&quot; för att specificera filens plats manuellt.&lt;br&gt;</translation>
</message>
<message>
<source>B&amp;rowse...</source>
<translation>B&amp;läddra...</translation>
</message>
<message>
<source>Select configuration file</source>
<translation>Välj konfigurationsfil</translation>
</message>
<message>
<source>&lt;b&gt;Morrowind.bsa&lt;/b&gt; is missing!&lt;br&gt;Make sure your Morrowind installation is complete.</source>
<translation>&lt;b&gt;Morrowind.bsa&lt;/b&gt; saknas!&lt;br&gt;Se till att din Morrowindinstallation är komplett.</translation>
</message>
<message>
<source>&lt;br&gt;&lt;b&gt;There may be a more recent version of Morrowind available.&lt;/b&gt;&lt;br&gt;&lt;br&gt;Do you wish to continue anyway?&lt;br&gt;</source>
<translation>&lt;br&gt;&lt;b&gt;Det kan finnas nyare version av Morrowind tillgänglig.&lt;/b&gt;&lt;br&gt;&lt;br&gt;Vill du fortsätta ändå?&lt;br&gt;</translation>
</message>
<message>
<source>Most recent Morrowind not detected</source>
<translation>Senaste versionen av Morrowind hittades inte</translation>
</message>
<message>
<source>Select a valid %1 installation media.&lt;br&gt;&lt;b&gt;Hint&lt;/b&gt;: make sure that it contains at least one &lt;b&gt;.cab&lt;/b&gt; file.</source>
<translation>Välj ett giltigt %1 installationsmedium.&lt;br&gt;&lt;b&gt;Tips&lt;/b&gt;: säkerställ att det finns åtminstone en &lt;b&gt;.cab&lt;/b&gt;-fil.</translation>
</message>
<message>
<source>There may be a more recent version of Morrowind available.&lt;br&gt;&lt;br&gt;Do you wish to continue anyway?</source>
<translation>Det kan finnas en mer uppdaterad version av Morrowind tillgänglig.&lt;br&gt;&lt;br&gt;Vill du fortsätta ändå?</translation>
</message>
</context>
<context>
<name>Wizard::ComponentSelectionPage</name>
<message>
<source>&amp;Install</source>
<translation>&amp;Installera</translation>
</message>
<message>
<source>&amp;Skip</source>
<translation>&amp;Hoppa över</translation>
</message>
<message>
<source>Morrowind (installed)</source>
<translation>Morrowind (installerat)</translation>
</message>
<message>
<source>Morrowind</source>
<translation>Morrowind</translation>
</message>
<message>
<source>Tribunal (installed)</source>
<translation>Tribunal (installerat)</translation>
</message>
<message>
<source>Tribunal</source>
<translation>Tribunal</translation>
</message>
<message>
<source>Bloodmoon (installed)</source>
<translation>Bloodmoon (installerat)</translation>
</message>
<message>
<source>Bloodmoon</source>
<translation>Bloodmoon</translation>
</message>
<message>
<source>About to install Tribunal after Bloodmoon</source>
<translation> väg att installera Tribunal efter Bloodmoon</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;You are about to install Tribunal&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Bloodmoon is already installed on your computer.&lt;/p&gt;&lt;p&gt;However, it is recommended that you install Tribunal before Bloodmoon.&lt;/p&gt;&lt;p&gt;Would you like to re-install Bloodmoon?&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Du håller att installera Tribunal&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Bloodmoon finns redan installerat på din dator.&lt;/p&gt;&lt;p&gt;Det är dock rekommenderat att du installerar Tribunal före Bloodmoon.&lt;/p&gt;&lt;p&gt;Vill du ominstallera Bloodmoon?&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Re-install &amp;Bloodmoon</source>
<translation>Ominstallera &amp;Bloodmoon</translation>
</message>
</context>
<context>
<name>Wizard::ConclusionPage</name>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The OpenMW Wizard successfully installed Morrowind on your computer.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;OpenMWs Installationsguide har installerat Morrowind din dator.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The OpenMW Wizard successfully modified your existing Morrowind installation.&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;OpenMWs installationsguide har justerat din befintliga Morrowindinstallation.&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The OpenMW Wizard failed to install Morrowind on your computer.&lt;/p&gt;&lt;p&gt;Please report any bugs you might have encountered to our &lt;a href=&quot;https://gitlab.com/OpenMW/openmw/issues&quot;&gt;bug tracker&lt;/a&gt;.&lt;br/&gt;Make sure to include the installation log.&lt;/p&gt;&lt;br/&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;OpenMWs installationsguide misslyckades med att installera Morrowind din dator.&lt;/p&gt;&lt;p&gt;Vänligen rapportera eventuella buggar på vår &lt;a href=&quot;https://gitlab.com/OpenMW/openmw/issues&quot;&gt;bug tracker&lt;/a&gt;.&lt;br/&gt;Se till att du inkluderar installationsloggen.&lt;/p&gt;&lt;br/&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
</context>
<context>
<name>Wizard::ExistingInstallationPage</name>
<message>
<source>No existing installations detected</source>
<translation>Inga befintliga installationer hittades</translation>
</message>
<message>
<source>Error detecting Morrowind configuration</source>
<translation>Kunde inte hitta Morrowind-konfigurationsfil</translation>
</message>
<message>
<source>Morrowind configuration file (*.ini)</source>
<translation>Morrowind konfigurationsfil (*.ini)</translation>
</message>
<message>
<source>Select Morrowind.esm (located in Data Files)</source>
<translation>Markera Morrowind.esm (hittas i Data Files)</translation>
</message>
<message>
<source>Morrowind master file (Morrowind.esm)</source>
<translation>Morrowind masterfil (Morrowind.esm)</translation>
</message>
<message>
<source>Error detecting Morrowind files</source>
<translation>Kunde inte hitta Morrowindfiler</translation>
</message>
</context>
<context>
<name>Wizard::InstallationPage</name>
<message>
<source>&lt;p&gt;Attempting to install component %1.&lt;/p&gt;</source>
<translation>&lt;p&gt;Försöker installera komponent %1.&lt;/p&gt;</translation>
</message>
<message>
<source>Attempting to install component %1.</source>
<translation>Försöker installera komponent %1.</translation>
</message>
<message>
<source>%1 Installation</source>
<translation>%1 Installation</translation>
</message>
<message>
<source>Select %1 installation media</source>
<translation>Välj %1 installationsmedia</translation>
</message>
<message>
<source>&lt;p&gt;&lt;br/&gt;&lt;span style=&quot;color:red;&quot;&gt;&lt;b&gt;Error: The installation was aborted by the user&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</source>
<translation>&lt;p&gt;&lt;br/&gt;&lt;span style=&quot;color:red;&quot;&gt;&lt;b&gt;Fel: Installationen avbröts av användaren&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</translation>
</message>
<message>
<source>&lt;p&gt;Detected old version of component Morrowind.&lt;/p&gt;</source>
<translation>&lt;p&gt;Hittade gammal version av Morrowind.&lt;/p&gt;</translation>
</message>
<message>
<source>Detected old version of component Morrowind.</source>
<translation>Hittade gammal version av komponenten Morrowind.</translation>
</message>
<message>
<source>Morrowind Installation</source>
<translation>Installation av Morrowind</translation>
</message>
<message>
<source>Installation finished</source>
<translation>Installationen klar</translation>
</message>
<message>
<source>Installation completed successfully!</source>
<translation>Installationen slutfördes!</translation>
</message>
<message>
<source>Installation failed!</source>
<translation>Installationen misslyckades!</translation>
</message>
<message>
<source>&lt;p&gt;&lt;br/&gt;&lt;span style=&quot;color:red;&quot;&gt;&lt;b&gt;Error: %1&lt;/b&gt;&lt;/p&gt;</source>
<translation>&lt;p&gt;&lt;br/&gt;&lt;span style=&quot;color:red;&quot;&gt;&lt;b&gt;Fel: %1&lt;/b&gt;&lt;/p&gt;</translation>
</message>
<message>
<source>&lt;p&gt;&lt;span style=&quot;color:red;&quot;&gt;&lt;b&gt;%1&lt;/b&gt;&lt;/p&gt;</source>
<translation>&lt;p&gt;&lt;span style=&quot;color:red;&quot;&gt;&lt;b&gt;%1&lt;/b&gt;&lt;/p&gt;</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;The Wizard has encountered an error&lt;/b&gt;&lt;/p&gt;&lt;p&gt;The error reported was:&lt;/p&gt;&lt;p&gt;%1&lt;/p&gt;&lt;p&gt;Press &amp;quot;Show Details...&amp;quot; for more information.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Guiden har stött ett fel&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Felet som rapporterades var:&lt;/p&gt;&lt;p&gt;%1&lt;/p&gt;&lt;p&gt;Tryck på &amp;quot;Visa detaljer...&amp;quot; för mer information.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>An error occurred</source>
<translation>Ett fel uppstod</translation>
</message>
</context>
<context>
<name>Wizard::InstallationTargetPage</name>
<message>
<source>Error creating destination</source>
<translation>Fel när målkatalogen skulle skapas</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Could not create the destination directory&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Please make sure you have the right permissions and try again, or specify a different location.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Kunde inte skapa målkatalogen&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Se till att du har rätt behörigheter och försök igen, eller specificera en annan katalog.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Could not write to the destination directory&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Please make sure you have the right permissions and try again, or specify a different location.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Kunde inte skriva till målkatalogen&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Se till att du har rätt behörigheter och försök igen, eller specificera en annan katalog.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;The destination directory is not empty&lt;/b&gt;&lt;/p&gt;&lt;p&gt;An existing Morrowind installation is present in the specified location.&lt;/p&gt;&lt;p&gt;Please specify a different location, or go back and select the location as an existing installation.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Målkatalogen är inte tom&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Det finns en befintlig Morrowindinstallation i den specificerade katalogen.&lt;/p&gt;&lt;p&gt;Välj en annan katalog eller gå tillbaka och välj katalogen som en befintlig installation.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Insufficient permissions</source>
<translation>Otillräckliga behörigheter</translation>
</message>
<message>
<source>Destination not empty</source>
<translation>Platsen är inte tom</translation>
</message>
<message>
<source>Select where to install Morrowind</source>
<translation>Välj där Morrowind ska installeras</translation>
</message>
</context>
<context>
<name>Wizard::LanguageSelectionPage</name>
<message>
<source>English</source>
<translation>Engelska</translation>
</message>
<message>
<source>French</source>
<translation>Franska</translation>
</message>
<message>
<source>German</source>
<translation>Tyska</translation>
</message>
<message>
<source>Italian</source>
<translation>Italienska</translation>
</message>
<message>
<source>Polish</source>
<translation>Polska</translation>
</message>
<message>
<source>Russian</source>
<translation>Ryska</translation>
</message>
<message>
<source>Spanish</source>
<translation>Spanska</translation>
</message>
</context>
<context>
<name>Wizard::MainWizard</name>
<message>
<source>OpenMW Wizard</source>
<translation>OpenMW installationsguide</translation>
</message>
<message>
<source>Error opening Wizard log file</source>
<translation>Kunde inte öppna guidens loggfil</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Could not open %1 for writing&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Please make sure you have the right permissions and try again.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Kunde inte öppna %1 för att skriva&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Se till att du har rätt behörigheter och försök igen.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Could not open %1 for reading&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Please make sure you have the right permissions and try again.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Kunde inte öppna %1 för läsning&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Se till att du har rätt behörigheter och försök igen.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Error opening OpenMW configuration file</source>
<translation>Fel när OpenMW-konfigurationsfil skulle öppnas</translation>
</message>
<message>
<source>Quit Wizard</source>
<translation>Avsluta guiden</translation>
</message>
<message>
<source>Are you sure you want to exit the Wizard?</source>
<translation>Är du säker att du vill avsluta guiden?</translation>
</message>
<message>
<source>Error creating OpenMW configuration directory</source>
<translation>Fel vid skapande av OpenMW-konfigurationskatalog</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Could not create %1&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Please make sure you have the right permissions and try again.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;Kunde inte skapa %1&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Se till att du har rätt behörigheter och försök igen.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Error writing OpenMW configuration file</source>
<translation>Kunde inte skriva OpenMW-konfigurationsfil</translation>
</message>
</context>
<context>
<name>Wizard::UnshieldWorker</name>
<message>
<source>Failed to open Morrowind configuration file!</source>
<translation>Kunde inte öppna en Morrowind-konfigurationsfil!</translation>
</message>
<message>
<source>Opening %1 failed: %2.</source>
<translation>Öppnar %1 misslyckad: %2.</translation>
</message>
<message>
<source>Failed to write Morrowind configuration file!</source>
<translation>Kunde inte skriva en Morrowind-konfigurationsfil!</translation>
</message>
<message>
<source>Writing to %1 failed: %2.</source>
<translation>Skriver till %1 misslyckad: %2.</translation>
</message>
<message>
<source>Installing: %1</source>
<translation>Installerar: %1</translation>
</message>
<message>
<source>Installing: %1 directory</source>
<translation>Installerar: %1 katalog</translation>
</message>
<message>
<source>Installation finished!</source>
<translation>Installationen slutförd!</translation>
</message>
<message>
<source>Component parameter is invalid!</source>
<translation>Komponentparametern är ogiltig!</translation>
</message>
<message>
<source>An invalid component parameter was supplied.</source>
<translation>En ogiltig komponentparameter angavs.</translation>
</message>
<message>
<source>Failed to find a valid archive containing %1.bsa! Retrying.</source>
<translation>Misslyckades att hitta ett giltigt arkiv som innehåller %1.bsa! Försöker igen.</translation>
</message>
<message>
<source>Installing %1</source>
<translation>Installerar %1</translation>
</message>
<message>
<source>Installation media path not set!</source>
<translation>Sökväg till installationsmedia inte inställd!</translation>
</message>
<message>
<source>The source path for %1 was not set.</source>
<translation>Källsökvägen för %1 ställdes inte in.</translation>
</message>
<message>
<source>Cannot create temporary directory!</source>
<translation>Kan inte skapa temporär katalog!</translation>
</message>
<message>
<source>Failed to create %1.</source>
<translation>Kunde inte skapa %1.</translation>
</message>
<message>
<source>Cannot move into temporary directory!</source>
<translation>Kan inte flytta till temporär katalog!</translation>
</message>
<message>
<source>Failed to move into %1.</source>
<translation>Misslyckades att flytta till %1.</translation>
</message>
<message>
<source>Moving installation files</source>
<translation>Flyttar installationsfiler</translation>
</message>
<message>
<source>Could not install directory!</source>
<translation>Kunde inte installera katalog!</translation>
</message>
<message>
<source>Installing %1 to %2 failed.</source>
<translation>Installation %1 till %2 misslyckades.</translation>
</message>
<message>
<source>Could not install translation file!</source>
<translation>Kunde inte installera översättningsfil!</translation>
</message>
<message>
<source>Failed to install *%1 files.</source>
<translation>Kunde inte installera *%1 filer.</translation>
</message>
<message>
<source>Could not install Morrowind data file!</source>
<translation>Kunde inte installera Morrowind-datafil!</translation>
</message>
<message>
<source>Failed to install %1.</source>
<translation>Misslyckades att installera %1.</translation>
</message>
<message>
<source>Could not install Morrowind configuration file!</source>
<translation>Kunde inte installera Morrowind-konfigurationsfil!</translation>
</message>
<message>
<source>Installing: Sound directory</source>
<translation>Installerar: Ljudkatalog</translation>
</message>
<message>
<source>Could not find Tribunal data file!</source>
<translation>Tribunal-datafil hittades inte!</translation>
</message>
<message>
<source>Failed to find %1.</source>
<translation>Misslyckades att hitta %1.</translation>
</message>
<message>
<source>Could not find Tribunal patch file!</source>
<translation>Tribunal-patchfil hittades inte!</translation>
</message>
<message>
<source>Could not find Bloodmoon data file!</source>
<translation>Bloodmoon-datafil hittades inte!</translation>
</message>
<message>
<source>Updating Morrowind configuration file</source>
<translation>Uppdaterar Morrowind-konfigurationsfil</translation>
</message>
<message>
<source>%1 installation finished!</source>
<translation>%1 installation klar!</translation>
</message>
<message>
<source>Extracting: %1</source>
<translation>Extraherar: %1</translation>
</message>
<message>
<source>Failed to open InstallShield Cabinet File.</source>
<translation>Misslyckades att öppna en InstallShield Cabinet-fil.</translation>
</message>
<message>
<source>Opening %1 failed.</source>
<translation>Misslyckades att öppna %1.</translation>
</message>
<message>
<source>Failed to extract %1.</source>
<translation>Misslyckades att extrahera %1.</translation>
</message>
<message>
<source>Complete path: %1</source>
<translation>Färdigställ sökväg: %1</translation>
</message>
</context>
</TS>

501
files/opencs/configure.svg Normal file
View File

@ -0,0 +1,501 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
<svg
version="1.0"
id="Livello_1"
width="128"
height="128.00018"
viewBox="0 0 119.999 120.061"
overflow="visible"
enable-background="new 0 0 119.999 120.061"
xml:space="preserve"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:ns="http://ns.adobe.com/SaveForWeb/1.0/"><defs
id="defs127"><linearGradient
id="linearGradient3291"><stop
style="stop-color:black;stop-opacity:1"
offset="0"
id="stop3293" /><stop
style="stop-color:black;stop-opacity:0"
offset="1"
id="stop3295" /></linearGradient><radialGradient
xlink:href="#linearGradient3291"
id="radialGradient3336"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,0.197802,0,92.82166)"
cx="63.912209"
cy="115.70919"
fx="33.953373"
fy="115.70919"
r="63.912209" /><linearGradient
xlink:href="#XMLID_11_"
id="linearGradient9291"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.1250507,0.1245433,0.1244801,1.1244802,-2.8257128,-11.708873)"
x1="41.188999"
y1="80.101601"
x2="27.6451"
y2="66.557701" /><linearGradient
id="linearGradient6141"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop6143" /><stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop6145" /></linearGradient><linearGradient
gradientTransform="translate(-3.0255781e-2,3.056501e-4)"
id="linearGradient5166"
gradientUnits="userSpaceOnUse"
x1="77.542931"
y1="41.868668"
x2="88.108376"
y2="31.303225"><stop
offset="0"
style="stop-color:#323232;stop-opacity:1;"
id="stop5168" /><stop
offset="0.75739998"
style="stop-color:#ffffff;stop-opacity:0;"
id="stop5170" /></linearGradient><linearGradient
y2="31.303225"
x2="88.108376"
y1="41.868668"
x1="77.542931"
gradientUnits="userSpaceOnUse"
id="XMLID_16_"
gradientTransform="translate(-3.0255781e-2,3.056501e-4)"><stop
id="stop92"
style="stop-color:#ffffff;stop-opacity:1;"
offset="0" /><stop
id="stop106"
style="stop-color:#ffffff;stop-opacity:0.01898734;"
offset="0.75739998" /></linearGradient><linearGradient
xlink:href="#XMLID_18_"
id="linearGradient3295"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7074656,-0.7074656,0.7071068,0.7071068,-18.749172,36.539651)"
x1="-6.165"
y1="112.5049"
x2="11.0181"
y2="112.5049" /><linearGradient
xlink:href="#XMLID_17_"
id="linearGradient3298"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0002442,-2.5378062e-4,-2.5378062e-4,1.0002442,8.6347844,-24.632129)"
x1="38.757801"
y1="7.4277"
x2="63.923302"
y2="7.4277" /><linearGradient
xlink:href="#XMLID_16_"
id="linearGradient3301"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7074656,-0.7074656,0.7071068,0.7071068,-24.896055,39.032512)"
x1="76.281174"
y1="43.060352"
x2="86.611"
y2="31.303225" /><linearGradient
xlink:href="#XMLID_15_"
id="linearGradient3304"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7074656,-0.7074656,0.7071068,0.7071068,-24.895791,39.125846)"
x1="76.919403"
y1="43.1436"
x2="100.6411"
y2="19.4214" /><linearGradient
xlink:href="#XMLID_14_"
id="linearGradient3307"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7074656,-0.7074656,0.7071068,0.7071068,-18.749172,36.539651)"
x1="67.880402"
y1="6.4331002"
x2="105.3924"
y2="43.945599" /><linearGradient
xlink:href="#XMLID_13_"
id="linearGradient3310"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.7074656,0.7074656,-0.7071068,-0.7071068,101.2385,185.78751)"
x1="-0.47799999"
y1="105.0586"
x2="6.3157001"
y2="111.8523" /><linearGradient
xlink:href="#XMLID_12_"
id="linearGradient3313"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.7074656,0.7074656,-0.7071068,-0.7071068,107.38512,183.20132)"
x1="1.3501"
y1="103.2324"
x2="8.1437998"
y2="110.0261" /><linearGradient
id="XMLID_10_"
gradientUnits="userSpaceOnUse"
x1="61.9692"
y1="88.430702"
x2="40.815899"
y2="88.430702"
gradientTransform="matrix(0.7074589,0.7071,-0.7074589,0.7071,68.76314,-21.373694)"><stop
offset="0.0533"
style="stop-color:#555555"
id="stop10" /><stop
offset="0.21113414"
style="stop-color:#BBBBBB"
id="stop12" /><stop
offset="0.36265489"
style="stop-color:#ffffff;stop-opacity:1;"
id="stop14" /><stop
offset="0.60322773"
style="stop-color:#ffffff;stop-opacity:1;"
id="stop16" /><stop
offset="0.84976584"
style="stop-color:#e9e9e9;stop-opacity:1;"
id="stop18" /><stop
offset="1"
style="stop-color:#555555"
id="stop20" /></linearGradient><linearGradient
xlink:href="#XMLID_10_"
id="linearGradient10168"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.5628683,-3.9649986e-4,-3.9652886e-4,1.562754,-20.23462,-103.35726)"
x1="61.9692"
y1="88.430702"
x2="40.815899"
y2="88.430702" /><linearGradient
xlink:href="#XMLID_10_"
id="linearGradient10170"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0663866,2.7056218e-4,-2.7056218e-4,-1.0663866,9.2387294,154.26114)"
x1="61.9692"
y1="88.430702"
x2="40.815899"
y2="88.430702" /><linearGradient
xlink:href="#XMLID_17_"
id="linearGradient10203"
x1="56.03125"
y1="121.5"
x2="72.031251"
y2="121.5"
gradientUnits="userSpaceOnUse" /><linearGradient
xlink:href="#XMLID_17_"
id="linearGradient10205"
x1="56.03125"
y1="110.5"
x2="72.031251"
y2="110.5"
gradientUnits="userSpaceOnUse" /><linearGradient
xlink:href="#XMLID_10_"
id="linearGradient10219"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0663866,2.7056218e-4,-2.7056218e-4,-1.0663866,9.2387294,154.26114)"
x1="61.9692"
y1="88.430702"
x2="40.815899"
y2="88.430702" /><linearGradient
xlink:href="#XMLID_17_"
id="linearGradient10221"
gradientUnits="userSpaceOnUse"
x1="56.03125"
y1="110.5"
x2="72.031251"
y2="110.5" /><linearGradient
xlink:href="#XMLID_14_"
id="linearGradient10223"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7074656,-0.7074656,0.7071068,0.7071068,-18.749172,36.539651)"
x1="67.880402"
y1="6.4331002"
x2="105.3924"
y2="43.945599" /><linearGradient
xlink:href="#XMLID_15_"
id="linearGradient10225"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7074656,-0.7074656,0.7071068,0.7071068,-24.895791,39.125846)"
x1="76.919403"
y1="43.1436"
x2="100.6411"
y2="19.4214" /><linearGradient
xlink:href="#XMLID_16_"
id="linearGradient10227"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7074656,-0.7074656,0.7071068,0.7071068,-24.895791,39.125846)"
x1="76.281174"
y1="43.060352"
x2="86.611"
y2="31.303225" /><linearGradient
xlink:href="#XMLID_17_"
id="linearGradient10229"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0002442,-2.5378062e-4,-2.5378062e-4,1.0002442,8.6347844,-24.632129)"
x1="38.757801"
y1="7.4277"
x2="63.923302"
y2="7.4277" /><linearGradient
xlink:href="#XMLID_17_"
id="linearGradient10231"
gradientUnits="userSpaceOnUse"
x1="56.03125"
y1="121.5"
x2="72.031251"
y2="121.5" /><linearGradient
xlink:href="#XMLID_18_"
id="linearGradient10233"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7074656,-0.7074656,0.7071068,0.7071068,-18.749172,36.539651)"
x1="-6.165"
y1="112.5049"
x2="11.0181"
y2="112.5049" /><linearGradient
xlink:href="#XMLID_18_"
id="linearGradient10236"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0005075,0,0,1.0000001,6.1427973,2.5178157)"
x1="-6.165"
y1="112.5049"
x2="11.0181"
y2="112.5049" /><linearGradient
xlink:href="#XMLID_17_"
id="linearGradient10239"
gradientUnits="userSpaceOnUse"
x1="56.03125"
y1="121.5"
x2="72.031251"
y2="121.5"
gradientTransform="matrix(0.6632486,0.6632486,-0.6632486,0.6632486,45.21604,-10.083813)" /><linearGradient
xlink:href="#XMLID_17_"
id="linearGradient10242"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7074589,0.7071,-0.7074589,0.7071,68.761161,-21.373784)"
x1="38.757801"
y1="7.4277"
x2="63.923302"
y2="7.4277" /><linearGradient
xlink:href="#XMLID_16_"
id="linearGradient10245"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0005075,0,0,1.0000001,-3.223487e-2,2.1565487e-4)"
x1="76.281174"
y1="43.060352"
x2="86.611"
y2="31.303225" /><linearGradient
xlink:href="#XMLID_15_"
id="linearGradient10248"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0005075,0,0,1.0000001,-3.223487e-2,2.1565487e-4)"
x1="76.919403"
y1="43.1436"
x2="100.6411"
y2="19.4214" /><linearGradient
xlink:href="#XMLID_14_"
id="linearGradient10251"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0005075,0,0,1.0000001,6.1427973,2.5178157)"
x1="67.880402"
y1="6.4331002"
x2="105.3924"
y2="43.945599" /><linearGradient
xlink:href="#XMLID_17_"
id="linearGradient10254"
gradientUnits="userSpaceOnUse"
x1="56.03125"
y1="110.5"
x2="72.031251"
y2="110.5"
gradientTransform="matrix(0.6632486,0.6632486,-0.6632486,0.6632486,45.21604,-10.083813)" /><linearGradient
xlink:href="#XMLID_10_"
id="linearGradient10257"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7071,0.7074589,0.7071,-0.7074589,-50.969871,98.357246)"
x1="61.9692"
y1="88.430702"
x2="40.815899"
y2="88.430702" /><linearGradient
xlink:href="#XMLID_17_"
id="linearGradient10260"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7074589,0.7071,-0.7074589,0.7071,68.761161,-21.373784)"
x1="51.339096"
y1="30.159527"
x2="51.326984"
y2="-17.582758" /><linearGradient
xlink:href="#linearGradient6141"
id="linearGradient6147"
x1="7.2493796"
y1="120.2508"
x2="57.625931"
y2="69.874252"
gradientUnits="userSpaceOnUse" /></defs><metadata
id="metadata3"><ns:sfw><ns:slices /><ns:sliceSourceBounds
y="3.97"
x="4"
height="120.061"
width="119.999"
bottomLeftOrigin="true" /></ns:sfw><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><linearGradient
id="XMLID_11_"
gradientUnits="userSpaceOnUse"
x1="41.188999"
y1="80.101601"
x2="27.6451"
y2="66.557701"
gradientTransform="translate(8.7075442,3.5814057)"><stop
offset="0.40830001"
style="stop-color:#f0f0f0;stop-opacity:1;"
id="stop25" /><stop
offset="0.50628459"
style="stop-color:#C3C3C3"
id="stop27" /><stop
offset="0.56008232"
style="stop-color:#8b8b8b;stop-opacity:1;"
id="stop29" /><stop
id="stop12212"
style="stop-color:#393939;stop-opacity:1;"
offset="0.67944592" /></linearGradient><linearGradient
id="XMLID_12_"
gradientUnits="userSpaceOnUse"
x1="1.3501"
y1="103.2324"
x2="8.1437998"
y2="110.0261"
gradientTransform="matrix(-1,0,0,-1,18.150444,221.94371)"><stop
offset="0"
style="stop-color:#EEEEEE"
id="stop36" /><stop
offset="1"
style="stop-color:#BBBBBB"
id="stop38" /></linearGradient><linearGradient
id="XMLID_13_"
gradientUnits="userSpaceOnUse"
x1="-0.47799999"
y1="105.0586"
x2="6.3157001"
y2="111.8523"
gradientTransform="matrix(-1,0,0,-1,11.978544,219.42611)"><stop
offset="0"
style="stop-color:#BBBBBB"
id="stop43" /><stop
offset="1"
style="stop-color:#555555"
id="stop45" /></linearGradient><linearGradient
id="XMLID_14_"
gradientUnits="userSpaceOnUse"
x1="67.880402"
y1="6.4331002"
x2="105.3924"
y2="43.945599"
gradientTransform="translate(6.1416442,2.5179057)"><stop
offset="0.0947"
style="stop-color:#666666;stop-opacity:1;"
id="stop50" /><stop
id="stop3330"
style="stop-color:#b7b7b7;stop-opacity:1;"
offset="0.40963161" /><stop
offset="0.50530702"
style="stop-color:#ffffff;stop-opacity:1;"
id="stop56" /><stop
id="stop3328"
style="stop-color:#a3a3a3;stop-opacity:1;"
offset="0.63043118" /><stop
offset="0.93489999"
style="stop-color:#646464;stop-opacity:1;"
id="stop62" /></linearGradient><linearGradient
id="XMLID_15_"
gradientUnits="userSpaceOnUse"
x1="76.919403"
y1="43.1436"
x2="100.6411"
y2="19.4214"
gradientTransform="translate(-3.0255781e-2,3.056501e-4)"><stop
offset="0"
style="stop-color:#aeaeae;stop-opacity:1;"
id="stop67" /><stop
offset="0.0602"
style="stop-color:#b2b2b2;stop-opacity:1;"
id="stop69" /><stop
offset="0.18103883"
style="stop-color:#d9d9d9;stop-opacity:1;"
id="stop71" /><stop
offset="0.3195"
style="stop-color:#ffffff;stop-opacity:1;"
id="stop73" /><stop
offset="0.43529999"
style="stop-color:#f4f4f4;stop-opacity:1;"
id="stop75" /><stop
offset="0.6028"
style="stop-color:#D4D4D4"
id="stop77" /><stop
offset="0.7633"
style="stop-color:#BBBBBB"
id="stop79" /><stop
offset="0.80790001"
style="stop-color:#b9bcc1;stop-opacity:1;"
id="stop81" /><stop
offset="0.92970002"
style="stop-color:#bdc0c3;stop-opacity:1;"
id="stop83" /><stop
offset="1"
style="stop-color:#b5b9bb;stop-opacity:1;"
id="stop85" /></linearGradient><linearGradient
id="XMLID_17_"
gradientUnits="userSpaceOnUse"
x1="38.757801"
y1="7.4277"
x2="63.923302"
y2="7.4277"
gradientTransform="matrix(0.7071,0.7071,-0.7071,0.7071,68.728244,-21.373694)"><stop
offset="0"
style="stop-color:#3a3a3a;stop-opacity:1;"
id="stop111" /><stop
offset="0.60194176"
style="stop-color:#ffffff;stop-opacity:1;"
id="stop113" /><stop
offset="1"
style="stop-color:#888888"
id="stop115" /></linearGradient><linearGradient
id="XMLID_18_"
gradientUnits="userSpaceOnUse"
x1="-6.165"
y1="112.5049"
x2="11.0181"
y2="112.5049"
gradientTransform="translate(6.1416442,2.5179057)"><stop
offset="0"
style="stop-color:#BBBBBB"
id="stop120" /><stop
offset="1"
style="stop-color:#000000"
id="stop122" /></linearGradient><path
style="fill:url(#linearGradient10257)"
d="M 76.409451,31.804481 C 75.443086,30.924606 74.357677,30.748097 72.740857,32.364097 L 2.6023174,103.87059 C 2.5703077,103.90258 2.569153,103.93922 2.5401379,103.97422 C -0.95363288,107.73022 -0.88972357,113.60492 2.7681296,117.25992 C 6.4259848,120.91592 12.29792,120.98291 16.053828,117.48791 C 16.088848,117.45991 16.126446,117.45673 16.157461,117.42573 L 87.72613,47.307917 C 90.960771,44.076918 86.953486,42.972159 84.679332,40.696158 L 79.373343,35.390169 C 78.234266,34.251669 77.375816,32.684356 76.409451,31.804481 z M 16.053828,104.01567 C 18.984982,106.94689 18.986335,111.69819 16.053828,114.62765 C 13.124457,117.55553 8.3745634,117.55731 5.4418505,114.62765 C 2.5091357,111.69799 2.5109065,106.94669 5.4418505,104.01567 C 8.3727812,101.08623 13.121121,101.086 16.053828,104.01567 z "
id="path10166" /><path
style="fill:url(#linearGradient10254)"
d="M 17.380325,102.68918 C 14.447618,99.759506 9.6992784,99.759734 6.7683477,102.68918 C 6.5428061,102.91472 6.3373933,103.14849 6.1465522,103.39388 C 9.0905761,101.11028 13.348454,101.3131 16.053828,104.01567 C 18.759425,106.72134 18.965866,110.98021 16.675624,113.92295 C 16.920967,113.73233 17.15491,113.52645 17.380325,113.30115 C 20.312833,110.3717 20.31148,105.6204 17.380325,102.68918 z "
id="path10186" /><path
d="M 118.731,16.620217 L 103.19512,32.150217 C 99.80841,35.534217 93.549236,33.235217 90.160517,29.850217 C 86.776801,26.465216 84.475632,20.211217 87.86035,16.825217 L 103.40023,1.2962158 C 92.523716,-2.8617843 79.067889,3.9452161 74.82974,8.1812158 C 63.636062,19.367217 62.837657,34.722217 74.065352,45.942217 C 85.286043,57.160217 100.64883,56.490217 111.84151,45.303218 C 116.07966,41.066217 122.89111,27.489217 118.731,16.620217 z "
id="path64"
style="fill:url(#linearGradient10251)" /><path
d="M 77.659175,11.009216 C 72.74068,15.925216 70.031306,21.867217 70.031306,27.741217 L 70.030305,27.742217 C 70.031306,33.311217 72.404509,38.626217 76.894787,43.113217 C 81.422083,47.639217 86.7778,50.031218 92.381644,50.031217 L 92.381644,50.031217 C 98.22961,50.031218 104.1346,47.348218 109.01107,42.474217 C 111.8225,39.664217 115.5714,32.245217 115.93458,25.071217 C 111.35626,29.647217 106.02356,34.977217 106.02356,34.977217 C 100.96199,40.037217 92.379643,37.724217 87.331082,32.680217 C 84.259524,29.607217 82.198478,25.228217 82.198477,21.129217 C 82.199478,18.494217 83.050911,15.977216 85.028913,13.997217 C 85.029914,13.996217 90.524701,8.5042158 94.982962,4.049216 C 87.471153,4.3972165 80.050388,8.6212158 77.659175,11.009216 z "
id="path87"
style="fill:url(#linearGradient10248);fill-opacity:1.0" /><path
d="M 85.387095,16.818216 C 81.792271,20.411216 84.661727,27.477217 88.592721,31.406217 C 92.527718,35.337217 99.594302,38.205217 103.19012,34.613217 C 103.19012,34.613217 118.96412,17.234216 118.73,16.620217 L 103.19412,32.150217 C 99.80741,35.534218 93.548235,33.235217 90.159515,29.850217 C 86.775799,26.465216 84.474633,20.211217 87.85935,16.825217 L 103.39923,1.2962158 L 85.387095,16.818216 z "
id="path117"
style="fill:url(#linearGradient10260)" /><path
style="fill:url(#linearGradient10239)"
d="M 4.820055,104.72037 C 4.5726725,104.91226 4.3425471,105.11509 4.1153533,105.34217 C 1.1844093,108.27319 1.1826385,113.02448 4.1153533,115.95415 C 7.0480662,118.88381 11.79796,118.88203 14.727331,115.95415 C 14.952993,115.72872 15.158195,115.49477 15.349126,115.24945 C 12.406133,117.53606 8.148893,117.33187 5.4418505,114.62765 C 2.734806,111.92343 2.5308464,107.66392 4.820055,104.72037 z "
id="path10190" /><path
d="M 2.7691837,116.26123 C 1.0242958,114.51723 0.10383016,112.26923 -0.011225313,109.98622 C -0.15229878,112.60123 0.76716563,115.26323 2.7691816,117.26123 C 6.4270358,120.91723 12.302016,120.99023 16.057925,117.49522 C 16.092943,117.46722 16.134962,117.44623 16.165977,117.41523 L 17.166488,116.41622 C 13.410582,119.90922 6.427038,119.91723 2.7691837,116.26123 z "
id="path124"
style="fill:url(#linearGradient10236)" /><path
style="fill:url(#linearGradient6147);fill-opacity:1.0;fill-rule:nonzero;opacity:0.5"
d="M 43.21875 69.5625 L 2.8125 110.75 C 2.7783735 110.78411 2.7809338 110.80644 2.75 110.84375 C -0.97480066 114.84812 -0.93098327 121.10331 2.96875 125 C 6.8684854 128.89776 13.151978 128.97611 17.15625 125.25 C 17.193586 125.22015 17.216934 125.22055 17.25 125.1875 L 57.3125 85.9375 C 53.66929 79.550692 48.861858 73.979783 43.21875 69.5625 z M 11.5 108.5625 C 13.547005 108.56247 15.592932 109.34455 17.15625 110.90625 C 20.281231 114.0313 20.282673 119.09558 17.15625 122.21875 C 14.033171 125.34024 8.9703922 125.34214 5.84375 122.21875 C 2.7171056 119.09537 2.7189937 114.03109 5.84375 110.90625 C 7.406121 109.34467 9.4529953 108.56253 11.5 108.5625 z "
id="path2236"
transform="matrix(0.9379752,0,0,0.9379752,-3.0914126e-2,0)" /><path
d="M 15.537923,96.062539 C 14.713663,96.978699 14.388162,98.22441 14.530816,99.52305 C 14.682682,100.89492 15.361817,102.33182 16.533505,103.50003 C 18.808146,105.77775 22.084895,106.19602 23.991153,104.45332 C 24.002148,104.47231 67.492075,61.01118 77.726913,50.782827 C 77.294045,50.416137 69.737251,42.875414 69.288707,42.347047 C 66.356033,45.277862 15.537923,96.062539 15.537923,96.062539 z "
id="path33"
style="fill:url(#linearGradient9291);opacity:0.5" /><path
style="fill:url(#linearGradient3301)"
id="path108"
d="M 94.304331,48.937972 C 88.699488,48.937972 83.344772,46.545972 78.817475,42.019972 C 74.327198,37.532972 71.953994,32.217972 71.952993,26.648972 L 71.953994,26.647972 C 71.953994,20.773972 74.663368,14.831973 79.581863,9.9159745 C 80.407282,9.0909731 81.84201,8.0489732 83.661933,7.0219732 C 80.923544,8.3389732 78.759446,9.8199731 77.661889,10.915974 C 72.743394,15.831973 70.034021,21.773972 70.034021,27.647972 L 70.033021,27.648972 C 70.034021,33.217972 72.407224,38.532972 76.897502,43.019972 C 81.424798,47.545972 86.780515,49.937972 92.384357,49.937972 C 96.062223,49.937972 99.7631,48.874972 103.21585,46.874972 C 100.32739,48.224972 97.308855,48.937972 94.304331,48.937972 z " /></svg>

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 505 KiB

After

Width:  |  Height:  |  Size: 505 KiB

View File

@ -89,23 +89,82 @@
x2="76.100182"
y2="249"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="Main-61">
<stop
style="stop-color:#4d4d4d;stop-opacity:1;"
offset="0"
id="stop2082-1" />
</linearGradient>
<linearGradient
xlink:href="#Main-6"
id="linearGradient2098"
x1="52.082603"
y1="61.780205"
x2="53.181248"
y2="61.780205"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(3.7795278,0,0,3.7795278,-189.44141,-219.41797)" />
<linearGradient
xlink:href="#Main-6"
id="linearGradient2096"
x1="50.281166"
y1="62.838539"
x2="52.926998"
y2="62.838539"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#Main-6"
id="linearGradient2098-8"
x1="52.082603"
y1="61.780205"
x2="53.181248"
y2="61.780205"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#Main-6"
id="linearGradient2096-5"
x1="50.286324"
y1="62.972603"
x2="52.024643"
y2="62.967445"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-0.27492098,-0.52917219)" />
<linearGradient
xlink:href="#Main-6"
id="linearGradient2098-4"
x1="52.082603"
y1="61.780205"
x2="53.181248"
y2="61.780205"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-1.3228271,-0.79375592)" />
<linearGradient
xlink:href="#Main-6"
id="linearGradient2092"
x1="45.243748"
y1="62.838539"
x2="47.88958"
y2="62.838539"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-0.26458362)" />
<linearGradient
xlink:href="#Main-6"
id="linearGradient2094"
x1="44.989498"
y1="61.780205"
x2="46.088139"
y2="61.780205"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(0.79373763)" />
</defs>
<g
id="menu-reload"
transform="matrix(3.7795278,0,0,3.7795278,-67.999997,-247.99997)"
transform="matrix(3.7795278,0,0,3.7795278,-187.99999,-227.99997)"
style="display:inline">
<rect
style="opacity:0;fill:#a51d2d;stroke-width:0.264583px;paint-order:fill markers stroke;stop-color:#000000"
id="rect1553"
width="4.2333331"
height="4.2333331"
x="17.991665"
y="65.616653"
rx="1.9824198e-15" />
<path
id="circle2129"
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:url(#linearGradient2146);fill-opacity:1;stroke:none;stroke-width:0.999999px;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;stop-opacity:1"
d="m 69.046875,241.90039 a 7.0999998,7.0999998 0 0 0 -3.628906,0.9707 l 3.210937,3.21094 0.800782,-0.80273 -1.892579,-1.89258 A 5.7999999,5.7999999 0 0 1 69,243.19922 5.7999999,5.7999999 0 0 1 74.800781,249 a 5.7999999,5.7999999 0 0 1 -0.697265,2.75586 l 1.144531,0.61719 a 7.0999998,7.0999998 0 0 0 -1.316406,-8.48047 7.0999998,7.0999998 0 0 0 -4.884766,-1.99219 z m -6.294922,3.72656 a 7.0999998,7.0999998 0 0 0 1.316406,8.48047 7.0999998,7.0999998 0 0 0 4.884766,1.99219 7.0999998,7.0999998 0 0 0 3.628906,-0.9707 l -3.210937,-3.21094 -0.800782,0.80273 1.892579,1.89258 A 5.7999999,5.7999999 0 0 1 69,254.80078 5.7999999,5.7999999 0 0 1 63.199219,249 a 5.7999999,5.7999999 0 0 1 0.697265,-2.75586 z"
transform="matrix(0.26458333,0,0,0.26458333,1.8520833,1.8520833)" />
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" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Some files were not shown because too many files have changed in this diff Show More