1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-12-13 23:13:05 +00:00
This commit is contained in:
Igilq 2025-07-31 06:49:58 +02:00
commit e5b5c2254d
106 changed files with 1164 additions and 551 deletions

View file

@ -27,14 +27,14 @@ variables:
.Ubuntu_Image: .Ubuntu_Image:
tags: tags:
- saas-linux-medium-amd64 - saas-linux-medium-amd64
image: ubuntu:22.04 image: ubuntu:24.04
rules: rules:
- if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event"
Ubuntu_GCC_preprocess: Ubuntu_GCC_preprocess:
extends: .Ubuntu_Image extends: .Ubuntu_Image
cache: cache:
key: Ubuntu_GCC_preprocess.ubuntu_22.04.v1 key: Ubuntu_GCC_preprocess.ubuntu_24.04.v1
paths: paths:
- apt-cache/ - apt-cache/
- .cache/pip/ - .cache/pip/
@ -43,7 +43,7 @@ Ubuntu_GCC_preprocess:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
before_script: before_script:
- CI/install_debian_deps.sh openmw-deps openmw-deps-dynamic gcc_preprocess - CI/install_debian_deps.sh openmw-deps openmw-deps-dynamic gcc_preprocess
- pip3 install --user click termtables - pip3 install --user --break-system-packages click termtables
script: script:
- CI/ubuntu_gcc_preprocess.sh - CI/ubuntu_gcc_preprocess.sh
rules: rules:
@ -82,7 +82,7 @@ Ubuntu_GCC_preprocess:
- if [[ "${BUILD_TESTS_ONLY}" && ! "${BUILD_WITH_CODE_COVERAGE}" ]]; then ./openmw_settings_access_benchmark; fi - if [[ "${BUILD_TESTS_ONLY}" && ! "${BUILD_WITH_CODE_COVERAGE}" ]]; then ./openmw_settings_access_benchmark; fi
- ccache -svv - ccache -svv
- df -h - df -h
- if [[ "${BUILD_WITH_CODE_COVERAGE}" ]]; then gcovr --xml-pretty --exclude-unreachable-branches --print-summary --root "${CI_PROJECT_DIR}" -j $(nproc) -o ../coverage.xml; fi - if [[ "${BUILD_WITH_CODE_COVERAGE}" ]]; then ~/.local/bin/gcovr --xml-pretty --exclude-unreachable-branches --gcov-ignore-parse-errors=negative_hits.warn_once_per_file --print-summary --root "${CI_PROJECT_DIR}" -j $(nproc) -o ../coverage.xml; fi
- ls | grep -v -e '^extern$' -e '^install$' -e '^components-tests.xml$' -e '^openmw-tests.xml$' -e '^openmw-cs-tests.xml$' | xargs -I '{}' rm -rf './{}' - ls | grep -v -e '^extern$' -e '^install$' -e '^components-tests.xml$' -e '^openmw-tests.xml$' -e '^openmw-cs-tests.xml$' | xargs -I '{}' rm -rf './{}'
- cd .. - cd ..
- df -h - df -h
@ -97,12 +97,12 @@ Ubuntu_GCC_preprocess:
Coverity: Coverity:
tags: tags:
- saas-linux-medium-amd64 - saas-linux-medium-amd64
image: ubuntu:22.04 image: ubuntu:24.04
stage: build stage: build
rules: rules:
- if: $CI_PIPELINE_SOURCE == "schedule" - if: $CI_PIPELINE_SOURCE == "schedule"
cache: cache:
key: Coverity.ubuntu_22.04.v1 key: Coverity.ubuntu_24.04.v1
paths: paths:
- apt-cache/ - apt-cache/
- ccache/ - ccache/
@ -141,7 +141,7 @@ Coverity:
Ubuntu_GCC: Ubuntu_GCC:
extends: .Ubuntu extends: .Ubuntu
cache: cache:
key: Ubuntu_GCC.ubuntu_22.04.v1 key: Ubuntu_GCC.ubuntu_24.04.v1
before_script: before_script:
- CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic
variables: variables:
@ -154,7 +154,7 @@ Ubuntu_GCC:
Ubuntu_GCC_asan: Ubuntu_GCC_asan:
extends: Ubuntu_GCC extends: Ubuntu_GCC
cache: cache:
key: Ubuntu_GCC_asan.ubuntu_22.04.v1 key: Ubuntu_GCC_asan.ubuntu_24.04.v1
variables: variables:
CMAKE_BUILD_TYPE: Debug CMAKE_BUILD_TYPE: Debug
CMAKE_CXX_FLAGS_DEBUG: -g -O1 -fno-omit-frame-pointer -fsanitize=address -fsanitize=pointer-subtract -fsanitize=leak CMAKE_CXX_FLAGS_DEBUG: -g -O1 -fno-omit-frame-pointer -fsanitize=address -fsanitize=pointer-subtract -fsanitize=leak
@ -167,7 +167,7 @@ Clang_Format:
extends: .Ubuntu_Image extends: .Ubuntu_Image
stage: checks stage: checks
cache: cache:
key: Ubuntu_Clang_Format.ubuntu_22.04.v1 key: Ubuntu_Clang_Format.ubuntu_24.04.v1
paths: paths:
- apt-cache/ - apt-cache/
variables: variables:
@ -183,11 +183,11 @@ Lupdate:
extends: .Ubuntu_Image extends: .Ubuntu_Image
stage: checks stage: checks
cache: cache:
key: Ubuntu_lupdate.ubuntu_22.04.v1 key: Ubuntu_lupdate.ubuntu_24.04.v1
paths: paths:
- apt-cache/ - apt-cache/
variables: variables:
LUPDATE: lupdate LUPDATE: /usr/lib/qt6/bin/lupdate
before_script: before_script:
- CI/install_debian_deps.sh openmw-qt-translations - CI/install_debian_deps.sh openmw-qt-translations
script: script:
@ -209,7 +209,7 @@ Teal:
Ubuntu_GCC_Debug: Ubuntu_GCC_Debug:
extends: .Ubuntu extends: .Ubuntu
cache: cache:
key: Ubuntu_GCC_Debug.ubuntu_22.04.v2 key: Ubuntu_GCC_Debug.ubuntu_24.04.v2
before_script: before_script:
- CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic
variables: variables:
@ -225,7 +225,7 @@ Ubuntu_GCC_Debug:
Ubuntu_GCC_tests: Ubuntu_GCC_tests:
extends: Ubuntu_GCC extends: Ubuntu_GCC
cache: cache:
key: Ubuntu_GCC_tests.ubuntu_22.04.v1 key: Ubuntu_GCC_tests.ubuntu_24.04.v1
variables: variables:
CCACHE_SIZE: 1G CCACHE_SIZE: 1G
BUILD_TESTS_ONLY: 1 BUILD_TESTS_ONLY: 1
@ -239,7 +239,7 @@ Ubuntu_GCC_tests:
.Ubuntu_GCC_tests_Debug: .Ubuntu_GCC_tests_Debug:
extends: Ubuntu_GCC extends: Ubuntu_GCC
cache: cache:
key: Ubuntu_GCC_tests_Debug.ubuntu_22.04.v1 key: Ubuntu_GCC_tests_Debug.ubuntu_24.04.v1
variables: variables:
CCACHE_SIZE: 1G CCACHE_SIZE: 1G
BUILD_TESTS_ONLY: 1 BUILD_TESTS_ONLY: 1
@ -255,7 +255,7 @@ Ubuntu_GCC_tests:
Ubuntu_GCC_tests_asan: Ubuntu_GCC_tests_asan:
extends: Ubuntu_GCC extends: Ubuntu_GCC
cache: cache:
key: Ubuntu_GCC_tests_asan.ubuntu_22.04.v1 key: Ubuntu_GCC_tests_asan.ubuntu_24.04.v1
variables: variables:
CCACHE_SIZE: 1G CCACHE_SIZE: 1G
BUILD_TESTS_ONLY: 1 BUILD_TESTS_ONLY: 1
@ -278,7 +278,7 @@ Ubuntu_GCC_tests_asan:
Ubuntu_GCC_tests_ubsan: Ubuntu_GCC_tests_ubsan:
extends: Ubuntu_GCC extends: Ubuntu_GCC
cache: cache:
key: Ubuntu_GCC_tests_ubsan.ubuntu_22.04.v1 key: Ubuntu_GCC_tests_ubsan.ubuntu_24.04.v1
variables: variables:
CCACHE_SIZE: 1G CCACHE_SIZE: 1G
BUILD_TESTS_ONLY: 1 BUILD_TESTS_ONLY: 1
@ -298,7 +298,7 @@ Ubuntu_GCC_tests_ubsan:
.Ubuntu_GCC_tests_tsan: .Ubuntu_GCC_tests_tsan:
extends: Ubuntu_GCC extends: Ubuntu_GCC
cache: cache:
key: Ubuntu_GCC_tests_tsan.ubuntu_22.04.v1 key: Ubuntu_GCC_tests_tsan.ubuntu_24.04.v1
variables: variables:
CCACHE_SIZE: 1G CCACHE_SIZE: 1G
BUILD_TESTS_ONLY: 1 BUILD_TESTS_ONLY: 1
@ -316,11 +316,15 @@ Ubuntu_GCC_tests_ubsan:
Ubuntu_GCC_tests_coverage: Ubuntu_GCC_tests_coverage:
extends: .Ubuntu_GCC_tests_Debug extends: .Ubuntu_GCC_tests_Debug
cache: cache:
key: Ubuntu_GCC_tests_coverage.ubuntu_22.04.v1 key: Ubuntu_GCC_tests_coverage.ubuntu_24.04.v1
paths:
- .cache/pip
variables: variables:
BUILD_WITH_CODE_COVERAGE: 1 BUILD_WITH_CODE_COVERAGE: 1
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
before_script: before_script:
- CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic openmw-coverage - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic openmw-coverage
- pipx install gcovr
coverage: /^\s*lines:\s*\d+.\d+\%/ coverage: /^\s*lines:\s*\d+.\d+\%/
artifacts: artifacts:
paths: [] paths: []
@ -345,7 +349,7 @@ Ubuntu_GCC_tests_coverage:
- "CI/**/*" - "CI/**/*"
- ".gitlab-ci.yml" - ".gitlab-ci.yml"
cache: cache:
key: Ubuntu_Static_Deps.ubuntu_22.04.v1 key: Ubuntu_Static_Deps.ubuntu_24.04.v1
paths: paths:
- apt-cache/ - apt-cache/
- ccache/ - ccache/
@ -362,7 +366,7 @@ Ubuntu_GCC_tests_coverage:
.Ubuntu_Static_Deps_tests: .Ubuntu_Static_Deps_tests:
extends: .Ubuntu_Static_Deps extends: .Ubuntu_Static_Deps
cache: cache:
key: Ubuntu_Static_Deps_tests.ubuntu_22.04.v1 key: Ubuntu_Static_Deps_tests.ubuntu_24.04.v1
variables: variables:
CCACHE_SIZE: 1G CCACHE_SIZE: 1G
BUILD_TESTS_ONLY: 1 BUILD_TESTS_ONLY: 1
@ -381,7 +385,7 @@ Ubuntu_Clang:
before_script: before_script:
- CI/install_debian_deps.sh clang openmw-deps openmw-deps-dynamic - CI/install_debian_deps.sh clang openmw-deps openmw-deps-dynamic
cache: cache:
key: Ubuntu_Clang.ubuntu_22.04.v2 key: Ubuntu_Clang.ubuntu_24.04.v2
variables: variables:
CC: clang CC: clang
CXX: clang++ CXX: clang++
@ -394,7 +398,7 @@ Ubuntu_Clang:
before_script: before_script:
- CI/install_debian_deps.sh clang clang-tidy openmw-deps openmw-deps-dynamic - CI/install_debian_deps.sh clang clang-tidy openmw-deps openmw-deps-dynamic
cache: cache:
key: Ubuntu_Clang_Tidy.ubuntu_22.04.v1 key: Ubuntu_Clang_Tidy.ubuntu_24.04.v1
variables: variables:
CMAKE_BUILD_TYPE: Debug CMAKE_BUILD_TYPE: Debug
CMAKE_CXX_FLAGS_DEBUG: -O0 CMAKE_CXX_FLAGS_DEBUG: -O0
@ -451,7 +455,7 @@ Ubuntu_Clang_Tidy_other:
.Ubuntu_Clang_tests: .Ubuntu_Clang_tests:
extends: Ubuntu_Clang extends: Ubuntu_Clang
cache: cache:
key: Ubuntu_Clang_tests.ubuntu_22.04.v1 key: Ubuntu_Clang_tests.ubuntu_24.04.v1
variables: variables:
CCACHE_SIZE: 1G CCACHE_SIZE: 1G
BUILD_TESTS_ONLY: 1 BUILD_TESTS_ONLY: 1
@ -465,7 +469,7 @@ Ubuntu_Clang_Tidy_other:
Ubuntu_Clang_tests_Debug: Ubuntu_Clang_tests_Debug:
extends: Ubuntu_Clang extends: Ubuntu_Clang
cache: cache:
key: Ubuntu_Clang_tests_Debug.ubuntu_22.04.v1 key: Ubuntu_Clang_tests_Debug.ubuntu_24.04.v1
variables: variables:
CCACHE_SIZE: 1G CCACHE_SIZE: 1G
BUILD_TESTS_ONLY: 1 BUILD_TESTS_ONLY: 1
@ -489,7 +493,7 @@ Ubuntu_Clang_tests_Debug:
- apt-cache/ - apt-cache/
before_script: before_script:
- CI/install_debian_deps.sh $OPENMW_DEPS - CI/install_debian_deps.sh $OPENMW_DEPS
- pip3 install --user numpy matplotlib termtables click - pip3 install --user --break-system-packages numpy matplotlib termtables click
script: script:
- CI/run_integration_tests.sh - CI/run_integration_tests.sh
after_script: after_script:
@ -500,7 +504,7 @@ Ubuntu_Clang_integration_tests:
needs: needs:
- Ubuntu_Clang - Ubuntu_Clang
cache: cache:
key: Ubuntu_Clang_integration_tests.ubuntu_22.04.v2 key: Ubuntu_Clang_integration_tests.ubuntu_24.04.v2
variables: variables:
OPENMW_DEPS: openmw-integration-tests OPENMW_DEPS: openmw-integration-tests
@ -509,9 +513,9 @@ Ubuntu_GCC_integration_tests_asan:
needs: needs:
- Ubuntu_GCC_asan - Ubuntu_GCC_asan
cache: cache:
key: Ubuntu_GCC_integration_tests_asan.ubuntu_22.04.v1 key: Ubuntu_GCC_integration_tests_asan.ubuntu_24.04.v1
variables: variables:
OPENMW_DEPS: openmw-integration-tests libasan6 OPENMW_DEPS: openmw-integration-tests libasan
ASAN_OPTIONS: halt_on_error=1:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1:detect_leaks=0 ASAN_OPTIONS: halt_on_error=1:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1:detect_leaks=0
.MacOS: .MacOS:
@ -990,7 +994,7 @@ Windows_MSBuild_CacheInit:
paths: paths:
- .cache/pip - .cache/pip
before_script: before_script:
- pip3 install --user requests click discord_webhook - pip3 install --user --break-system-packages requests click discord_webhook
script: script:
- scripts/find_missing_merge_requests.py --project_id=$CI_PROJECT_ID --ignored_mrs_path=$CI_PROJECT_DIR/.resubmitted_merge_requests.txt - scripts/find_missing_merge_requests.py --project_id=$CI_PROJECT_ID --ignored_mrs_path=$CI_PROJECT_DIR/.resubmitted_merge_requests.txt

View file

@ -2,6 +2,8 @@
------ ------
Bug #2967: Inventory windows don't update when changing items by script Bug #2967: Inventory windows don't update when changing items by script
Bug #4437: Transformations for NiSkinInstance are ignored
Bug #4885: Disable in dialogue result script causes a crash
Bug #5331: Pathfinding works incorrectly when actor is moved from one interior cell to another Bug #5331: Pathfinding works incorrectly when actor is moved from one interior cell to another
Bug #6039: Next Spell keybind fails while selected enchanted item has multiple copies Bug #6039: Next Spell keybind fails while selected enchanted item has multiple copies
Bug #6573: Editor: Selection behaves incorrectly on high-DPI displays Bug #6573: Editor: Selection behaves incorrectly on high-DPI displays
@ -9,9 +11,11 @@
Bug #7371: Equipping item from inventory does not play a Down sound when equipping fails Bug #7371: Equipping item from inventory does not play a Down sound when equipping fails
Bug #7622: Player's marksman weapons don't work on close actors underwater Bug #7622: Player's marksman weapons don't work on close actors underwater
Bug #7649: The sound and vfx of resisted enchanted items' magic still play Bug #7649: The sound and vfx of resisted enchanted items' magic still play
Bug #7693: I.ItemUsage should return an item to the selected stack if equipping/consumption is denied
Bug #7740: Magic items in the HUD aren't composited correctly Bug #7740: Magic items in the HUD aren't composited correctly
Bug #7799: Picking up ingredients while object paging active grid is on may cause a hiccup Bug #7799: Picking up ingredients while object paging active grid is on may cause a hiccup
Bug #7871: Kwama Queen doesn't start combat with player Bug #7871: Kwama Queen doesn't start combat with player
Bug #7979: Paralyzed NPCs battlecry
Bug #8245: The console command ShowVars does not list global mwscripts Bug #8245: The console command ShowVars does not list global mwscripts
Bug #8265: Topics are linked incorrectly Bug #8265: Topics are linked incorrectly
Bug #8303: On target spells cast by non-actors should fire underwater Bug #8303: On target spells cast by non-actors should fire underwater
@ -25,30 +29,40 @@
Bug #8375: Moon phase cycle doesn't match Morrowind Bug #8375: Moon phase cycle doesn't match Morrowind
Bug #8383: Casting bound helm or boots on beast races doesn't cleanup properly Bug #8383: Casting bound helm or boots on beast races doesn't cleanup properly
Bug #8385: Russian encoding broken with locale parameters and calendar Bug #8385: Russian encoding broken with locale parameters and calendar
Bug #8404: Prevent merchant equipping breaks on lights
Bug #8408: OpenMW doesn't report all the potential resting hindrances Bug #8408: OpenMW doesn't report all the potential resting hindrances
Bug #8414: Waterwalking works when collision is disabled Bug #8414: Waterwalking works when collision is disabled
Bug #8431: Behaviour of removed items from a container is buggy Bug #8431: Behaviour of removed items from a container is buggy
Bug #8432: Changing to and from an interior cell doesn't update collision Bug #8432: Changing to and from an interior cell doesn't update collision
Bug #8433: Wandering NPCs are not capable of avoiding easy obstacles
Bug #8436: Spell selection in a pinned spellbook window doesn't update Bug #8436: Spell selection in a pinned spellbook window doesn't update
Bug #8437: Pinned inventory window's pin button doesn't look pressed Bug #8437: Pinned inventory window's pin button doesn't look pressed
Bug #8446: Travel prices are strangely inconsistent Bug #8446: Travel prices are strangely inconsistent
Bug #8447: Werewolf swimming animation breaks in third person perspective
Bug #8459: Changing magic effect base cost doesn't change spell price Bug #8459: Changing magic effect base cost doesn't change spell price
Bug #8466: Showmap "" reveals nameless cells Bug #8466: Showmap "" reveals nameless cells
Bug #8485: Witchwither disease and probably other common diseases don't work correctly Bug #8485: Witchwither disease and probably other common diseases don't work correctly
Bug #8490: Normals on Water disappear when Water Shader is Enabled but Refraction is Disabled Bug #8490: Normals on Water disappear when Water Shader is Enabled but Refraction is Disabled
Bug #8500: OpenMW Alarm behaviour doesn't match morrowind.exe Bug #8500: OpenMW Alarm behaviour doesn't match morrowind.exe
Bug #8519: Multiple bounty is sometimes assigned to player when detected during a pickpocketing action Bug #8519: Multiple bounty is sometimes assigned to player when detected during a pickpocketing action
Bug #8540: Magic resistance is applied to effects without a magnitude
Bug #8557: Charm's disposition changes capped on 100, uncapped below 0
Bug #8582: addScript-attached local scripts start out inactive
Bug #8585: Dialogue topic list doesn't have enough padding Bug #8585: Dialogue topic list doesn't have enough padding
Bug #8587: Minor INI importer problems Bug #8587: Minor INI importer problems
Bug #8593: Render targets do not generate mipmaps Bug #8593: Render targets do not generate mipmaps
Bug #8598: Post processing shaders don't interact with the vfs correctly Bug #8598: Post processing shaders don't interact with the vfs correctly
Bug #8599: Non-ASCII paths in BSA files don't work Bug #8599: Non-ASCII paths in BSA files don't work
Bug #8606: Floating point imprecision can mess with container capacity
Bug #8609: The crosshair is too large Bug #8609: The crosshair is too large
Bug #8610: Terrain normal maps using NormalGL format instead of NormalDX Bug #8610: Terrain normal maps using NormalGL format instead of NormalDX
Bug #8612: Using aiactivate on an ingredient when graphical herbalism is enabled triggers non-stop pickup sounds Bug #8612: Using aiactivate on an ingredient when graphical herbalism is enabled triggers non-stop pickup sounds
Bug #8614: Lua garbage collection fails to remove unused data
Bug #8615: Rest/wait time progress speed is different from vanilla Bug #8615: Rest/wait time progress speed is different from vanilla
Feature #2522: Support quick item transfer Feature #2522: Support quick item transfer
Feature #3769: Allow GetSpellEffects on enchantments Feature #3769: Allow GetSpellEffects on enchantments
Feature #6976: [Lua] Weather API
Feature #8077: Save settings changes when clicking "ok"/closing the window
Feature #8112: Expose landscape record data to Lua Feature #8112: Expose landscape record data to Lua
Feature #8113: Support extended selection in autodetected subdirectory dialog Feature #8113: Support extended selection in autodetected subdirectory dialog
Feature #8139: Editor: Redesign the selection markers Feature #8139: Editor: Redesign the selection markers
@ -57,8 +71,10 @@
Feature #8320: Add access mwscript source text to lua api Feature #8320: Add access mwscript source text to lua api
Feature #8334: Lua: AddTopic equivalent Feature #8334: Lua: AddTopic equivalent
Feature #8355: Lua: Window visibility checking in interfaces.UI Feature #8355: Lua: Window visibility checking in interfaces.UI
Feature #8509: FillJournal script instruction
Feature #8580: Sort characters in the save loading menu Feature #8580: Sort characters in the save loading menu
Feature #8597: Lua: Add more built-in event handlers Feature #8597: Lua: Add more built-in event handlers
Feature #8629: Expose path grid data to Lua
0.49.0 0.49.0
------ ------

View file

@ -708,17 +708,12 @@ printf "Qt ${QT_VER}... "
DLLSUFFIX="" DLLSUFFIX=""
fi fi
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
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" # Since Qt 6.7.0 plugin is called "qmodernwindowsstyle"
if [ "${QT_MINOR_VER}" -ge 7 ]; then if [ "${QT_MINOR_VER}" -ge 7 ]; then
add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qmodernwindowsstyle${DLLSUFFIX}.dll" 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 else
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" add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll"
fi fi

View file

@ -33,10 +33,10 @@ declare -rA GROUPED_DEPS=(
libboost-system-dev libboost-iostreams-dev libboost-system-dev libboost-iostreams-dev
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev
libsdl2-dev libqt5opengl5-dev qttools5-dev qttools5-dev-tools libopenal-dev libsdl2-dev libqt6opengl6-dev qt6-tools-dev qt6-tools-dev-tools libopenal-dev
libunshield-dev libtinyxml-dev libbullet-dev liblz4-dev libpng-dev libjpeg-dev libunshield-dev libtinyxml-dev libbullet-dev liblz4-dev libpng-dev libjpeg-dev
libluajit-5.1-dev librecast-dev libsqlite3-dev ca-certificates libicu-dev libluajit-5.1-dev librecast-dev libsqlite3-dev ca-certificates libicu-dev
libyaml-cpp-dev libqt5svg5 libqt5svg5-dev libyaml-cpp-dev libqt6svg6 libqt6svg6-dev
" "
# These dependencies can alternatively be built and linked statically. # These dependencies can alternatively be built and linked statically.
@ -57,22 +57,22 @@ declare -rA GROUPED_DEPS=(
libsdl2-dev libboost-system-dev libboost-filesystem-dev libgl-dev libsdl2-dev libboost-system-dev libboost-filesystem-dev libgl-dev
" "
[openmw-coverage]="gcovr" [openmw-coverage]="pipx"
[openmw-integration-tests]=" [openmw-integration-tests]="
ca-certificates ca-certificates
gdb gdb
git git
git-lfs git-lfs
libavcodec58 libavcodec60
libavformat58 libavformat60
libavutil56 libavutil58
libboost-iostreams1.74.0 libboost-iostreams1.83.0
libboost-program-options1.74.0 libboost-program-options1.83.0
libboost-system1.74.0 libboost-system1.83.0
libbullet3.24 libbullet3.24
libcollada-dom2.5-dp0 libcollada-dom2.5-dp0
libicu70 libicu74
libjpeg8 libjpeg8
libluajit-5.1-2 libluajit-5.1-2
liblz4-1 liblz4-1
@ -80,19 +80,19 @@ declare -rA GROUPED_DEPS=(
libopenal1 libopenal1
libopenscenegraph161 libopenscenegraph161
libpng16-16 libpng16-16
libqt5opengl5 libqt6opengl6
librecast1 librecast1
libsdl2-2.0-0 libsdl2-2.0-0
libsqlite3-0 libsqlite3-0
libswresample3 libswresample4
libswscale5 libswscale7
libtinyxml2.6.2v5 libtinyxml2.6.2v5
libyaml-cpp0.8 libyaml-cpp0.8
python3-pip python3-pip
xvfb xvfb
" "
[libasan6]="libasan6" [libasan]="libasan8"
[android]="binutils build-essential cmake ccache curl unzip git pkg-config" [android]="binutils build-essential cmake ccache curl unzip git pkg-config"
@ -102,8 +102,8 @@ declare -rA GROUPED_DEPS=(
" "
[openmw-qt-translations]=" [openmw-qt-translations]="
qttools5-dev qt6-tools-dev
qttools5-dev-tools qt6-tools-dev-tools
git-core git-core
" "
) )

View file

@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...")
set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MAJOR 0)
set(OPENMW_VERSION_MINOR 50) set(OPENMW_VERSION_MINOR 50)
set(OPENMW_VERSION_RELEASE 0) set(OPENMW_VERSION_RELEASE 0)
set(OPENMW_LUA_API_REVISION 83) set(OPENMW_LUA_API_REVISION 85)
set(OPENMW_POSTPROCESSING_API_REVISION 3) set(OPENMW_POSTPROCESSING_API_REVISION 3)
set(OPENMW_VERSION_COMMITHASH "") set(OPENMW_VERSION_COMMITHASH "")
@ -249,12 +249,8 @@ endif()
find_package(LZ4 REQUIRED) find_package(LZ4 REQUIRED)
if (USE_QT) if (USE_QT)
find_package(QT REQUIRED COMPONENTS Core NAMES Qt6 Qt5) find_package(QT REQUIRED COMPONENTS Core NAMES Qt6)
if (QT_VERSION_MAJOR VERSION_EQUAL 5) find_package(Qt6 COMPONENTS Core Widgets Network OpenGL OpenGLWidgets LinguistTools Svg REQUIRED)
find_package(Qt5 5.15 COMPONENTS Core Widgets Network OpenGL LinguistTools Svg REQUIRED)
else()
find_package(Qt6 COMPONENTS Core Widgets Network OpenGL OpenGLWidgets LinguistTools Svg REQUIRED)
endif()
message(STATUS "Using Qt${QT_VERSION}") message(STATUS "Using Qt${QT_VERSION}")
endif() endif()

View file

@ -985,7 +985,7 @@ namespace
0, 0, 0, 0, 0, // row 0 0, 0, 0, 0, 0, // row 0
0, 0, 0, 0, 0, // row 1 0, 0, 0, 0, 0, // row 1
0, 0, 1000, 0, 0, // row 2 0, 0, 1000, 0, 0, // row 2
0, 0, 0, 0, 0, // row 3 0, 0, 1000, 0, 0, // row 3
0, 0, 0, 0, 0, // row 4 0, 0, 0, 0, 0, // row 4
} }; } };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
@ -1017,7 +1017,7 @@ namespace
0, 0, 0, 0, 0, // row 0 0, 0, 0, 0, 0, // row 0
0, 0, 0, 0, 0, // row 1 0, 0, 0, 0, 0, // row 1
0, 0, -1000, 0, 0, // row 2 0, 0, -1000, 0, 0, // row 2
0, 0, 0, 0, 0, // row 3 0, 0, -1000, 0, 0, // row 3
0, 0, 0, 0, 0, // row 4 0, 0, 0, 0, 0, // row 4
} }; } };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
@ -1049,7 +1049,7 @@ namespace
0, 0, 0, 0, 0, // row 0 0, 0, 0, 0, 0, // row 0
0, 0, 0, 0, 0, // row 1 0, 0, 0, 0, 0, // row 1
0, 0, 1000, 0, 0, // row 2 0, 0, 1000, 0, 0, // row 2
0, 0, 0, 0, 0, // row 3 0, 0, 1000, 0, 0, // row 3
0, 0, 0, 0, 0, // row 4 0, 0, 0, 0, 0, // row 4
} }; } };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
@ -1086,7 +1086,7 @@ namespace
0, 0, 0, 0, 0, // row 0 0, 0, 0, 0, 0, // row 0
0, 0, 0, 0, 0, // row 1 0, 0, 0, 0, 0, // row 1
0, 0, 1000, 0, 0, // row 2 0, 0, 1000, 0, 0, // row 2
0, 0, 0, 0, 0, // row 3 0, 0, 1000, 0, 0, // row 3
0, 0, 0, 0, 0, // row 4 0, 0, 0, 0, 0, // row 4
} }; } };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);

View file

@ -88,11 +88,7 @@ void Launcher::ImportPage::on_importerButton_clicked()
// Create the file if it doesn't already exist, else the importer will fail // Create the file if it doesn't already exist, else the importer will fail
auto path = mCfgMgr.getUserConfigPath(); auto path = mCfgMgr.getUserConfigPath();
path /= "openmw.cfg"; path /= "openmw.cfg";
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QFile file(path); QFile file(path);
#else
QFile file(Files::pathToQString(path));
#endif
if (!file.exists()) if (!file.exists())
{ {

View file

@ -497,11 +497,7 @@ bool Launcher::MainDialog::writeSettings()
} }
// Game settings // Game settings
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QFile file(userPath / Files::openmwCfgFile); QFile file(userPath / Files::openmwCfgFile);
#else
QFile file(Files::getUserConfigPathQString(mCfgMgr));
#endif
if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) if (!file.open(QIODevice::ReadWrite | QIODevice::Text))
{ {

View file

@ -163,8 +163,6 @@ bool Launcher::SettingsPage::loadSettings()
loadSettingInt(Settings::physics().mAsyncNumThreads, *physicsThreadsSpinBox); loadSettingInt(Settings::physics().mAsyncNumThreads, *physicsThreadsSpinBox);
loadSettingBool( loadSettingBool(
Settings::game().mAllowActorsToFollowOverWaterSurface, *allowNPCToFollowOverWaterSurfaceCheckBox); Settings::game().mAllowActorsToFollowOverWaterSurface, *allowNPCToFollowOverWaterSurfaceCheckBox);
loadSettingBool(
Settings::game().mUnarmedCreatureAttacksDamageArmor, *unarmedCreatureAttacksDamageArmorCheckBox);
loadSettingInt(Settings::game().mActorCollisionShapeType, *actorCollisonShapeTypeComboBox); loadSettingInt(Settings::game().mActorCollisionShapeType, *actorCollisonShapeTypeComboBox);
} }
@ -373,8 +371,6 @@ void Launcher::SettingsPage::saveSettings()
saveSettingInt(*physicsThreadsSpinBox, Settings::physics().mAsyncNumThreads); saveSettingInt(*physicsThreadsSpinBox, Settings::physics().mAsyncNumThreads);
saveSettingBool( saveSettingBool(
*allowNPCToFollowOverWaterSurfaceCheckBox, Settings::game().mAllowActorsToFollowOverWaterSurface); *allowNPCToFollowOverWaterSurfaceCheckBox, Settings::game().mAllowActorsToFollowOverWaterSurface);
saveSettingBool(
*unarmedCreatureAttacksDamageArmorCheckBox, Settings::game().mUnarmedCreatureAttacksDamageArmor);
saveSettingInt(*actorCollisonShapeTypeComboBox, Settings::game().mActorCollisionShapeType); saveSettingInt(*actorCollisonShapeTypeComboBox, Settings::game().mActorCollisionShapeType);
} }

View file

@ -53,7 +53,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="10" column="1"> <item row="9" column="1">
<widget class="QCheckBox" name="normaliseRaceSpeedCheckBox"> <widget class="QCheckBox" name="normaliseRaceSpeedCheckBox">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Don't use race weight in NPC movement speed calculations.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Don't use race weight in NPC movement speed calculations.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -63,7 +63,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="9" column="1"> <item row="8" column="1">
<widget class="QCheckBox" name="classicCalmSpellsCheckBox"> <widget class="QCheckBox" name="classicCalmSpellsCheckBox">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Stops combat with NPCs affected by Calm spells every frame -- like in Morrowind without the MCP.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Stops combat with NPCs affected by Calm spells every frame -- like in Morrowind without the MCP.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -73,7 +73,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="12" column="1"> <item row="11" column="1">
<widget class="QCheckBox" name="avoidCollisionsCheckBox"> <widget class="QCheckBox" name="avoidCollisionsCheckBox">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If enabled NPCs apply evasion maneuver to avoid collisions with others.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If enabled NPCs apply evasion maneuver to avoid collisions with others.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -123,7 +123,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="5" column="1">
<widget class="QCheckBox" name="requireAppropriateAmmunitionCheckBox"> <widget class="QCheckBox" name="requireAppropriateAmmunitionCheckBox">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If enabled, a magical ammunition is required to bypass normal weapon resistance or weakness. If disabled, a magical ranged weapon or a magical ammunition is required.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If enabled, a magical ammunition is required to bypass normal weapon resistance or weakness. If disabled, a magical ranged weapon or a magical ammunition is required.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -133,7 +133,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="13" column="1"> <item row="12" column="1">
<widget class="QCheckBox" name="graphicHerbalismCheckBox"> <widget class="QCheckBox" name="graphicHerbalismCheckBox">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If this setting is true, containers supporting graphic herbalism will do so instead of opening the menu.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If this setting is true, containers supporting graphic herbalism will do so instead of opening the menu.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -143,7 +143,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="11" column="1"> <item row="10" column="1">
<widget class="QCheckBox" name="swimUpwardCorrectionCheckBox"> <widget class="QCheckBox" name="swimUpwardCorrectionCheckBox">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Makes player swim a bit upward from the line of sight. Applies only in third person mode. Intended to make simpler swimming without diving.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Makes player swim a bit upward from the line of sight. Applies only in third person mode. Intended to make simpler swimming without diving.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -153,7 +153,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="1"> <item row="7" column="1">
<widget class="QCheckBox" name="enchantedWeaponsMagicalCheckBox"> <widget class="QCheckBox" name="enchantedWeaponsMagicalCheckBox">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Make enchanted weapons without Magical flag bypass normal weapons resistance, like in Morrowind.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Make enchanted weapons without Magical flag bypass normal weapons resistance, like in Morrowind.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -183,7 +183,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="1"> <item row="6" column="1">
<widget class="QCheckBox" name="canLootDuringDeathAnimationCheckBox"> <widget class="QCheckBox" name="canLootDuringDeathAnimationCheckBox">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor's script instantly.&lt;/p&gt;&lt;p&gt;If this setting is false, player has to wait until end of death animation in all cases. Makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor's script instantly.&lt;/p&gt;&lt;p&gt;If this setting is false, player has to wait until end of death animation in all cases. Makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -203,7 +203,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="4" column="1">
<widget class="QCheckBox" name="classicReflectedAbsorbSpellsCheckBox"> <widget class="QCheckBox" name="classicReflectedAbsorbSpellsCheckBox">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Effects of reflected Absorb spells are not mirrored - like in Morrowind.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Effects of reflected Absorb spells are not mirrored - like in Morrowind.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -213,16 +213,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1">
<widget class="QCheckBox" name="unarmedCreatureAttacksDamageArmorCheckBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Unarmed Creature Attacks Damage Armor</string>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item> <item>

View file

@ -240,11 +240,7 @@ target_link_libraries(openmw-cs-lib
components_qt components_qt
) )
if (QT_VERSION_MAJOR VERSION_EQUAL 6) target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::OpenGLWidgets Qt::Svg)
target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::OpenGLWidgets Qt::Svg)
else()
target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::Svg)
endif()
if (WIN32) if (WIN32)
target_sources(openmw-cs PRIVATE ${CMAKE_SOURCE_DIR}/files/windows/openmw-cs.exe.manifest) target_sources(openmw-cs PRIVATE ${CMAKE_SOURCE_DIR}/files/windows/openmw-cs.exe.manifest)

View file

@ -34,11 +34,11 @@ bool CSMFilter::TextNode::test(const CSMWorld::IdTableBase& table, int row, cons
QString string; QString string;
if (data.type() == QVariant::String) if (data.typeId() == QMetaType::QString)
{ {
string = data.toString(); string = data.toString();
} }
else if ((data.type() == QVariant::Int || data.type() == QVariant::UInt) else if ((data.typeId() == QMetaType::Int || data.typeId() == QMetaType::UInt)
&& CSMWorld::Columns::hasEnums(static_cast<CSMWorld::Columns::ColumnId>(mColumnId))) && CSMWorld::Columns::hasEnums(static_cast<CSMWorld::Columns::ColumnId>(mColumnId)))
{ {
int value = data.toInt(); int value = data.toInt();
@ -49,7 +49,7 @@ bool CSMFilter::TextNode::test(const CSMWorld::IdTableBase& table, int row, cons
if (value >= 0 && value < static_cast<int>(enums.size())) if (value >= 0 && value < static_cast<int>(enums.size()))
string = QString::fromUtf8(enums[value].second.c_str()); string = QString::fromUtf8(enums[value].second.c_str());
} }
else if (data.type() == QVariant::Bool) else if (data.typeId() == QMetaType::Bool)
{ {
string = data.toBool() ? "true" : "false"; string = data.toBool() ? "true" : "false";
} }

View file

@ -29,8 +29,8 @@ bool CSMFilter::ValueNode::test(const CSMWorld::IdTableBase& table, int row, con
QVariant data = table.data(index); QVariant data = table.data(index);
if (data.type() != QVariant::Double && data.type() != QVariant::Bool && data.type() != QVariant::Int if (data.typeId() != QMetaType::Double && data.typeId() != QMetaType::Bool && data.typeId() != QMetaType::Int
&& data.type() != QVariant::UInt && data.type() != static_cast<QVariant::Type>(QMetaType::Float)) && data.typeId() != QMetaType::UInt && data.typeId() != QMetaType::Float)
return false; return false;
double value = data.toDouble(); double value = data.toDouble();

View file

@ -62,39 +62,31 @@ namespace CSMPrefs
{ {
QWidget* widget = static_cast<QWidget*>(watched); QWidget* widget = static_cast<QWidget*>(watched);
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
unsigned int mod = (unsigned int)keyEvent->modifiers();
unsigned int key = (unsigned int)keyEvent->key();
if (!keyEvent->isAutoRepeat()) if (!keyEvent->isAutoRepeat())
return activate(widget, mod, key); return activate(widget, keyEvent->keyCombination());
} }
else if (event->type() == QEvent::KeyRelease) else if (event->type() == QEvent::KeyRelease)
{ {
QWidget* widget = static_cast<QWidget*>(watched); QWidget* widget = static_cast<QWidget*>(watched);
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
unsigned int mod = (unsigned int)keyEvent->modifiers();
unsigned int key = (unsigned int)keyEvent->key();
if (!keyEvent->isAutoRepeat()) if (!keyEvent->isAutoRepeat())
return deactivate(widget, mod, key); return deactivate(widget, keyEvent->keyCombination());
} }
else if (event->type() == QEvent::MouseButtonPress) else if (event->type() == QEvent::MouseButtonPress)
{ {
QWidget* widget = static_cast<QWidget*>(watched); QWidget* widget = static_cast<QWidget*>(watched);
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event); QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
unsigned int mod = (unsigned int)mouseEvent->modifiers();
unsigned int button = (unsigned int)mouseEvent->button();
return activate(widget, mod, button); return activate(widget, QKeyCombination(mouseEvent->modifiers(), Qt::Key(mouseEvent->button())));
} }
else if (event->type() == QEvent::MouseButtonRelease) else if (event->type() == QEvent::MouseButtonRelease)
{ {
QWidget* widget = static_cast<QWidget*>(watched); QWidget* widget = static_cast<QWidget*>(watched);
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event); QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
unsigned int mod = (unsigned int)mouseEvent->modifiers();
unsigned int button = (unsigned int)mouseEvent->button();
return deactivate(widget, mod, button); return deactivate(widget, QKeyCombination(mouseEvent->modifiers(), Qt::Key(mouseEvent->button())));
} }
else if (event->type() == QEvent::FocusOut) else if (event->type() == QEvent::FocusOut)
{ {
@ -149,7 +141,7 @@ namespace CSMPrefs
} }
} }
bool ShortcutEventHandler::activate(QWidget* widget, unsigned int mod, unsigned int button) bool ShortcutEventHandler::activate(QWidget* widget, QKeyCombination keyCombination)
{ {
std::vector<std::pair<MatchResult, Shortcut*>> potentials; std::vector<std::pair<MatchResult, Shortcut*>> potentials;
bool used = false; bool used = false;
@ -167,7 +159,7 @@ namespace CSMPrefs
if (!shortcut->isEnabled()) if (!shortcut->isEnabled())
continue; continue;
if (checkModifier(mod, button, shortcut, true)) if (checkModifier(keyCombination, shortcut, true))
used = true; used = true;
if (shortcut->getActivationStatus() != Shortcut::AS_Inactive) if (shortcut->getActivationStatus() != Shortcut::AS_Inactive)
@ -175,7 +167,8 @@ namespace CSMPrefs
int pos = shortcut->getPosition(); int pos = shortcut->getPosition();
int lastPos = shortcut->getLastPosition(); int lastPos = shortcut->getLastPosition();
MatchResult result = match(mod, button, shortcut->getSequence()[pos]); MatchResult result = match(keyCombination.keyboardModifiers(), keyCombination.key(),
shortcut->getSequence()[pos].toCombined());
if (result == Matches_WithMod || result == Matches_NoMod) if (result == Matches_WithMod || result == Matches_NoMod)
{ {
@ -220,10 +213,8 @@ namespace CSMPrefs
return used; return used;
} }
bool ShortcutEventHandler::deactivate(QWidget* widget, unsigned int mod, unsigned int button) bool ShortcutEventHandler::deactivate(QWidget* widget, QKeyCombination keyCombination)
{ {
const int KeyMask = 0x01FFFFFF;
bool used = false; bool used = false;
while (widget) while (widget)
@ -235,11 +226,11 @@ namespace CSMPrefs
{ {
Shortcut* shortcut = *it; Shortcut* shortcut = *it;
if (checkModifier(mod, button, shortcut, false)) if (checkModifier(keyCombination, shortcut, false))
used = true; used = true;
int pos = shortcut->getPosition(); int pos = shortcut->getPosition();
MatchResult result = match(0, button, shortcut->getSequence()[pos] & KeyMask); MatchResult result = match(0, keyCombination.key(), shortcut->getSequence()[pos].key());
if (result != Matches_Not) if (result != Matches_Not)
{ {
@ -268,13 +259,13 @@ namespace CSMPrefs
return used; return used;
} }
bool ShortcutEventHandler::checkModifier(unsigned int mod, unsigned int button, Shortcut* shortcut, bool activate) bool ShortcutEventHandler::checkModifier(QKeyCombination keyCombination, Shortcut* shortcut, bool activate)
{ {
if (!shortcut->isEnabled() || !shortcut->getModifier() || shortcut->getSecondaryMode() == Shortcut::SM_Ignore if (!shortcut->isEnabled() || !shortcut->getModifier() || shortcut->getSecondaryMode() == Shortcut::SM_Ignore
|| shortcut->getModifierStatus() == activate) || shortcut->getModifierStatus() == activate)
return false; return false;
MatchResult result = match(mod, button, shortcut->getModifier()); MatchResult result = match(keyCombination.keyboardModifiers(), keyCombination.key(), shortcut->getModifier());
bool used = false; bool used = false;
if (result != Matches_Not) if (result != Matches_Not)

View file

@ -42,11 +42,11 @@ namespace CSMPrefs
void updateParent(QWidget* widget); void updateParent(QWidget* widget);
bool activate(QWidget* widget, unsigned int mod, unsigned int button); bool activate(QWidget* widget, QKeyCombination keyCombination);
bool deactivate(QWidget* widget, unsigned int mod, unsigned int button); bool deactivate(QWidget* widget, QKeyCombination keyCombination);
bool checkModifier(unsigned int mod, unsigned int button, Shortcut* shortcut, bool activate); bool checkModifier(QKeyCombination keyCombination, Shortcut* shortcut, bool activate);
MatchResult match(unsigned int mod, unsigned int button, unsigned int value); MatchResult match(unsigned int mod, unsigned int button, unsigned int value);

View file

@ -115,15 +115,12 @@ namespace CSMPrefs
std::string ShortcutManager::convertToString(const QKeySequence& sequence) const std::string ShortcutManager::convertToString(const QKeySequence& sequence) const
{ {
const int MouseKeyMask = 0x01FFFFFF;
const int ModMask = 0x7E000000;
std::string result; std::string result;
for (int i = 0; i < (int)sequence.count(); ++i) for (int i = 0; i < sequence.count(); ++i)
{ {
int mods = sequence[i] & ModMask; int mods = sequence[i].keyboardModifiers();
int key = sequence[i] & MouseKeyMask; int key = sequence[i].key();
if (key) if (key)
{ {

View file

@ -59,13 +59,6 @@ void CSMPrefs::State::declare()
.setTooltip("Minimum width of subviews.") .setTooltip("Minimum width of subviews.")
.setRange(50, 10000); .setRange(50, 10000);
declareEnum(mValues->mWindows.mMainwindowScrollbar, "Main Window Horizontal Scrollbar Mode"); declareEnum(mValues->mWindows.mMainwindowScrollbar, "Main Window Horizontal Scrollbar Mode");
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
declareBool(mValues->mWindows.mGrowLimit, "Grow Limit Screen")
.setTooltip(
"When \"Grow then Scroll\" option is selected, the window size grows to"
" the width of the virtual desktop. \nIf this option is selected the the window growth"
"is limited to the current screen.");
#endif
declareCategory("Records"); declareCategory("Records");
declareEnum(mValues->mRecords.mStatusFormat, "Modification Status Display Format"); declareEnum(mValues->mRecords.mStatusFormat, "Modification Status Display Format");

View file

@ -659,11 +659,7 @@ void CSVDoc::View::addSubView(const CSMWorld::UniversalId& id, const std::string
// //
mScrollbarOnly = windows["mainwindow-scrollbar"].toString() == "Scrollbar Only"; mScrollbarOnly = windows["mainwindow-scrollbar"].toString() == "Scrollbar Only";
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
updateWidth(windows["grow-limit"].isTrue(), minWidth);
#else
updateWidth(true, minWidth); updateWidth(true, minWidth);
#endif
mSubViewWindow.addDockWidget(Qt::TopDockWidgetArea, view); mSubViewWindow.addDockWidget(Qt::TopDockWidgetArea, view);

View file

@ -50,7 +50,7 @@ namespace CSVFilter
std::pair<std::string, FilterType> operator()(const QVariant& variantData) std::pair<std::string, FilterType> operator()(const QVariant& variantData)
{ {
FilterType filterType = FilterType::String; FilterType filterType = FilterType::String;
QMetaType::Type dataType = static_cast<QMetaType::Type>(variantData.type()); QMetaType::Type dataType = static_cast<QMetaType::Type>(variantData.typeId());
if (dataType == QMetaType::QString || dataType == QMetaType::Bool || dataType == QMetaType::Int) if (dataType == QMetaType::QString || dataType == QMetaType::Bool || dataType == QMetaType::Int)
filterType = FilterType::String; filterType = FilterType::String;
if (dataType == QMetaType::Int || dataType == QMetaType::Float) if (dataType == QMetaType::Int || dataType == QMetaType::Float)

View file

@ -1132,7 +1132,7 @@ void CSVRender::InstanceMode::dropEvent(QDropEvent* event)
return; return;
WorldspaceHitResult hit WorldspaceHitResult hit
= getWorldspaceWidget().mousePick(event->pos(), getWorldspaceWidget().getInteractionMask()); = getWorldspaceWidget().mousePick(event->position().toPoint(), getWorldspaceWidget().getInteractionMask());
std::string cellId = getWorldspaceWidget().getCellId(hit.worldPos); std::string cellId = getWorldspaceWidget().getCellId(hit.worldPos);

View file

@ -161,8 +161,6 @@ namespace CSVRender
, mLighting(nullptr) , mLighting(nullptr)
, mHasDefaultAmbient(false) , mHasDefaultAmbient(false)
, mIsExterior(true) , mIsExterior(true)
, mPrevMouseX(0)
, mPrevMouseY(0)
, mCamPositionSet(false) , mCamPositionSet(false)
{ {
mFreeCamControl = new FreeCameraController(this); mFreeCamControl = new FreeCameraController(this);
@ -423,10 +421,10 @@ namespace CSVRender
void SceneWidget::mouseMoveEvent(QMouseEvent* event) void SceneWidget::mouseMoveEvent(QMouseEvent* event)
{ {
mCurrentCamControl->handleMouseMoveEvent(event->x() - mPrevMouseX, event->y() - mPrevMouseY); QPointF pos = event->position();
mCurrentCamControl->handleMouseMoveEvent(pos.x() - mPrevMouse.x(), pos.y() - mPrevMouse.y());
mPrevMouseX = event->x(); mPrevMouse = pos;
mPrevMouseY = event->y();
} }
void SceneWidget::wheelEvent(QWheelEvent* event) void SceneWidget::wheelEvent(QWheelEvent* event)

View file

@ -137,7 +137,7 @@ namespace CSVRender
LightingNight mLightingNight; LightingNight mLightingNight;
LightingBright mLightingBright; LightingBright mLightingBright;
int mPrevMouseX, mPrevMouseY; QPointF mPrevMouse;
/// Tells update that camera isn't set /// Tells update that camera isn't set
bool mCamPositionSet; bool mCamPositionSet;

View file

@ -1661,7 +1661,7 @@ void CSVRender::TerrainShapeMode::dragMoveEvent(QDragMoveEvent* event) {}
void CSVRender::TerrainShapeMode::mouseMoveEvent(QMouseEvent* event) void CSVRender::TerrainShapeMode::mouseMoveEvent(QMouseEvent* event)
{ {
WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getInteractionMask()); WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->position().toPoint(), getInteractionMask());
if (hit.hit && mBrushDraw && !(mShapeEditTool == ShapeEditTool_Drag && mIsEditing)) if (hit.hit && mBrushDraw && !(mShapeEditTool == ShapeEditTool_Drag && mIsEditing))
mBrushDraw->update(hit.worldPos, mBrushSize, mBrushShape); mBrushDraw->update(hit.worldPos, mBrushSize, mBrushShape);
if (!hit.hit && mBrushDraw && !(mShapeEditTool == ShapeEditTool_Drag && mIsEditing)) if (!hit.hit && mBrushDraw && !(mShapeEditTool == ShapeEditTool_Drag && mIsEditing))

View file

@ -724,7 +724,7 @@ void CSVRender::TerrainTextureMode::dragMoveEvent(QDragMoveEvent* event) {}
void CSVRender::TerrainTextureMode::mouseMoveEvent(QMouseEvent* event) void CSVRender::TerrainTextureMode::mouseMoveEvent(QMouseEvent* event)
{ {
WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getInteractionMask()); WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->position().toPoint(), getInteractionMask());
if (hit.hit && mBrushDraw) if (hit.hit && mBrushDraw)
mBrushDraw->update(hit.worldPos, mBrushSize, mBrushShape); mBrushDraw->update(hit.worldPos, mBrushSize, mBrushShape);
if (!hit.hit && mBrushDraw) if (!hit.hit && mBrushDraw)

View file

@ -687,11 +687,12 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent(QMouseEvent* event)
if (mDragging) if (mDragging)
{ {
int diffX = event->x() - mDragX; QPoint pos = event->position().toPoint();
int diffY = (height() - event->y()) - mDragY; int diffX = pos.x() - mDragX;
int diffY = (height() - pos.y()) - mDragY;
mDragX = event->x(); mDragX = pos.x();
mDragY = height() - event->y(); mDragY = height() - pos.y();
double factor = mDragFactor; double factor = mDragFactor;
@ -700,32 +701,32 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent(QMouseEvent* event)
EditMode& editMode = dynamic_cast<CSVRender::EditMode&>(*mEditMode->getCurrent()); EditMode& editMode = dynamic_cast<CSVRender::EditMode&>(*mEditMode->getCurrent());
editMode.drag(event->pos(), diffX, diffY, factor); editMode.drag(event->position().toPoint(), diffX, diffY, factor);
} }
else if (mDragMode != InteractionType_None) else if (mDragMode != InteractionType_None)
{ {
EditMode& editMode = dynamic_cast<CSVRender::EditMode&>(*mEditMode->getCurrent()); EditMode& editMode = dynamic_cast<CSVRender::EditMode&>(*mEditMode->getCurrent());
if (mDragMode == InteractionType_PrimaryEdit) if (mDragMode == InteractionType_PrimaryEdit)
mDragging = editMode.primaryEditStartDrag(event->pos()); mDragging = editMode.primaryEditStartDrag(event->position().toPoint());
else if (mDragMode == InteractionType_SecondaryEdit) else if (mDragMode == InteractionType_SecondaryEdit)
mDragging = editMode.secondaryEditStartDrag(event->pos()); mDragging = editMode.secondaryEditStartDrag(event->position().toPoint());
else if (mDragMode == InteractionType_PrimarySelect) else if (mDragMode == InteractionType_PrimarySelect)
mDragging = editMode.primarySelectStartDrag(event->pos()); mDragging = editMode.primarySelectStartDrag(event->position().toPoint());
else if (mDragMode == InteractionType_SecondarySelect) else if (mDragMode == InteractionType_SecondarySelect)
mDragging = editMode.secondarySelectStartDrag(event->pos()); mDragging = editMode.secondarySelectStartDrag(event->position().toPoint());
if (mDragging) if (mDragging)
{ {
mDragX = event->localPos().x(); mDragX = event->position().x();
mDragY = height() - event->localPos().y(); mDragY = height() - event->position().y();
} }
} }
else else
{ {
if (event->globalPos() != mToolTipPos) if (event->globalPosition().toPoint() != mToolTipPos)
{ {
mToolTipPos = event->globalPos(); mToolTipPos = event->globalPosition().toPoint();
if (mShowToolTips) if (mShowToolTips)
{ {
@ -734,7 +735,7 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent(QMouseEvent* event)
} }
} }
const QPointF& pos = event->localPos(); QPoint pos = event->position().toPoint();
handleMarkerHighlight(pos.x(), pos.y()); handleMarkerHighlight(pos.x(), pos.y());
SceneWidget::mouseMoveEvent(event); SceneWidget::mouseMoveEvent(event);
} }

View file

@ -260,8 +260,6 @@ namespace CSVRender
void settingChanged(const CSMPrefs::Setting* setting) override; void settingChanged(const CSMPrefs::Setting* setting) override;
bool getSpeedMode();
void cycleNavigationMode(); void cycleNavigationMode();
private: private:

View file

@ -93,7 +93,7 @@ void CSVTools::ReportTable::contextMenuEvent(QContextMenuEvent* event)
void CSVTools::ReportTable::mouseMoveEvent(QMouseEvent* event) void CSVTools::ReportTable::mouseMoveEvent(QMouseEvent* event)
{ {
if (event->buttons() & Qt::LeftButton) if (event->buttons() & Qt::LeftButton)
startDragFromTable(*this, indexAt(event->pos())); startDragFromTable(*this, indexAt(event->position().toPoint()));
} }
void CSVTools::ReportTable::mouseDoubleClickEvent(QMouseEvent* event) void CSVTools::ReportTable::mouseDoubleClickEvent(QMouseEvent* event)

View file

@ -52,7 +52,7 @@ void CSVWidget::ColorPickerPopup::mousePressEvent(QMouseEvent* event)
// If the mouse is pressed above the pop-up parent, // If the mouse is pressed above the pop-up parent,
// the pop-up will be hidden and the pressed signal won't be repeated for the parent // the pop-up will be hidden and the pressed signal won't be repeated for the parent
if (buttonRect.contains(event->globalPos()) || buttonRect.contains(event->pos())) if (buttonRect.contains(event->globalPosition().toPoint()) || buttonRect.contains(event->position().toPoint()))
{ {
setAttribute(Qt::WA_NoMouseReplay); setAttribute(Qt::WA_NoMouseReplay);
} }

View file

@ -22,12 +22,8 @@ int CSVWidget::CompleterPopup::sizeHintForRow(int row) const
ensurePolished(); ensurePolished();
QModelIndex index = model()->index(row, modelColumn()); QModelIndex index = model()->index(row, modelColumn());
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QStyleOptionViewItem option; QStyleOptionViewItem option;
initViewItemOption(&option); initViewItemOption(&option);
#else QAbstractItemDelegate* delegate = itemDelegateForIndex(index);
QStyleOptionViewItem option = viewOptions();
#endif
QAbstractItemDelegate* delegate = itemDelegate(index);
return delegate->sizeHint(option, index).height(); return delegate->sizeHint(option, index).height();
} }

View file

@ -85,7 +85,7 @@ void CSVWorld::NotEditableSubDelegate::setEditorData(QWidget* editor, const QMod
CSMWorld::Columns::ColumnId columnId CSMWorld::Columns::ColumnId columnId
= static_cast<CSMWorld::Columns::ColumnId>(mTable->getColumnId(index.column())); = static_cast<CSMWorld::Columns::ColumnId>(mTable->getColumnId(index.column()));
if (QVariant::String == v.type()) if (QMetaType::QString == v.typeId())
{ {
label->setText(v.toString()); label->setText(v.toString());
} }

View file

@ -55,7 +55,7 @@ void CSVWorld::DragRecordTable::dragEnterEvent(QDragEnterEvent* event)
void CSVWorld::DragRecordTable::dragMoveEvent(QDragMoveEvent* event) void CSVWorld::DragRecordTable::dragMoveEvent(QDragMoveEvent* event)
{ {
QModelIndex index = indexAt(event->pos()); QModelIndex index = indexAt(event->position().toPoint());
if (CSVWorld::DragDropUtils::canAcceptData(*event, getIndexDisplayType(index)) if (CSVWorld::DragDropUtils::canAcceptData(*event, getIndexDisplayType(index))
|| CSVWorld::DragDropUtils::isInfo(*event, getIndexDisplayType(index)) || CSVWorld::DragDropUtils::isInfo(*event, getIndexDisplayType(index))
|| CSVWorld::DragDropUtils::isTopicOrJournal(*event, getIndexDisplayType(index))) || CSVWorld::DragDropUtils::isTopicOrJournal(*event, getIndexDisplayType(index)))
@ -71,7 +71,7 @@ void CSVWorld::DragRecordTable::dragMoveEvent(QDragMoveEvent* event)
void CSVWorld::DragRecordTable::dropEvent(QDropEvent* event) void CSVWorld::DragRecordTable::dropEvent(QDropEvent* event)
{ {
QModelIndex index = indexAt(event->pos()); QModelIndex index = indexAt(event->position().toPoint());
CSMWorld::ColumnBase::Display display = getIndexDisplayType(index); CSMWorld::ColumnBase::Display display = getIndexDisplayType(index);
if (CSVWorld::DragDropUtils::canAcceptData(*event, display)) if (CSVWorld::DragDropUtils::canAcceptData(*event, display))
{ {

View file

@ -341,7 +341,7 @@ void CSVWorld::RegionMap::viewInTable()
void CSVWorld::RegionMap::mouseMoveEvent(QMouseEvent* event) void CSVWorld::RegionMap::mouseMoveEvent(QMouseEvent* event)
{ {
startDragFromTable(*this, indexAt(event->pos())); startDragFromTable(*this, indexAt(event->position().toPoint()));
} }
std::vector<CSMWorld::UniversalId> CSVWorld::RegionMap::getDraggedRecords() const std::vector<CSMWorld::UniversalId> CSVWorld::RegionMap::getDraggedRecords() const
@ -376,7 +376,7 @@ void CSVWorld::RegionMap::dragMoveEvent(QDragMoveEvent* event)
void CSVWorld::RegionMap::dropEvent(QDropEvent* event) void CSVWorld::RegionMap::dropEvent(QDropEvent* event)
{ {
QModelIndex index = indexAt(event->pos()); QModelIndex index = indexAt(event->position().toPoint());
bool exists = QTableView::model()->data(index, Qt::BackgroundRole) != QBrush(Qt::DiagCrossPattern); bool exists = QTableView::model()->data(index, Qt::BackgroundRole) != QBrush(Qt::DiagCrossPattern);
if (!index.isValid() || !exists) if (!index.isValid() || !exists)

View file

@ -136,7 +136,7 @@ void CSVWorld::ScriptEdit::dragEnterEvent(QDragEnterEvent* event)
QPlainTextEdit::dragEnterEvent(event); QPlainTextEdit::dragEnterEvent(event);
else else
{ {
setTextCursor(cursorForPosition(event->pos())); setTextCursor(cursorForPosition(event->position().toPoint()));
event->acceptProposedAction(); event->acceptProposedAction();
} }
} }
@ -148,7 +148,7 @@ void CSVWorld::ScriptEdit::dragMoveEvent(QDragMoveEvent* event)
QPlainTextEdit::dragMoveEvent(event); QPlainTextEdit::dragMoveEvent(event);
else else
{ {
setTextCursor(cursorForPosition(event->pos())); setTextCursor(cursorForPosition(event->position().toPoint()));
event->accept(); event->accept();
} }
} }
@ -162,7 +162,7 @@ void CSVWorld::ScriptEdit::dropEvent(QDropEvent* event)
return; return;
} }
setTextCursor(cursorForPosition(event->pos())); setTextCursor(cursorForPosition(event->position().toPoint()));
if (mime->fromDocument(mDocument)) if (mime->fromDocument(mDocument))
{ {

View file

@ -592,7 +592,7 @@ void CSVWorld::Table::moveRecords(QDropEvent* event)
if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant))
return; return;
QModelIndex targedIndex = indexAt(event->pos()); QModelIndex targedIndex = indexAt(event->position().toPoint());
QModelIndexList selectedRows = selectionModel()->selectedRows(); QModelIndexList selectedRows = selectionModel()->selectedRows();
int targetRowRaw = targedIndex.row(); int targetRowRaw = targedIndex.row();
@ -872,7 +872,7 @@ void CSVWorld::Table::mouseMoveEvent(QMouseEvent* event)
{ {
if (event->buttons() & Qt::LeftButton) if (event->buttons() & Qt::LeftButton)
{ {
startDragFromTable(*this, indexAt(event->pos())); startDragFromTable(*this, indexAt(event->position().toPoint()));
} }
} }

View file

@ -31,7 +31,7 @@ namespace CSVWorld
auto& clickEvent = static_cast<QMouseEvent&>(*event); auto& clickEvent = static_cast<QMouseEvent&>(*event);
if ((clickEvent.button() == Qt::MiddleButton)) if ((clickEvent.button() == Qt::MiddleButton))
{ {
const auto& index = table.indexAt(clickEvent.pos()); const auto& index = table.indexAt(clickEvent.position().toPoint());
table.setColumnHidden(index.column(), true); table.setColumnHidden(index.column(), true);
clickEvent.accept(); clickEvent.accept();
return true; return true;

View file

@ -171,7 +171,7 @@ QWidget* CSVWorld::CommandDelegate::createEditor(
// TODO: Find a better solution? // TODO: Find a better solution?
if (display == CSMWorld::ColumnBase::Display_Boolean) if (display == CSMWorld::ColumnBase::Display_Boolean)
{ {
return QItemEditorFactory::defaultFactory()->createEditor(QVariant::Bool, parent); return QItemEditorFactory::defaultFactory()->createEditor(QMetaType::Bool, parent);
} }
// For tables the pop-up of the color editor should appear immediately after the editor creation // For tables the pop-up of the color editor should appear immediately after the editor creation
// (the third parameter of ColorEditor's constructor) // (the third parameter of ColorEditor's constructor)
@ -362,11 +362,7 @@ void CSVWorld::CommandDelegate::setEditorData(QWidget* editor, const QModelIndex
if (!n.isEmpty()) if (!n.isEmpty())
{ {
if (!variant.isValid()) if (!variant.isValid())
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
variant = QVariant(editor->property(n).metaType(), (const void*)nullptr); variant = QVariant(editor->property(n).metaType(), (const void*)nullptr);
#else
variant = QVariant(editor->property(n).userType(), (const void*)nullptr);
#endif
editor->setProperty(n, variant); editor->setProperty(n, variant);
} }
} }

View file

@ -9,6 +9,7 @@
#include <SDL_events.h> #include <SDL_events.h>
#include "../mwgui/mode.hpp" #include "../mwgui/mode.hpp"
#include "../mwmechanics/damagesourcetype.hpp"
#include "../mwrender/animationpriority.hpp" #include "../mwrender/animationpriority.hpp"
#include <components/sdlutil/events.hpp> #include <components/sdlutil/events.hpp>
@ -39,6 +40,11 @@ namespace LuaUtil
} }
} }
namespace osg
{
class Vec3f;
}
namespace MWBase namespace MWBase
{ {
// \brief LuaManager is the central interface through which the engine invokes lua scripts. // \brief LuaManager is the central interface through which the engine invokes lua scripts.
@ -71,6 +77,10 @@ namespace MWBase
= 0; = 0;
virtual void skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) = 0; virtual void skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) = 0;
virtual void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) = 0; virtual void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) = 0;
virtual void onHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& weapon,
const MWWorld::Ptr& ammo, int attackType, float attackStrength, float damage, bool isHealth,
const osg::Vec3f& hitPos, bool successful, MWMechanics::DamageSourceType)
= 0;
virtual void exteriorCreated(MWWorld::CellStore& cell) = 0; virtual void exteriorCreated(MWWorld::CellStore& cell) = 0;
virtual void actorDied(const MWWorld::Ptr& actor) = 0; virtual void actorDied(const MWWorld::Ptr& actor) = 0;
virtual void questUpdated(const ESM::RefId& questId, int stage) = 0; virtual void questUpdated(const ESM::RefId& questId, int stage) = 0;

View file

@ -8,9 +8,6 @@
#include <string_view> #include <string_view>
#include <vector> #include <vector>
#include "../mwmechanics/greetingstate.hpp"
#include "../mwrender/animationpriority.hpp"
#include "../mwworld/ptr.hpp" #include "../mwworld/ptr.hpp"
namespace osg namespace osg
@ -27,6 +24,11 @@ namespace ESM
class ESMWriter; class ESMWriter;
} }
namespace MWMechanics
{
enum class GreetingState;
}
namespace MWWorld namespace MWWorld
{ {
class Ptr; class Ptr;

View file

@ -526,9 +526,6 @@ namespace MWBase
/// Spawn a random creature from a levelled list next to the player /// Spawn a random creature from a levelled list next to the player
virtual void spawnRandomCreature(const ESM::RefId& creatureList) = 0; virtual void spawnRandomCreature(const ESM::RefId& creatureList) = 0;
/// Spawn a blood effect for \a ptr at \a worldPosition
virtual void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0;
virtual void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, virtual void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride,
const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true, bool useAmbientLight = true) const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true, bool useAmbientLight = true)
= 0; = 0;

View file

@ -12,6 +12,8 @@
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwlua/localscripts.hpp"
#include "../mwworld/actionequip.hpp" #include "../mwworld/actionequip.hpp"
#include "../mwworld/cellstore.hpp" #include "../mwworld/cellstore.hpp"
#include "../mwworld/containerstore.hpp" #include "../mwworld/containerstore.hpp"
@ -109,8 +111,23 @@ namespace MWClass
return std::make_pair(slots_, false); return std::make_pair(slots_, false);
} }
ESM::RefId Armor::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const ESM::RefId Armor::getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const
{ {
// We don't actually need an actor as such. We just need an object that has
// lua scripts and the Combat interface.
if (useLuaInterfaceIfAvailable)
{
// In this interface call, both objects are effectively const, so stripping Const from the ConstPtr is fine.
MWWorld::Ptr mutablePtr(
const_cast<MWWorld::LiveCellRefBase*>(ptr.mRef), const_cast<MWWorld::CellStore*>(ptr.mCell));
auto res = MWLua::LocalScripts::callPlayerInterface<std::string>(
"Combat", "getArmorSkill", MWLua::LObject(mutablePtr));
if (res)
return ESM::RefId::deserializeText(res.value());
}
// Fallback to the old engine implementation when actors don't have their scripts attached yet.
const MWWorld::LiveCellRef<ESM::Armor>* ref = ptr.get<ESM::Armor>(); const MWWorld::LiveCellRef<ESM::Armor>* ref = ptr.get<ESM::Armor>();
std::string_view typeGmst; std::string_view typeGmst;
@ -175,7 +192,7 @@ namespace MWClass
const ESM::RefId& Armor::getUpSoundId(const MWWorld::ConstPtr& ptr) const const ESM::RefId& Armor::getUpSoundId(const MWWorld::ConstPtr& ptr) const
{ {
const ESM::RefId es = getEquipmentSkill(ptr); const ESM::RefId es = getEquipmentSkill(ptr, false);
static const ESM::RefId lightUp = ESM::RefId::stringRefId("Item Armor Light Up"); static const ESM::RefId lightUp = ESM::RefId::stringRefId("Item Armor Light Up");
static const ESM::RefId mediumUp = ESM::RefId::stringRefId("Item Armor Medium Up"); static const ESM::RefId mediumUp = ESM::RefId::stringRefId("Item Armor Medium Up");
static const ESM::RefId heavyUp = ESM::RefId::stringRefId("Item Armor Heavy Up"); static const ESM::RefId heavyUp = ESM::RefId::stringRefId("Item Armor Heavy Up");
@ -190,7 +207,7 @@ namespace MWClass
const ESM::RefId& Armor::getDownSoundId(const MWWorld::ConstPtr& ptr) const const ESM::RefId& Armor::getDownSoundId(const MWWorld::ConstPtr& ptr) const
{ {
const ESM::RefId es = getEquipmentSkill(ptr); const ESM::RefId es = getEquipmentSkill(ptr, false);
static const ESM::RefId lightDown = ESM::RefId::stringRefId("Item Armor Light Down"); static const ESM::RefId lightDown = ESM::RefId::stringRefId("Item Armor Light Down");
static const ESM::RefId mediumDown = ESM::RefId::stringRefId("Item Armor Medium Down"); static const ESM::RefId mediumDown = ESM::RefId::stringRefId("Item Armor Medium Down");
static const ESM::RefId heavyDown = ESM::RefId::stringRefId("Item Armor Heavy Down"); static const ESM::RefId heavyDown = ESM::RefId::stringRefId("Item Armor Heavy Down");
@ -221,24 +238,29 @@ namespace MWClass
std::string text; std::string text;
// get armor type string (light/medium/heavy) // get armor type string (light/medium/heavy)
std::string_view typeText; std::string typeText;
if (ref->mBase->mData.mWeight == 0) if (ref->mBase->mData.mWeight == 0)
{ {
// no type // no type
} }
else else
{ {
const ESM::RefId armorType = getEquipmentSkill(ptr); const ESM::RefId armorType = getEquipmentSkill(ptr, true);
if (armorType == ESM::Skill::LightArmor) if (armorType == ESM::Skill::LightArmor)
typeText = "#{sLight}"; typeText = "#{sLight}";
else if (armorType == ESM::Skill::MediumArmor) else if (armorType == ESM::Skill::MediumArmor)
typeText = "#{sMedium}"; typeText = "#{sMedium}";
else else if (armorType == ESM::Skill::HeavyArmor)
typeText = "#{sHeavy}"; typeText = "#{sHeavy}";
// For other skills, just subtitute the skill name
// Normally you would never see this case, but modding allows getEquipmentSkill() to return any skill.
else
typeText = "#{sSkill" + armorType.toString() + "}";
} }
text += "\n#{sArmorRating}: " text += "\n#{sArmorRating}: "
+ MWGui::ToolTips::toString(static_cast<int>(getEffectiveArmorRating(ptr, MWMechanics::getPlayer()))); + MWGui::ToolTips::toString(
static_cast<int>(getSkillAdjustedArmorRating(ptr, MWMechanics::getPlayer(), true)));
int remainingHealth = getItemHealth(ptr); int remainingHealth = getItemHealth(ptr);
text += "\n#{sCondition}: " + MWGui::ToolTips::toString(remainingHealth) + "/" text += "\n#{sCondition}: " + MWGui::ToolTips::toString(remainingHealth) + "/"
@ -289,11 +311,25 @@ namespace MWClass
return record->mId; return record->mId;
} }
float Armor::getEffectiveArmorRating(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& actor) const float Armor::getSkillAdjustedArmorRating(
const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& actor, bool useLuaInterfaceIfAvailable) const
{ {
if (useLuaInterfaceIfAvailable && actor == MWMechanics::getPlayer())
{
// In this interface call, both objects are effectively const, so stripping Const from the ConstPtr is fine.
MWWorld::Ptr mutablePtr(
const_cast<MWWorld::LiveCellRefBase*>(ptr.mRef), const_cast<MWWorld::CellStore*>(ptr.mCell));
auto res = MWLua::LocalScripts::callPlayerInterface<float>(
"Combat", "getSkillAdjustedArmorRating", MWLua::LObject(mutablePtr), MWLua::LObject(actor));
if (res)
return res.value();
}
// Fallback to the old engine implementation when actors don't have their scripts attached yet.
const MWWorld::LiveCellRef<ESM::Armor>* ref = ptr.get<ESM::Armor>(); const MWWorld::LiveCellRef<ESM::Armor>* ref = ptr.get<ESM::Armor>();
const ESM::RefId armorSkillType = getEquipmentSkill(ptr); const ESM::RefId armorSkillType = getEquipmentSkill(ptr, useLuaInterfaceIfAvailable);
float armorSkill = actor.getClass().getSkill(actor, armorSkillType); float armorSkill = actor.getClass().getSkill(actor, armorSkillType);
int iBaseArmorSkill = MWBase::Environment::get() int iBaseArmorSkill = MWBase::Environment::get()

View file

@ -41,7 +41,7 @@ namespace MWClass
///< \return first: Return IDs of the slot this object can be equipped in; second: can object ///< \return first: Return IDs of the slot this object can be equipped in; second: can object
/// stay stacked when equipped? /// stay stacked when equipped?
ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override; ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const override;
MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override;
///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip.
@ -81,7 +81,8 @@ namespace MWClass
bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override;
/// Get the effective armor rating, factoring in the actor's skills, for the given armor. /// Get the effective armor rating, factoring in the actor's skills, for the given armor.
float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const override; float getSkillAdjustedArmorRating(
const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor, bool useLuaInterfaceIfAvailable) const override;
}; };
} }

View file

@ -98,7 +98,7 @@ namespace MWClass
return std::make_pair(slots_, false); return std::make_pair(slots_, false);
} }
ESM::RefId Clothing::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const ESM::RefId Clothing::getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const
{ {
const MWWorld::LiveCellRef<ESM::Clothing>* ref = ptr.get<ESM::Clothing>(); const MWWorld::LiveCellRef<ESM::Clothing>* ref = ptr.get<ESM::Clothing>();

View file

@ -33,7 +33,7 @@ namespace MWClass
///< \return first: Return IDs of the slot this object can be equipped in; second: can object ///< \return first: Return IDs of the slot this object can be equipped in; second: can object
/// stay stacked when equipped? /// stay stacked when equipped?
ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override; ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const override;
MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override;
///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip.

View file

@ -25,11 +25,14 @@
#include "../mwmechanics/setbaseaisetting.hpp" #include "../mwmechanics/setbaseaisetting.hpp"
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/luamanager.hpp"
#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/soundmanager.hpp" #include "../mwbase/soundmanager.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwlua/localscripts.hpp"
#include "../mwworld/actionopen.hpp" #include "../mwworld/actionopen.hpp"
#include "../mwworld/actiontalk.hpp" #include "../mwworld/actiontalk.hpp"
#include "../mwworld/cellstore.hpp" #include "../mwworld/cellstore.hpp"
@ -283,8 +286,8 @@ namespace MWClass
if (!success) if (!success)
{ {
victim.getClass().onHit( MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength,
victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); 0.0f, false, hitPosition, false, MWMechanics::DamageSourceType::Melee);
MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr);
return; return;
} }
@ -342,12 +345,12 @@ namespace MWClass
MWMechanics::diseaseContact(victim, ptr); MWMechanics::diseaseContact(victim, ptr);
victim.getClass().onHit( MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength,
victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee); damage, healthdmg, hitPosition, true, MWMechanics::DamageSourceType::Melee);
} }
void Creature::onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, void Creature::onHit(const MWWorld::Ptr& ptr, const std::map<std::string, float>& damages,
const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, const MWWorld::Ptr& object, const MWWorld::Ptr& attacker, bool successful,
const MWMechanics::DamageSourceType sourceType) const const MWMechanics::DamageSourceType sourceType) const
{ {
MWMechanics::CreatureStats& stats = getCreatureStats(ptr); MWMechanics::CreatureStats& stats = getCreatureStats(ptr);
@ -397,19 +400,44 @@ namespace MWClass
if (!successful) if (!successful)
{ {
// Missed // Missed
if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer())
MWBase::Environment::get().getSoundManager()->playSound3D(
ptr, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f);
return; return;
} }
if (!object.isEmpty()) if (!object.isEmpty())
stats.setLastHitObject(object.getCellRef().getRefId()); stats.setLastHitObject(object.getCellRef().getRefId());
if (damage < 0.001f) bool hasDamage = false;
damage = 0; bool hasHealthDamage = false;
float healthDamage = 0.f;
for (auto& [stat, damage] : damages)
{
if (damage < 0.001f)
continue;
hasDamage = true;
if (damage > 0.f) if (stat == "health")
{
hasHealthDamage = true;
healthDamage = damage;
MWMechanics::DynamicStat<float> health(getCreatureStats(ptr).getHealth());
health.setCurrent(health.getCurrent() - damage);
stats.setHealth(health);
}
else if (stat == "fatigue")
{
MWMechanics::DynamicStat<float> fatigue(getCreatureStats(ptr).getFatigue());
fatigue.setCurrent(fatigue.getCurrent() - damage, true);
stats.setFatigue(fatigue);
}
else if (stat == "magicka")
{
MWMechanics::DynamicStat<float> magicka(getCreatureStats(ptr).getMagicka());
magicka.setCurrent(magicka.getCurrent() - damage);
stats.setMagicka(magicka);
}
}
if (hasDamage)
{ {
if (!attacker.isEmpty()) if (!attacker.isEmpty())
{ {
@ -420,35 +448,11 @@ namespace MWClass
* getGmst().iKnockDownOddsMult->mValue.getInteger() * 0.01f * getGmst().iKnockDownOddsMult->mValue.getInteger() * 0.01f
+ getGmst().iKnockDownOddsBase->mValue.getInteger(); + getGmst().iKnockDownOddsBase->mValue.getInteger();
auto& prng = MWBase::Environment::get().getWorld()->getPrng(); auto& prng = MWBase::Environment::get().getWorld()->getPrng();
if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99(prng)) if (hasHealthDamage && agilityTerm <= healthDamage && knockdownTerm <= Misc::Rng::roll0to99(prng))
stats.setKnockedDown(true); stats.setKnockedDown(true);
else else
stats.setHitRecovery(true); // Is this supposed to always occur? stats.setHitRecovery(true); // Is this supposed to always occur?
} }
if (ishealth)
{
damage *= damage / (damage + getArmorRating(ptr));
damage = std::max(1.f, damage);
if (!attacker.isEmpty())
{
damage = scaleDamage(damage, attacker, ptr);
MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition);
}
MWBase::Environment::get().getSoundManager()->playSound3D(
ptr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f);
MWMechanics::DynamicStat<float> health(stats.getHealth());
health.setCurrent(health.getCurrent() - damage);
stats.setHealth(health);
}
else
{
MWMechanics::DynamicStat<float> fatigue(stats.getFatigue());
fatigue.setCurrent(fatigue.getCurrent() - damage, true);
stats.setFatigue(fatigue);
}
} }
} }
@ -591,7 +595,7 @@ namespace MWClass
return info; return info;
} }
float Creature::getArmorRating(const MWWorld::Ptr& ptr) const float Creature::getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const
{ {
// Equipment armor rating is deliberately ignored. // Equipment armor rating is deliberately ignored.
return getCreatureStats(ptr).getMagicEffects().getOrDefault(ESM::MagicEffect::Shield).getMagnitude(); return getCreatureStats(ptr).getMagicEffects().getOrDefault(ESM::MagicEffect::Shield).getMagnitude();
@ -764,11 +768,6 @@ namespace MWClass
} }
} }
int Creature::getBloodTexture(const MWWorld::ConstPtr& ptr) const
{
return ptr.get<ESM::Creature>()->mBase->mBloodType;
}
void Creature::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const void Creature::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const
{ {
if (!state.mHasCustomState) if (!state.mHasCustomState)

View file

@ -66,8 +66,8 @@ namespace MWClass
void hit(const MWWorld::Ptr& ptr, float attackStrength, int type, const MWWorld::Ptr& victim, void hit(const MWWorld::Ptr& ptr, float attackStrength, int type, const MWWorld::Ptr& victim,
const osg::Vec3f& hitPosition, bool success) const override; const osg::Vec3f& hitPosition, bool success) const override;
void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, void onHit(const MWWorld::Ptr& ptr, const std::map<std::string, float>& damages, const MWWorld::Ptr& object,
const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, const MWWorld::Ptr& attacker, bool successful,
const MWMechanics::DamageSourceType sourceType) const override; const MWMechanics::DamageSourceType sourceType) const override;
std::unique_ptr<MWWorld::Action> activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; std::unique_ptr<MWWorld::Action> activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override;
@ -88,7 +88,7 @@ namespace MWClass
///< Return total weight that fits into the object. Throws an exception, if the object can't ///< Return total weight that fits into the object. Throws an exception, if the object can't
/// hold other objects. /// hold other objects.
float getArmorRating(const MWWorld::Ptr& ptr) const override; float getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const override;
///< @return combined armor rating of this actor ///< @return combined armor rating of this actor
bool isEssential(const MWWorld::ConstPtr& ptr) const override; bool isEssential(const MWWorld::ConstPtr& ptr) const override;
@ -118,9 +118,6 @@ namespace MWClass
float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const override; float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const override;
/// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini)
int getBloodTexture(const MWWorld::ConstPtr& ptr) const override;
void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override;
///< Read additional state from \a state into \a ptr. ///< Read additional state from \a state into \a ptr.

View file

@ -29,6 +29,8 @@
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwlua/localscripts.hpp"
#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/actorutil.hpp"
#include "../mwmechanics/aisetting.hpp" #include "../mwmechanics/aisetting.hpp"
#include "../mwmechanics/autocalcspell.hpp" #include "../mwmechanics/autocalcspell.hpp"
@ -620,8 +622,8 @@ namespace MWClass
float damage = 0.0f; float damage = 0.0f;
if (!success) if (!success)
{ {
othercls.onHit( MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength,
victim, damage, false, weapon, ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); damage, false, hitPosition, false, MWMechanics::DamageSourceType::Melee);
MWMechanics::reduceWeaponCondition(damage, false, weapon, ptr); MWMechanics::reduceWeaponCondition(damage, false, weapon, ptr);
MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage);
return; return;
@ -694,14 +696,13 @@ namespace MWClass
MWMechanics::diseaseContact(victim, ptr); MWMechanics::diseaseContact(victim, ptr);
othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee); MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength,
damage, healthdmg, hitPosition, true, MWMechanics::DamageSourceType::Melee);
} }
void Npc::onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, void Npc::onHit(const MWWorld::Ptr& ptr, const std::map<std::string, float>& damages, const MWWorld::Ptr& object,
const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, const MWWorld::Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const
const MWMechanics::DamageSourceType sourceType) const
{ {
MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager();
MWMechanics::CreatureStats& stats = getCreatureStats(ptr); MWMechanics::CreatureStats& stats = getCreatureStats(ptr);
bool wasDead = stats.isDead(); bool wasDead = stats.isDead();
@ -748,23 +749,47 @@ namespace MWClass
if (!successful) if (!successful)
{ {
// Missed // Missed
if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer())
sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f);
return; return;
} }
if (!object.isEmpty()) if (!object.isEmpty())
stats.setLastHitObject(object.getCellRef().getRefId()); stats.setLastHitObject(object.getCellRef().getRefId());
if (damage < 0.001f) if (ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState())
damage = 0; return;
bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); bool hasDamage = false;
bool hasHealthDamage = false;
float healthDamage = 0.f;
for (auto& [stat, damage] : damages)
{
if (damage < 0.001f)
continue;
hasDamage = true;
if (godmode) if (stat == "health")
damage = 0; {
hasHealthDamage = true;
healthDamage = damage;
MWMechanics::DynamicStat<float> health(getCreatureStats(ptr).getHealth());
health.setCurrent(health.getCurrent() - damage);
stats.setHealth(health);
}
else if (stat == "fatigue")
{
MWMechanics::DynamicStat<float> fatigue(getCreatureStats(ptr).getFatigue());
fatigue.setCurrent(fatigue.getCurrent() - damage, true);
stats.setFatigue(fatigue);
}
else if (stat == "magicka")
{
MWMechanics::DynamicStat<float> magicka(getCreatureStats(ptr).getMagicka());
magicka.setCurrent(magicka.getCurrent() - damage);
stats.setMagicka(magicka);
}
}
if (damage > 0.0f && !attacker.isEmpty()) if (hasDamage && !attacker.isEmpty())
{ {
// 'ptr' is losing health. Play a 'hit' voiced dialog entry if not already saying // 'ptr' is losing health. Play a 'hit' voiced dialog entry if not already saying
// something, alert the character controller, scripts, etc. // something, alert the character controller, scripts, etc.
@ -783,109 +808,16 @@ namespace MWClass
float knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() float knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified()
* gmst.iKnockDownOddsMult->mValue.getInteger() * 0.01f * gmst.iKnockDownOddsMult->mValue.getInteger() * 0.01f
+ gmst.iKnockDownOddsBase->mValue.getInteger(); + gmst.iKnockDownOddsBase->mValue.getInteger();
if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99(prng)) if (hasHealthDamage && agilityTerm <= healthDamage && knockdownTerm <= Misc::Rng::roll0to99(prng))
stats.setKnockedDown(true); stats.setKnockedDown(true);
else else
stats.setHitRecovery(true); // Is this supposed to always occur? stats.setHitRecovery(true); // Is this supposed to always occur?
if (damage > 0 && ishealth)
{
// Hit percentages:
// cuirass = 30%
// shield, helmet, greaves, boots, pauldrons = 10% each
// guantlets = 5% each
static const int hitslots[20]
= { MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass,
MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass,
MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass,
MWWorld::InventoryStore::Slot_CarriedLeft, MWWorld::InventoryStore::Slot_CarriedLeft,
MWWorld::InventoryStore::Slot_Helmet, MWWorld::InventoryStore::Slot_Helmet,
MWWorld::InventoryStore::Slot_Greaves, MWWorld::InventoryStore::Slot_Greaves,
MWWorld::InventoryStore::Slot_Boots, MWWorld::InventoryStore::Slot_Boots,
MWWorld::InventoryStore::Slot_LeftPauldron, MWWorld::InventoryStore::Slot_LeftPauldron,
MWWorld::InventoryStore::Slot_RightPauldron, MWWorld::InventoryStore::Slot_RightPauldron,
MWWorld::InventoryStore::Slot_LeftGauntlet, MWWorld::InventoryStore::Slot_RightGauntlet };
int hitslot = hitslots[Misc::Rng::rollDice(20, prng)];
float unmitigatedDamage = damage;
float x = damage / (damage + getArmorRating(ptr));
damage *= std::max(gmst.fCombatArmorMinMult->mValue.getFloat(), x);
int damageDiff = static_cast<int>(unmitigatedDamage - damage);
damage = std::max(1.f, damage);
damageDiff = std::max(1, damageDiff);
MWWorld::InventoryStore& inv = getInventoryStore(ptr);
MWWorld::ContainerStoreIterator armorslot = inv.getSlot(hitslot);
MWWorld::Ptr armor = ((armorslot != inv.end()) ? *armorslot : MWWorld::Ptr());
bool hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId;
// If there's no item in the carried left slot or if it is not a shield redistribute the hit.
if (!hasArmor && hitslot == MWWorld::InventoryStore::Slot_CarriedLeft)
{
if (Misc::Rng::rollDice(2, prng) == 0)
hitslot = MWWorld::InventoryStore::Slot_Cuirass;
else
hitslot = MWWorld::InventoryStore::Slot_LeftPauldron;
armorslot = inv.getSlot(hitslot);
if (armorslot != inv.end())
{
armor = *armorslot;
hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId;
}
}
if (hasArmor)
{
// Unarmed creature attacks don't affect armor condition unless it was
// explicitly requested.
if (!object.isEmpty() || attacker.isEmpty() || attacker.getClass().isNpc()
|| Settings::game().mUnarmedCreatureAttacksDamageArmor)
{
int armorhealth = armor.getClass().getItemHealth(armor);
armorhealth -= std::min(damageDiff, armorhealth);
armor.getCellRef().setCharge(armorhealth);
// Armor broken? unequip it
if (armorhealth == 0)
armor = *inv.unequipItem(armor);
}
ESM::RefId skill = armor.getClass().getEquipmentSkill(armor);
if (ptr == MWMechanics::getPlayer())
skillUsageSucceeded(ptr, skill, ESM::Skill::Armor_HitByOpponent);
if (skill == ESM::Skill::LightArmor)
sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Light Armor Hit"), 1.0f, 1.0f);
else if (skill == ESM::Skill::MediumArmor)
sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Medium Armor Hit"), 1.0f, 1.0f);
else if (skill == ESM::Skill::HeavyArmor)
sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Heavy Armor Hit"), 1.0f, 1.0f);
}
else if (ptr == MWMechanics::getPlayer())
skillUsageSucceeded(ptr, ESM::Skill::Unarmored, ESM::Skill::Armor_HitByOpponent);
}
} }
if (ishealth) if (hasHealthDamage && healthDamage > 0.0f)
{ {
if (!attacker.isEmpty() && !godmode) if (ptr == MWMechanics::getPlayer())
damage = scaleDamage(damage, attacker, ptr); MWBase::Environment::get().getWindowManager()->activateHitOverlay();
if (damage > 0.0f)
{
sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f);
if (ptr == MWMechanics::getPlayer())
MWBase::Environment::get().getWindowManager()->activateHitOverlay();
if (!attacker.isEmpty())
MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition);
}
MWMechanics::DynamicStat<float> health(getCreatureStats(ptr).getHealth());
health.setCurrent(health.getCurrent() - damage);
stats.setHealth(health);
}
else
{
MWMechanics::DynamicStat<float> fatigue(getCreatureStats(ptr).getFatigue());
fatigue.setCurrent(fatigue.getCurrent() - damage, true);
stats.setFatigue(fatigue);
} }
if (!wasDead && getCreatureStats(ptr).isDead()) if (!wasDead && getCreatureStats(ptr).isDead())
@ -1136,8 +1068,17 @@ namespace MWClass
MWBase::Environment::get().getLuaManager()->skillUse(ptr, skill, usageType, extraFactor); MWBase::Environment::get().getLuaManager()->skillUse(ptr, skill, usageType, extraFactor);
} }
float Npc::getArmorRating(const MWWorld::Ptr& ptr) const float Npc::getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const
{ {
if (useLuaInterfaceIfAvailable && ptr == MWMechanics::getPlayer())
{
auto res = MWLua::LocalScripts::callPlayerInterface<float>("Combat", "getArmorRating");
if (res)
return res.value();
}
// Fallback to the old engine implementation when actors don't have their scripts attached yet.
const MWWorld::Store<ESM::GameSetting>& store const MWWorld::Store<ESM::GameSetting>& store
= MWBase::Environment::get().getESMStore()->get<ESM::GameSetting>(); = MWBase::Environment::get().getESMStore()->get<ESM::GameSetting>();
@ -1159,7 +1100,7 @@ namespace MWClass
} }
else else
{ {
ratings[i] = it->getClass().getEffectiveArmorRating(*it, ptr); ratings[i] = it->getClass().getSkillAdjustedArmorRating(*it, ptr);
// Take in account armor condition // Take in account armor condition
const bool hasHealth = it->getClass().hasItemHealth(*it); const bool hasHealth = it->getClass().hasItemHealth(*it);
@ -1308,11 +1249,6 @@ namespace MWClass
return getNpcStats(ptr).getSkill(id).getModified(); return getNpcStats(ptr).getSkill(id).getModified();
} }
int Npc::getBloodTexture(const MWWorld::ConstPtr& ptr) const
{
return ptr.get<ESM::NPC>()->mBase->mBloodType;
}
void Npc::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const void Npc::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const
{ {
if (!state.mHasCustomState) if (!state.mHasCustomState)

View file

@ -81,8 +81,8 @@ namespace MWClass
void hit(const MWWorld::Ptr& ptr, float attackStrength, int type, const MWWorld::Ptr& victim, void hit(const MWWorld::Ptr& ptr, float attackStrength, int type, const MWWorld::Ptr& victim,
const osg::Vec3f& hitPosition, bool success) const override; const osg::Vec3f& hitPosition, bool success) const override;
void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, void onHit(const MWWorld::Ptr& ptr, const std::map<std::string, float>& damages, const MWWorld::Ptr& object,
const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, const MWWorld::Ptr& attacker, bool successful,
const MWMechanics::DamageSourceType sourceType) const override; const MWMechanics::DamageSourceType sourceType) const override;
void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector<std::string_view>& models) const override; void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector<std::string_view>& models) const override;
@ -112,7 +112,7 @@ namespace MWClass
///< Returns total weight of objects inside this object (including modifications from magic ///< Returns total weight of objects inside this object (including modifications from magic
/// effects). Throws an exception, if the object can't hold other objects. /// effects). Throws an exception, if the object can't hold other objects.
float getArmorRating(const MWWorld::Ptr& ptr) const override; float getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const override;
///< @return combined armor rating of this actor ///< @return combined armor rating of this actor
void adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const override; void adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const override;
@ -137,9 +137,6 @@ namespace MWClass
float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const override; float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const override;
/// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini)
int getBloodTexture(const MWWorld::ConstPtr& ptr) const override;
bool isNpc() const override { return true; } bool isNpc() const override { return true; }
void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override;

View file

@ -105,7 +105,7 @@ namespace MWClass
return std::make_pair(slots_, stack); return std::make_pair(slots_, stack);
} }
ESM::RefId Weapon::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const ESM::RefId Weapon::getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const
{ {
const MWWorld::LiveCellRef<ESM::Weapon>* ref = ptr.get<ESM::Weapon>(); const MWWorld::LiveCellRef<ESM::Weapon>* ref = ptr.get<ESM::Weapon>();
int type = ref->mBase->mData.mType; int type = ref->mBase->mData.mType;

View file

@ -42,7 +42,7 @@ namespace MWClass
///< \return first: Return IDs of the slot this object can be equipped in; second: can object ///< \return first: Return IDs of the slot this object can be equipped in; second: can object
/// stay stacked when equipped? /// stay stacked when equipped?
ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override; ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const override;
int getValue(const MWWorld::ConstPtr& ptr) const override; int getValue(const MWWorld::ConstPtr& ptr) const override;
///< Return trade value of the object. Throws an exception, if the object can't be traded. ///< Return trade value of the object. Throws an exception, if the object can't be traded.

View file

@ -470,11 +470,10 @@ namespace MWGui
if (mPtr.isEmpty()) if (mPtr.isEmpty())
return; return;
mArmorRating->setCaptionWithReplacing( auto rating = MyGUI::utility::toString(static_cast<int>(mPtr.getClass().getArmorRating(mPtr, true)));
"#{sArmor}: " + MyGUI::utility::toString(static_cast<int>(mPtr.getClass().getArmorRating(mPtr)))); mArmorRating->setCaptionWithReplacing("#{sArmor}: " + rating);
if (mArmorRating->getTextSize().width > mArmorRating->getSize().width) if (mArmorRating->getTextSize().width > mArmorRating->getSize().width)
mArmorRating->setCaptionWithReplacing( mArmorRating->setCaptionWithReplacing(rating);
MyGUI::utility::toString(static_cast<int>(mPtr.getClass().getArmorRating(mPtr))));
} }
void InventoryWindow::updatePreviewSize() void InventoryWindow::updatePreviewSize()

View file

@ -11,6 +11,7 @@
#include <components/lua/util.hpp> #include <components/lua/util.hpp>
#include <components/misc/strings/algorithm.hpp> #include <components/misc/strings/algorithm.hpp>
#include <components/misc/strings/lower.hpp> #include <components/misc/strings/lower.hpp>
#include <components/settings/values.hpp>
#include <components/version/version.hpp> #include <components/version/version.hpp>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
@ -159,6 +160,8 @@ namespace MWLua
}; };
} }
api["getGameDifficulty"] = []() { return Settings::game().mDifficulty.get(); };
sol::table readOnlyApi = LuaUtil::makeReadOnly(api); sol::table readOnlyApi = LuaUtil::makeReadOnly(api);
return context.setTypePackage(readOnlyApi, "openmw_core"); return context.setTypePackage(readOnlyApi, "openmw_core");
} }

View file

@ -10,6 +10,7 @@
#include <components/lua/scriptscontainer.hpp> #include <components/lua/scriptscontainer.hpp>
#include "../mwbase/luamanager.hpp" #include "../mwbase/luamanager.hpp"
#include "../mwmechanics/actorutil.hpp"
#include "object.hpp" #include "object.hpp"
@ -91,6 +92,19 @@ namespace MWLua
void applyStatsCache(); void applyStatsCache();
// Calls a lua interface on the player's scripts. This call is only meant for use in updating UI elements.
template <typename T, typename... Args>
static std::optional<T> callPlayerInterface(
std::string_view interfaceName, std::string_view identifier, const Args&... args)
{
auto player = MWMechanics::getPlayer();
auto scripts = player.getRefData().getLuaScripts();
if (scripts)
return scripts->callInterface<T>(interfaceName, identifier, args...);
return std::nullopt;
}
protected: protected:
SelfObject mData; SelfObject mData;

View file

@ -5,8 +5,6 @@
#include <MyGUI_InputManager.h> #include <MyGUI_InputManager.h>
#include <osg/Stats> #include <osg/Stats>
#include "sol/state_view.hpp"
#include <components/debug/debuglog.hpp> #include <components/debug/debuglog.hpp>
#include <components/esm/luascripts.hpp> #include <components/esm/luascripts.hpp>
@ -152,6 +150,17 @@ namespace MWLua
}); });
} }
void LuaManager::sendLocalEvent(
const MWWorld::Ptr& target, const std::string& name, const std::optional<sol::table>& data)
{
LuaUtil::BinaryData binary = {};
if (data)
{
binary = LuaUtil::serialize(*data, mLocalSerializer.get());
}
mLuaEvents.addLocalEvent({ getId(target), name, binary });
}
void LuaManager::update() void LuaManager::update()
{ {
if (const int steps = Settings::lua().mGcStepsPerFrame; steps > 0) if (const int steps = Settings::lua().mGcStepsPerFrame; steps > 0)
@ -482,6 +491,49 @@ namespace MWLua
EngineEvents::OnSkillLevelUp{ getId(actor), skillId.serializeText(), std::string(source) }); EngineEvents::OnSkillLevelUp{ getId(actor), skillId.serializeText(), std::string(source) });
} }
void LuaManager::onHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& weapon,
const MWWorld::Ptr& ammo, int attackType, float attackStrength, float damage, bool isHealth,
const osg::Vec3f& hitPos, bool successful, MWMechanics::DamageSourceType sourceType)
{
mLua.protectedCall([&](LuaUtil::LuaView& view) {
sol::table damageTable = view.newTable();
if (isHealth)
damageTable["health"] = damage;
else
damageTable["fatigue"] = damage;
sol::table data = view.newTable();
if (!attacker.isEmpty())
data["attacker"] = LObject(attacker);
if (!weapon.isEmpty())
data["weapon"] = LObject(weapon);
if (!ammo.isEmpty())
data["ammo"] = LObject(weapon);
data["type"] = attackType;
data["strength"] = attackStrength;
data["damage"] = damageTable;
data["hitPos"] = hitPos;
data["successful"] = successful;
switch (sourceType)
{
case MWMechanics::DamageSourceType::Unspecified:
data["sourceType"] = "unspecified";
break;
case MWMechanics::DamageSourceType::Melee:
data["sourceType"] = "melee";
break;
case MWMechanics::DamageSourceType::Ranged:
data["sourceType"] = "ranged";
break;
case MWMechanics::DamageSourceType::Magical:
data["sourceType"] = "magic";
break;
}
sendLocalEvent(victim, "Hit", data);
});
}
void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr)
{ {
mObjectLists.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. mObjectLists.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet.

View file

@ -92,6 +92,9 @@ namespace MWLua
bool loopfallback) override; bool loopfallback) override;
void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) override; void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) override;
void skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) override; void skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) override;
void onHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& weapon,
const MWWorld::Ptr& ammo, int attackType, float attackStrength, float damage, bool isHealth,
const osg::Vec3f& hitPos, bool successful, MWMechanics::DamageSourceType sourceType) override;
void exteriorCreated(MWWorld::CellStore& cell) override void exteriorCreated(MWWorld::CellStore& cell) override
{ {
mEngineEvents.addToQueue(EngineEvents::OnNewExterior{ cell }); mEngineEvents.addToQueue(EngineEvents::OnNewExterior{ cell });
@ -166,6 +169,9 @@ namespace MWLua
LuaUtil::InputAction::Registry& inputActions() { return mInputActions; } LuaUtil::InputAction::Registry& inputActions() { return mInputActions; }
LuaUtil::InputTrigger::Registry& inputTriggers() { return mInputTriggers; } LuaUtil::InputTrigger::Registry& inputTriggers() { return mInputTriggers; }
void sendLocalEvent(
const MWWorld::Ptr& target, const std::string& name, const std::optional<sol::table>& data = std::nullopt);
private: private:
void initConfiguration(); void initConfiguration();
LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr,

View file

@ -976,9 +976,6 @@ namespace MWLua
bool hasDuration = !(mgef->mData.mFlags & ESM::MagicEffect::NoDuration); bool hasDuration = !(mgef->mData.mFlags & ESM::MagicEffect::NoDuration);
effect.mDuration = hasDuration ? static_cast<float>(enam.mData.mDuration) : 1.f; effect.mDuration = hasDuration ? static_cast<float>(enam.mData.mDuration) : 1.f;
bool appliedOnce = mgef->mData.mFlags & ESM::MagicEffect::AppliedOnce;
if (!appliedOnce)
effect.mDuration = std::max(1.f, effect.mDuration);
effect.mTimeLeft = effect.mDuration; effect.mTimeLeft = effect.mDuration;
params.getEffects().emplace_back(effect); params.getEffects().emplace_back(effect);

View file

@ -420,6 +420,42 @@ namespace MWLua
return ptr.getClass().getCapacity(ptr); return ptr.getClass().getCapacity(ptr);
}; };
actor["_onHit"] = [context](const SelfObject& self, const sol::table& options) {
sol::optional<sol::table> damageLua = options.get<sol::optional<sol::table>>("damage");
std::map<std::string, float> damageCpp;
if (damageLua)
{
for (auto& [key, value] : damageLua.value())
{
damageCpp[key.as<std::string>()] = value.as<float>();
}
}
std::string sourceTypeStr = options.get_or<std::string>("sourceType", "unspecified");
MWMechanics::DamageSourceType sourceType = MWMechanics::DamageSourceType::Unspecified;
if (sourceTypeStr == "melee")
sourceType = MWMechanics::DamageSourceType::Melee;
else if (sourceTypeStr == "ranged")
sourceType = MWMechanics::DamageSourceType::Ranged;
else if (sourceTypeStr == "magic")
sourceType = MWMechanics::DamageSourceType::Magical;
sol::optional<Object> weapon = options.get<sol::optional<Object>>("weapon");
sol::optional<Object> ammo = options.get<sol::optional<Object>>("ammo");
context.mLuaManager->addAction(
[self = self, damages = std::move(damageCpp), attacker = options.get<sol::optional<Object>>("attacker"),
weapon = ammo ? ammo : weapon, successful = options.get<bool>("successful"),
sourceType = sourceType] {
MWWorld::Ptr attackerPtr;
MWWorld::Ptr weaponPtr;
if (attacker)
attackerPtr = attacker->ptr();
if (weapon)
weaponPtr = weapon->ptr();
self.ptr().getClass().onHit(self.ptr(), damages, weaponPtr, attackerPtr, successful, sourceType);
},
"HitAction");
};
addActorStatsBindings(actor, context); addActorStatsBindings(actor, context);
addActorMagicBindings(actor, context); addActorMagicBindings(actor, context);
} }

View file

@ -74,6 +74,7 @@ namespace MWLua
[](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Essential; }); [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Essential; });
record["isRespawning"] = sol::readonly_property( record["isRespawning"] = sol::readonly_property(
[](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Respawn; }); [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Respawn; });
record["bloodType"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mBloodType; });
addActorServicesBindings<ESM::Creature>(record, context); addActorServicesBindings<ESM::Creature>(record, context);
} }

View file

@ -102,6 +102,7 @@ namespace MWLua
record["isRespawning"] record["isRespawning"]
= sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Respawn; }); = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Respawn; });
record["baseGold"] = sol::readonly_property([](const ESM::NPC& rec) -> int { return rec.mNpdt.mGold; }); record["baseGold"] = sol::readonly_property([](const ESM::NPC& rec) -> int { return rec.mNpdt.mGold; });
record["bloodType"] = sol::readonly_property([](const ESM::NPC& rec) -> int { return rec.mBloodType; });
addActorServicesBindings<ESM::NPC>(record, context); addActorServicesBindings<ESM::NPC>(record, context);
npc["classes"] = initClassRecordBindings(context); npc["classes"] = initClassRecordBindings(context);

View file

@ -73,7 +73,7 @@ namespace MWMechanics
CharacterController mCharacterController; CharacterController mCharacterController;
int mGreetingTimer{ 0 }; int mGreetingTimer{ 0 };
float mTargetAngleRadians{ 0.f }; float mTargetAngleRadians{ 0.f };
GreetingState mGreetingState{ Greet_None }; GreetingState mGreetingState{ GreetingState::None };
Misc::DeviatingPeriodicTimer mEngageCombat{ 1.0f, 0.25f, Misc::DeviatingPeriodicTimer mEngageCombat{ 1.0f, 0.25f,
Misc::Rng::deviate(0, 0.25f, MWBase::Environment::get().getWorld()->getPrng()) }; Misc::Rng::deviate(0, 0.25f, MWBase::Environment::get().getWorld()->getPrng()) };
bool mIsTurningToPlayer{ false }; bool mIsTurningToPlayer{ false };

View file

@ -50,6 +50,7 @@
#include "attacktype.hpp" #include "attacktype.hpp"
#include "character.hpp" #include "character.hpp"
#include "creaturestats.hpp" #include "creaturestats.hpp"
#include "greetingstate.hpp"
#include "movement.hpp" #include "movement.hpp"
#include "npcstats.hpp" #include "npcstats.hpp"
#include "steering.hpp" #include "steering.hpp"
@ -487,7 +488,7 @@ namespace MWMechanics
{ {
actorState.setTurningToPlayer(false); actorState.setTurningToPlayer(false);
actorState.setGreetingTimer(0); actorState.setGreetingTimer(0);
actorState.setGreetingState(Greet_None); actorState.setGreetingState(GreetingState::None);
return; return;
} }
@ -525,7 +526,7 @@ namespace MWMechanics
int greetingTimer = actorState.getGreetingTimer(); int greetingTimer = actorState.getGreetingTimer();
GreetingState greetingState = actorState.getGreetingState(); GreetingState greetingState = actorState.getGreetingState();
if (greetingState == Greet_None) if (greetingState == GreetingState::None)
{ {
if ((playerPos - actorPos).length2() <= helloDistance * helloDistance && !playerStats.isDead() if ((playerPos - actorPos).length2() <= helloDistance * helloDistance && !playerStats.isDead()
&& !actorStats.isParalyzed() && !isTargetMagicallyHidden(player) && !actorStats.isParalyzed() && !isTargetMagicallyHidden(player)
@ -535,14 +536,14 @@ namespace MWMechanics
if (greetingTimer >= GREETING_SHOULD_START) if (greetingTimer >= GREETING_SHOULD_START)
{ {
greetingState = Greet_InProgress; greetingState = GreetingState::InProgress;
if (!MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("hello"))) if (!MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("hello")))
greetingState = Greet_Done; greetingState = GreetingState::Done;
greetingTimer = 0; greetingTimer = 0;
} }
} }
if (greetingState == Greet_InProgress) if (greetingState == GreetingState::InProgress)
{ {
greetingTimer++; greetingTimer++;
@ -554,16 +555,16 @@ namespace MWMechanics
if (greetingTimer >= GREETING_COOLDOWN) if (greetingTimer >= GREETING_COOLDOWN)
{ {
greetingState = Greet_Done; greetingState = GreetingState::Done;
greetingTimer = 0; greetingTimer = 0;
} }
} }
if (greetingState == Greet_Done) if (greetingState == GreetingState::Done)
{ {
float resetDist = 2 * helloDistance; float resetDist = 2 * helloDistance;
if ((playerPos - actorPos).length2() >= resetDist * resetDist) if ((playerPos - actorPos).length2() >= resetDist * resetDist)
greetingState = Greet_None; greetingState = GreetingState::None;
} }
actorState.setGreetingTimer(greetingTimer); actorState.setGreetingTimer(greetingTimer);
@ -2381,7 +2382,7 @@ namespace MWMechanics
{ {
const auto it = mIndex.find(ptr.mRef); const auto it = mIndex.find(ptr.mRef);
if (it == mIndex.end()) if (it == mIndex.end())
return Greet_None; return GreetingState::None;
return it->second->getGreetingState(); return it->second->getGreetingState();
} }

View file

@ -12,6 +12,7 @@
#include "character.hpp" #include "character.hpp"
#include "creaturestats.hpp" #include "creaturestats.hpp"
#include "greetingstate.hpp"
#include "movement.hpp" #include "movement.hpp"
namespace namespace
@ -77,7 +78,7 @@ namespace MWMechanics
if (!stats.getMovementFlag(CreatureStats::Flag_ForceJump) if (!stats.getMovementFlag(CreatureStats::Flag_ForceJump)
&& !stats.getMovementFlag(CreatureStats::Flag_ForceSneak) && !stats.getMovementFlag(CreatureStats::Flag_ForceSneak)
&& (mechMgr->isTurningToPlayer(actor) || mechMgr->getGreetingState(actor) == Greet_InProgress)) && (mechMgr->isTurningToPlayer(actor) || mechMgr->getGreetingState(actor) == GreetingState::InProgress))
return false; return false;
const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3());

View file

@ -25,6 +25,7 @@
#include "actorutil.hpp" #include "actorutil.hpp"
#include "character.hpp" #include "character.hpp"
#include "creaturestats.hpp" #include "creaturestats.hpp"
#include "greetingstate.hpp"
#include "movement.hpp" #include "movement.hpp"
#include "pathgrid.hpp" #include "pathgrid.hpp"
@ -257,7 +258,7 @@ namespace MWMechanics
&& !cStats.getMovementFlag(CreatureStats::Flag_ForceSneak)) && !cStats.getMovementFlag(CreatureStats::Flag_ForceSneak))
{ {
GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor); GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor);
if (greetingState == Greet_InProgress) if (greetingState == GreetingState::InProgress)
{ {
if (storage.mState == AiWanderStorage::Wander_Walking) if (storage.mState == AiWanderStorage::Wander_Walking)
{ {
@ -526,7 +527,7 @@ namespace MWMechanics
// Check if idle animation finished // Check if idle animation finished
GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor); GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor);
if (!checkIdle(actor, storage.mIdleAnimation) && (greetingState == Greet_Done || greetingState == Greet_None)) if (!checkIdle(actor, storage.mIdleAnimation) && greetingState != GreetingState::InProgress)
{ {
if (mPathFinder.isPathConstructed()) if (mPathFinder.isPathConstructed())
storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid);

View file

@ -12,6 +12,7 @@
#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/dialoguemanager.hpp"
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/luamanager.hpp"
#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/soundmanager.hpp" #include "../mwbase/soundmanager.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
@ -240,8 +241,8 @@ namespace MWMechanics
if (Misc::Rng::roll0to99(world->getPrng()) >= getHitChance(attacker, victim, skillValue)) if (Misc::Rng::roll0to99(world->getPrng()) >= getHitChance(attacker, victim, skillValue))
{ {
victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false, MWBase::Environment::get().getLuaManager()->onHit(attacker, victim, weapon, projectile, 0,
MWMechanics::DamageSourceType::Ranged); attackStrength, damage, false, hitPosition, false, MWMechanics::DamageSourceType::Ranged);
MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker); MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker);
return; return;
} }
@ -299,8 +300,8 @@ namespace MWMechanics
victim.getClass().getContainerStore(victim).add(projectile, 1); victim.getClass().getContainerStore(victim).add(projectile, 1);
} }
victim.getClass().onHit( MWBase::Environment::get().getLuaManager()->onHit(attacker, victim, weapon, projectile, 0, attackStrength,
victim, damage, true, projectile, attacker, hitPosition, true, MWMechanics::DamageSourceType::Ranged); damage, true, hitPosition, true, MWMechanics::DamageSourceType::Ranged);
} }
} }

View file

@ -3,11 +3,11 @@
namespace MWMechanics namespace MWMechanics
{ {
enum GreetingState enum class GreetingState
{ {
Greet_None, None,
Greet_InProgress, InProgress,
Greet_Done Done
}; };
} }

View file

@ -1453,6 +1453,7 @@ namespace MWMechanics
} }
startCombat(actor, player, &playerFollowers); startCombat(actor, player, &playerFollowers);
observerStats.setHitAttemptActorId(player.getClass().getCreatureStats(player).getActorId());
// Apply aggression value to the base Fight rating, so that the actor can continue fighting // Apply aggression value to the base Fight rating, so that the actor can continue fighting
// after a Calm spell wears off // after a Calm spell wears off

View file

@ -359,8 +359,7 @@ namespace
// Notify the target actor they've been hit // Notify the target actor they've been hit
bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful;
if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful)
target.getClass().onHit( target.getClass().onHit(target, {}, MWWorld::Ptr(), caster, true, MWMechanics::DamageSourceType::Magical);
target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true, MWMechanics::DamageSourceType::Magical);
// Apply resistances // Apply resistances
if (!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances)) if (!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances))
{ {

View file

@ -22,6 +22,7 @@
#include "../mwmechanics/aitravel.hpp" #include "../mwmechanics/aitravel.hpp"
#include "../mwmechanics/aiwander.hpp" #include "../mwmechanics/aiwander.hpp"
#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/greetingstate.hpp"
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/mechanicsmanager.hpp"
@ -487,7 +488,7 @@ namespace MWScript
else if (testedTargetId == "Player") // Currently the player ID is hardcoded else if (testedTargetId == "Player") // Currently the player ID is hardcoded
{ {
MWBase::MechanicsManager* mechMgr = MWBase::Environment::get().getMechanicsManager(); MWBase::MechanicsManager* mechMgr = MWBase::Environment::get().getMechanicsManager();
bool greeting = mechMgr->getGreetingState(actor) == MWMechanics::Greet_InProgress; bool greeting = mechMgr->getGreetingState(actor) == MWMechanics::GreetingState::InProgress;
bool sayActive = MWBase::Environment::get().getSoundManager()->sayActive(actor); bool sayActive = MWBase::Environment::get().getSoundManager()->sayActive(actor);
targetsAreEqual = (greeting && sayActive) || mechMgr->isTurningToPlayer(actor); targetsAreEqual = (greeting && sayActive) || mechMgr->isTurningToPlayer(actor);
} }

View file

@ -119,8 +119,8 @@ namespace MWWorld
throw std::runtime_error("class cannot hit"); throw std::runtime_error("class cannot hit");
} }
void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, void Class::onHit(const Ptr& ptr, const std::map<std::string, float>& damages, const Ptr& object,
const osg::Vec3f& hitPosition, bool successful, const MWMechanics::DamageSourceType sourceType) const const Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const
{ {
throw std::runtime_error("class cannot be hit"); throw std::runtime_error("class cannot be hit");
} }
@ -205,7 +205,7 @@ namespace MWWorld
return std::make_pair(std::vector<int>(), false); return std::make_pair(std::vector<int>(), false);
} }
ESM::RefId Class::getEquipmentSkill(const ConstPtr& ptr) const ESM::RefId Class::getEquipmentSkill(const ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const
{ {
return {}; return {};
} }
@ -235,7 +235,7 @@ namespace MWWorld
return false; return false;
} }
float Class::getArmorRating(const MWWorld::Ptr& ptr) const float Class::getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const
{ {
throw std::runtime_error("Class does not support armor rating"); throw std::runtime_error("Class does not support armor rating");
} }
@ -452,11 +452,6 @@ namespace MWWorld
throw std::runtime_error("class does not support skills"); throw std::runtime_error("class does not support skills");
} }
int Class::getBloodTexture(const MWWorld::ConstPtr& ptr) const
{
throw std::runtime_error("class does not support gore");
}
void Class::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const {} void Class::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const {}
void Class::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const {} void Class::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const {}
@ -514,7 +509,8 @@ namespace MWWorld
return -1; return -1;
} }
float Class::getEffectiveArmorRating(const ConstPtr& armor, const Ptr& actor) const float Class::getSkillAdjustedArmorRating(
const ConstPtr& armor, const Ptr& actor, bool useLuaInterfaceIfAvailable) const
{ {
throw std::runtime_error("class does not support armor ratings"); throw std::runtime_error("class does not support armor ratings");
} }

View file

@ -6,7 +6,6 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <osg/Quat>
#include <osg/Vec4f> #include <osg/Vec4f>
#include "doorstate.hpp" #include "doorstate.hpp"
@ -16,9 +15,13 @@
#include "../mwmechanics/damagesourcetype.hpp" #include "../mwmechanics/damagesourcetype.hpp"
#include <components/esm/refid.hpp> #include <components/esm/refid.hpp>
#include <components/esm3/loadskil.hpp>
#include <components/vfs/pathutil.hpp> #include <components/vfs/pathutil.hpp>
namespace osg
{
class Quat;
}
namespace ESM namespace ESM
{ {
struct ObjectState; struct ObjectState;
@ -144,11 +147,11 @@ namespace MWWorld
/// enums. ignored for creature attacks. /// enums. ignored for creature attacks.
/// (default implementation: throw an exception) /// (default implementation: throw an exception)
virtual void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, virtual void onHit(const MWWorld::Ptr& ptr, const std::map<std::string, float>& damages,
const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, const MWWorld::Ptr& object, const MWWorld::Ptr& attacker, bool successful,
const MWMechanics::DamageSourceType sourceType) const; const MWMechanics::DamageSourceType sourceType) const;
///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is ///< Alerts \a ptr that it's being hit for \a damages by \a object (sword, arrow, etc). \a attacker specifies
/// true (else fatigue) by \a object (sword, arrow, etc). \a attacker specifies the ///< the
/// actor responsible for the attack. \a successful specifies if the hit is /// actor responsible for the attack. \a successful specifies if the hit is
/// successful or not. \a sourceType classifies the damage source. /// successful or not. \a sourceType classifies the damage source.
@ -209,7 +212,7 @@ namespace MWWorld
/// ///
/// Default implementation: return (empty vector, false). /// Default implementation: return (empty vector, false).
virtual ESM::RefId getEquipmentSkill(const ConstPtr& ptr) const; virtual ESM::RefId getEquipmentSkill(const ConstPtr& ptr, bool useLuaInterfaceIfAvailable = false) const;
/// Return the index of the skill this item corresponds to when equipped. /// Return the index of the skill this item corresponds to when equipped.
/// (default implementation: return empty ref id) /// (default implementation: return empty ref id)
@ -255,7 +258,7 @@ namespace MWWorld
virtual ESM::RefId getSoundIdFromSndGen(const Ptr& ptr, std::string_view type) const; virtual ESM::RefId getSoundIdFromSndGen(const Ptr& ptr, std::string_view type) const;
///< Returns the sound ID for \a ptr of the given soundgen \a type. ///< Returns the sound ID for \a ptr of the given soundgen \a type.
virtual float getArmorRating(const MWWorld::Ptr& ptr) const; virtual float getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable = false) const;
///< @return combined armor rating of this actor ///< @return combined armor rating of this actor
virtual const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const; virtual const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const;
@ -310,9 +313,6 @@ namespace MWWorld
virtual bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const { return true; } virtual bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const { return true; }
///< Return whether this class of object can be activated with telekinesis ///< Return whether this class of object can be activated with telekinesis
/// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini)
virtual int getBloodTexture(const MWWorld::ConstPtr& ptr) const;
virtual Ptr copyToCell(const ConstPtr& ptr, CellStore& cell, int count) const; virtual Ptr copyToCell(const ConstPtr& ptr, CellStore& cell, int count) const;
// Similar to `copyToCell`, but preserves RefNum and moves LuaScripts. // Similar to `copyToCell`, but preserves RefNum and moves LuaScripts.
@ -375,7 +375,8 @@ namespace MWWorld
virtual int getPrimaryFactionRank(const MWWorld::ConstPtr& ptr) const; virtual int getPrimaryFactionRank(const MWWorld::ConstPtr& ptr) const;
/// Get the effective armor rating, factoring in the actor's skills, for the given armor. /// Get the effective armor rating, factoring in the actor's skills, for the given armor.
virtual float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const; virtual float getSkillAdjustedArmorRating(
const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor, bool useLuaInterfaceIfAvailable = false) const;
virtual osg::Vec4f getEnchantmentColor(const MWWorld::ConstPtr& item) const; virtual osg::Vec4f getEnchantmentColor(const MWWorld::ConstPtr& item) const;

View file

@ -408,7 +408,7 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_)
{ {
if (actorIsNpc) if (actorIsNpc)
{ {
if (testCls.getEffectiveArmorRating(test, actor) <= unarmoredRating) if (testCls.getSkillAdjustedArmorRating(test, actor) <= unarmoredRating)
continue; continue;
} }
else else
@ -463,8 +463,8 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_)
// For NPCs, compare armor rating; for creatures, compare condition // For NPCs, compare armor rating; for creatures, compare condition
if (actorIsNpc) if (actorIsNpc)
{ {
const float rating = testCls.getEffectiveArmorRating(test, actor); const float rating = testCls.getSkillAdjustedArmorRating(test, actor);
const float oldRating = oldCls.getEffectiveArmorRating(old, actor); const float oldRating = oldCls.getSkillAdjustedArmorRating(old, actor);
if (rating <= oldRating) if (rating <= oldRating)
continue; continue;
} }

View file

@ -105,7 +105,7 @@ namespace MWWorld
void setLocals(const ESM::Script& script); void setLocals(const ESM::Script& script);
MWLua::LocalScripts* getLuaScripts() { return mLuaScripts.get(); } MWLua::LocalScripts* getLuaScripts() const { return mLuaScripts.get(); }
void setLuaScripts(std::shared_ptr<MWLua::LocalScripts>&&); void setLuaScripts(std::shared_ptr<MWLua::LocalScripts>&&);
/// This flag is only used for content stack loading and will not be stored in the savegame. /// This flag is only used for content stack loading and will not be stored in the savegame.

View file

@ -3706,24 +3706,6 @@ namespace MWWorld
} }
} }
void World::spawnBloodEffect(const Ptr& ptr, const osg::Vec3f& worldPosition)
{
if (ptr == getPlayerPtr() && Settings::gui().mHitFader)
return;
std::string_view texture
= Fallback::Map::getString("Blood_Texture_" + std::to_string(ptr.getClass().getBloodTexture(ptr)));
if (texture.empty())
texture = Fallback::Map::getString("Blood_Texture_0");
// [0, 2]
const int number = Misc::Rng::rollDice(3);
const VFS::Path::Normalized model = Misc::ResourceHelpers::correctMeshPath(
VFS::Path::Normalized(Fallback::Map::getString("Blood_Model_" + std::to_string(number))));
mRendering->spawnEffect(model, texture, worldPosition, 1.0f, false, false);
}
void World::spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, void World::spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride,
const osg::Vec3f& worldPos, float scale, bool isMagicVFX, bool useAmbientLight) const osg::Vec3f& worldPos, float scale, bool isMagicVFX, bool useAmbientLight)
{ {

View file

@ -608,9 +608,6 @@ namespace MWWorld
/// Spawn a random creature from a levelled list next to the player /// Spawn a random creature from a levelled list next to the player
void spawnRandomCreature(const ESM::RefId& creatureList) override; void spawnRandomCreature(const ESM::RefId& creatureList) override;
/// Spawn a blood effect for \a ptr at \a worldPosition
void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) override;
void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride,
const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true,
bool useAmbientLight = true) override; bool useAmbientLight = true) override;

View file

@ -25,11 +25,6 @@ namespace
Config::GameSettings::GameSettings(const Files::ConfigurationManager& cfg) Config::GameSettings::GameSettings(const Files::ConfigurationManager& cfg)
: mCfgMgr(cfg) : mCfgMgr(cfg)
{ {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
// this needs calling once so Qt can see its stream operators, which it needs when dragging and dropping
// it's automatic with Qt 6
qRegisterMetaTypeStreamOperators<SettingValue>("Config::SettingValue");
#endif
} }
void Config::GameSettings::validatePaths() void Config::GameSettings::validatePaths()

View file

@ -14,11 +14,7 @@ namespace L10n
// Try to load OpenMW translations from resources folder first. // Try to load OpenMW translations from resources folder first.
// If we loaded them, try to load Qt translations from both // If we loaded them, try to load Qt translations from both
// resources folder and default translations folder as well. // resources folder and default translations folder as well.
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
auto qtPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath); auto qtPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
#else
auto qtPath = QLibraryInfo::location(QLibraryInfo::TranslationsPath);
#endif
auto localPath = resourcesPath + "/translations"; auto localPath = resourcesPath + "/translations";
if (AppTranslator.load(QLocale::system(), appName, "_", localPath) if (AppTranslator.load(QLocale::system(), appName, "_", localPath)

View file

@ -165,6 +165,29 @@ namespace LuaUtil
virtual bool isActive() const { return false; } virtual bool isActive() const { return false; }
protected: protected:
// Call a function on an interface.
template <typename T, typename... Args>
std::optional<T> callInterface(std::string_view interfaceName, std::string_view identifier, const Args&... args)
{
std::optional<T> res = std::nullopt;
mLua.protectedCall([&](LuaUtil::LuaView& view) {
LoadedData& data = ensureLoaded();
auto I = data.mPublicInterfaces.get<sol::optional<sol::table>>(interfaceName);
if (I)
{
auto o = I->get_or<sol::object>(identifier, sol::nil);
if (o.is<sol::function>())
{
sol::object luaRes = o.as<sol::function>().call(args...);
if (luaRes.is<T>())
res = luaRes.as<T>();
}
}
});
return res;
}
struct Handler struct Handler
{ {
int mScriptId; int mScriptId;

View file

@ -3,20 +3,13 @@
#include <QtGlobal> #include <QtGlobal>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <QTextCodec>
#endif
#include <QTextStream> #include <QTextStream>
namespace Misc namespace Misc
{ {
inline void ensureUtf8Encoding(QTextStream& stream) inline void ensureUtf8Encoding(QTextStream& stream)
{ {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
stream.setCodec(QTextCodec::codecForName("UTF-8"));
#else
stream.setEncoding(QStringConverter::Utf8); stream.setEncoding(QStringConverter::Utf8);
#endif
} }
} }
#endif #endif

View file

@ -71,8 +71,6 @@ namespace Settings
SettingValue<osg::Vec3f> mDefaultActorPathfindHalfExtents{ mIndex, "Game", SettingValue<osg::Vec3f> mDefaultActorPathfindHalfExtents{ mIndex, "Game",
"default actor pathfind half extents", makeMaxStrictSanitizerVec3f(osg::Vec3f(0, 0, 0)) }; "default actor pathfind half extents", makeMaxStrictSanitizerVec3f(osg::Vec3f(0, 0, 0)) };
SettingValue<bool> mDayNightSwitches{ mIndex, "Game", "day night switches" }; SettingValue<bool> mDayNightSwitches{ mIndex, "Game", "day night switches" };
SettingValue<bool> mUnarmedCreatureAttacksDamageArmor{ mIndex, "Game",
"unarmed creature attacks damage armor" };
SettingValue<DetourNavigator::CollisionShapeType> mActorCollisionShapeType{ mIndex, "Game", SettingValue<DetourNavigator::CollisionShapeType> mActorCollisionShapeType{ mIndex, "Game",
"actor collision shape type" }; "actor collision shape type" };
SettingValue<bool> mPlayerMovementIgnoresAnimation{ mIndex, "Game", "player movement ignores animation" }; SettingValue<bool> mPlayerMovementIgnoresAnimation{ mIndex, "Game", "player movement ignores animation" };

View file

@ -2,6 +2,7 @@ paths=(
openmw_aux/*lua openmw_aux/*lua
scripts/omw/activationhandlers.lua scripts/omw/activationhandlers.lua
scripts/omw/ai.lua scripts/omw/ai.lua
scripts/omw/combat/local.lua
scripts/omw/input/playercontrols.lua scripts/omw/input/playercontrols.lua
scripts/omw/mechanics/animationcontroller.lua scripts/omw/mechanics/animationcontroller.lua
scripts/omw/input/gamepadcontrols.lua scripts/omw/input/gamepadcontrols.lua

View file

@ -62,6 +62,25 @@ However, it depends on several packages which are not in stable,
so it is not possible to install OpenMW in Wheezy without creating a FrankenDebian. so it is not possible to install OpenMW in Wheezy without creating a FrankenDebian.
This is not recommended or supported. This is not recommended or supported.
Fedora
======
OpenMW is available in the official repository of Fedora for versions 41 and up.
To install simply run the following as root (or in sudo), depending on what packages
you want.
``openmw`` includes the launcher, install wizard, iniimporter and the engine itself.
``openmw-cs`` includes the construction set.
``openmw-tools`` includes ``bsatool``, ``esmtool`` and ``niftest``.
.. code-block:: console
$ dnf install openmw
$ dnf install openmw-cs
$ dnf install openmw-tools
Flatpak Flatpak
======= =======

View file

@ -71,11 +71,70 @@ Calls the corresponding function in openw.core on the target. Will use core.soun
.. code-block:: Lua .. code-block:: Lua
actor:sendEvent('PlaySound3d', {sound = 'Open Lock'}) actor:sendEvent('PlaySound3d', {sound = 'Open Lock'})
**BreakInvisibility** **BreakInvisibility**
Forces the actor to lose all active invisibility effects. Forces the actor to lose all active invisibility effects.
**Unequip**
Any script can send ``Unequip`` events with the argument ``item`` or ``slot`` to any actor, to make that actor unequip an item.
The following two examples are equivalent, except the ``item`` variant is guaranteed to only unequip the specified item and won't unequip a different item if the actor's equipment changed during the same frame:
.. code-block:: Lua
local item = Actor.getEquipment(actor, Actor.EQUIPMENT_SLOT.CarriedLeft)
if item then
actor:sendEvent('Unequip', {item = item})
end
.. code-block:: Lua
actor:sendEvent('Unequip', {slot = Actor.EQUIPMENT_SLOT.CarriedLeft})
Combat events
-------------
**Hit**
Any script can send ``Hit`` events with arguments described in the Combat interface to cause a hit to an actor
Example:
.. code-block:: Lua
-- See Combat#AttackInfo
local attack = {
attacker = self,
weapon = Actor.getEquipment(self, Actor.EQUIPMENT_SLOT.CarriedRight),
sourceType = I.Combat.ATTACK_SOURCE_TYPE.Melee,
strenght = 1,
type = self.ATTACK_TYPE.Chop,
damage = {
health = 20,
fatigue = 10,
},
successful = true,
}
victim:sendEvent('Hit', attack)
Item events
-----------
**ModifyItemCondition**
Any script can send ``ModifyItemCondition`` events to global to adjust the condition of an item.
Example:
.. code-block:: Lua
local item = Actor.getEquipment(actor, Actor.EQUIPMENT_SLOT.CarriedLeft)
if item then
-- Reduce condition by 1
-- Note that actor should be included, if applicable, to allow forcibly unequipping items whose condition is reduced to 0
core:sendGlobalEvent('ModifyItemCondition', {actor = self, item = item, amount: -1})
end
UI events UI events
--------- ---------

View file

@ -0,0 +1,8 @@
Interface Combat
================
.. include:: version.rst
.. raw:: html
:file: generated_html/scripts_omw_combat_local.html

View file

@ -23,6 +23,9 @@
* - :doc:`Crimes </reference/lua-scripting/interface_crimes>` * - :doc:`Crimes </reference/lua-scripting/interface_crimes>`
- |bdg-ctx-global| - |bdg-ctx-global|
- Commit crimes. - Commit crimes.
* - :doc:`Combat </reference/lua-scripting/interface_combat>`
- |bdg-ctx-local|
- Control combat of NPCs and creatures
* - :doc:`GamepadControls </reference/lua-scripting/interface_gamepadcontrols>` * - :doc:`GamepadControls </reference/lua-scripting/interface_gamepadcontrols>`
- |bdg-ctx-player| - |bdg-ctx-player|
- Allows to alter behavior of the built-in script that handles player gamepad controls. - Allows to alter behavior of the built-in script that handles player gamepad controls.

View file

@ -80,7 +80,6 @@ GUI Settings
Enables or disables the red flash overlay when the character takes damage. Enables or disables the red flash overlay when the character takes damage.
Disabling causes the player to "bleed" like NPCs.
.. omw-setting:: .. omw-setting::
:title: werewolf overlay :title: werewolf overlay

View file

@ -442,17 +442,6 @@ Game Settings
Some mods add models which change visuals based on time of day. When this setting is enabled, supporting models will automatically make use of Day/night state. Some mods add models which change visuals based on time of day. When this setting is enabled, supporting models will automatically make use of Day/night state.
.. omw-setting::
:title: unarmed creature attacks damage armor
:type: boolean
:range: true, false
:default: false
:location: :bdg-success:`Launcher > Settings > Gameplay`
If disabled unarmed creature attacks do not reduce armor condition, just as with vanilla engine.
If enabled unarmed creature attacks reduce armor condition, the same as attacks from NPCs and armed creatures.
.. omw-setting:: .. omw-setting::
:title: actor collision shape type :title: actor collision shape type
:type: int :type: int

View file

@ -209,6 +209,7 @@ if (BUILD_BENCHMARKS AND NOT OPENMW_USE_SYSTEM_BENCHMARK)
set(BENCHMARK_ENABLE_TESTING OFF) set(BENCHMARK_ENABLE_TESTING OFF)
set(BENCHMARK_ENABLE_INSTALL OFF) set(BENCHMARK_ENABLE_INSTALL OFF)
set(BENCHMARK_ENABLE_GTEST_TESTS OFF) set(BENCHMARK_ENABLE_GTEST_TESTS OFF)
set(BENCHMARK_ENABLE_WERROR OFF)
include(FetchContent) include(FetchContent)
FetchContent_Declare(benchmark FetchContent_Declare(benchmark

View file

@ -17,11 +17,7 @@ set(OSGQT_SOURCE_FILES
add_library(${OSGQT_LIBRARY} STATIC ${OSGQT_SOURCE_FILES}) add_library(${OSGQT_LIBRARY} STATIC ${OSGQT_SOURCE_FILES})
if (QT_VERSION_MAJOR VERSION_EQUAL 6) target_link_libraries(${OSGQT_LIBRARY} Qt::Core Qt::OpenGL Qt::OpenGLWidgets)
target_link_libraries(${OSGQT_LIBRARY} Qt::Core Qt::OpenGL Qt::OpenGLWidgets)
else()
target_link_libraries(${OSGQT_LIBRARY} Qt::Core Qt::OpenGL)
endif()
link_directories(${CMAKE_CURRENT_BINARY_DIR}) link_directories(${CMAKE_CURRENT_BINARY_DIR})

View file

@ -25,6 +25,9 @@ PLAYER: scripts/omw/input/gamepadcontrols.lua
NPC,CREATURE: scripts/omw/ai.lua NPC,CREATURE: scripts/omw/ai.lua
GLOBAL: scripts/omw/mechanics/globalcontroller.lua GLOBAL: scripts/omw/mechanics/globalcontroller.lua
CREATURE, NPC, PLAYER: scripts/omw/mechanics/actorcontroller.lua CREATURE, NPC, PLAYER: scripts/omw/mechanics/actorcontroller.lua
GLOBAL: scripts/omw/combat/global.lua
MENU: scripts/omw/combat/menu.lua
NPC,CREATURE,PLAYER: scripts/omw/combat/local.lua
# User interface # User interface
PLAYER: scripts/omw/ui.lua PLAYER: scripts/omw/ui.lua

View file

@ -0,0 +1,16 @@
Combat: "OpenMW Combat"
combatSettingsPageDescription: "OpenMW Combat settings"
combatSettings: "Combat"
unarmedCreatureAttacksDamageArmor: "Unarmed creature attacks damage armor"
unarmedCreatureAttacksDamageArmorDescription: |
Unarmed creatures now also damage armor.
redistributeShieldHitsWhenNotWearingShield: "Redistribute shield hits when not wearing a shield"
redistributeShieldHitsWhenNotWearingShieldDescription: |
Equivalent to "Shield hit location fix" from Morrowind Code Patch. Redistributes hits to the shield armor slot to left pauldron or cuirass when not wearing a shield.
spawnBloodEffectsOnPlayer: "Spawn blood effects on player"
spawnBloodEffectsOnPlayerDescription: |
If enabled blood effects are spawned on the player when hit in combat, same as any other character.

View file

@ -0,0 +1,16 @@
Combat: "Боевая система OpenMW"
combatSettingsPageDescription: "Настройки боевой системы OpenMW"
combatSettings: "Бой"
unarmedCreatureAttacksDamageArmor: "Атаки существ повреждают броню"
unarmedCreatureAttacksDamageArmorDescription: |
Атаки невооруженных существ тоже будут наносить броне урон.
redistributeShieldHitsWhenNotWearingShield: "Перераспространять удары по щитам"
redistributeShieldHitsWhenNotWearingShieldDescription: |
Эквивалентно настройке "Shield hit location fix" из Morrowind Code Patch. Когда персонаж не носит щит, удары по его слоту будут проходить по слоту левого наплечника или кирасы.
spawnBloodEffectsOnPlayer: "Использовать эффекты крови для игрока"
spawnBloodEffectsOnPlayerDescription: |
Если настройка включена, эффекты крови будут играть при получении ударов в бою персонажем игрока, как для всех остальных персонажей.

View file

@ -0,0 +1,16 @@
Combat: "OpenMW Strid"
combatSettingsPageDescription: "OpenMW Stridsinställningar"
combatSettings: "Strid"
unarmedCreatureAttacksDamageArmor: "Obeväpnad attack från varelser skadar rustning"
unarmedCreatureAttacksDamageArmorDescription: |
Obeväpnade varelser skadar nu även rustning.
redistributeShieldHitsWhenNotWearingShield: "Omfördela sköldträffar när sköld inte är buren"
redistributeShieldHitsWhenNotWearingShieldDescription: |
Motsvarar ”Shield hit location fix” från Morrowind Code Patch. Omfördelar träffar på sköldens rustningsplats till vänster axelstycke (pauldron) eller kyrass (cuirass) när du inte bär en sköld.
spawnBloodEffectsOnPlayer: "Skapa blodeffekter på spelarfiguren"
spawnBloodEffectsOnPlayerDescription: |
Om blodeffekter är aktiverade visas de på spelarfiguren när denne träffas i strid, precis som på alla andra rollfigurer.

View file

@ -0,0 +1,41 @@
local async = require('openmw.async')
local storage = require('openmw.storage')
local I = require('openmw.interfaces')
local combatGroup = 'SettingsOMWCombat'
return {
registerSettingsPage = function()
I.Settings.registerPage({
key = 'OMWCombat',
l10n = 'OMWCombat',
name = 'Combat',
description = 'combatSettingsPageDescription',
})
end,
registerSettingsGroup = function()
local function boolSetting(key, default)
return {
key = key,
renderer = 'checkbox',
name = key,
description = key..'Description',
default = default,
}
end
I.Settings.registerGroup({
key = combatGroup,
page = 'OMWCombat',
l10n = 'OMWCombat',
name = 'combatSettings',
permanentStorage = false,
order = 0,
settings = {
boolSetting('unarmedCreatureAttacksDamageArmor', false),
boolSetting('redistributeShieldHitsWhenNotWearingShield', false),
boolSetting('spawnBloodEffectsOnPlayer', false),
},
})
end,
}

View file

@ -0,0 +1 @@
require('scripts.omw.combat.common').registerSettingsGroup()

View file

@ -0,0 +1,431 @@
local animation = require('openmw.animation')
local async = require('openmw.async')
local core = require('openmw.core')
local I = require('openmw.interfaces')
local self = require('openmw.self')
local storage = require('openmw.storage')
local types = require('openmw.types')
local util = require('openmw.util')
local Actor = types.Actor
local Weapon = types.Weapon
local Player = types.Player
local Creature = types.Creature
local Armor = types.Armor
local isPlayer = Player.objectIsInstance(self)
local godMode = function() return false end
if isPlayer then
-- openmw.debug is only allowed on player scripts
godMode = function() return require('openmw.debug').isGodMode() end
end
local onHitHandlers = {}
local settings = storage.globalSection('SettingsOMWCombat')
local function getSkill(actor, skillId)
if Creature.objectIsInstance(actor) then
local specialization = core.stats.Skill.record(skillId).specialization
local creatureRecord = Creature.record(actor)
return creatureRecord[specialization..'Skill']
else
return types.NPC.stats.skills[skillId](actor).modified
end
end
local armorTypeGmst = {
[Armor.TYPE.Boots] = core.getGMST('iBootsWeight'),
[Armor.TYPE.Cuirass] = core.getGMST('iCuirassWeight'),
[Armor.TYPE.Greaves] = core.getGMST('iGreavesWeight'),
[Armor.TYPE.Helmet] = core.getGMST('iHelmWeight'),
[Armor.TYPE.LBracer] = core.getGMST('iGauntletWeight'),
[Armor.TYPE.LGauntlet] = core.getGMST('iGauntletWeight'),
[Armor.TYPE.LPauldron] = core.getGMST('iPauldronWeight'),
[Armor.TYPE.RBracer] = core.getGMST('iGauntletWeight'),
[Armor.TYPE.RGauntlet] = core.getGMST('iGauntletWeight'),
[Armor.TYPE.RPauldron] = core.getGMST('iPauldronWeight'),
[Armor.TYPE.Shield] = core.getGMST('iShieldWeight'),
}
local armorSlots = {
Actor.EQUIPMENT_SLOT.Boots,
Actor.EQUIPMENT_SLOT.Cuirass,
Actor.EQUIPMENT_SLOT.Greaves,
Actor.EQUIPMENT_SLOT.Helmet,
Actor.EQUIPMENT_SLOT.LeftGauntlet,
Actor.EQUIPMENT_SLOT.LeftPauldron,
Actor.EQUIPMENT_SLOT.RightGauntlet,
Actor.EQUIPMENT_SLOT.RightPauldron,
Actor.EQUIPMENT_SLOT.CarriedLeft,
}
local function getArmorSkill(item)
if not item or not Armor.objectIsInstance(item) then
return 'unarmored'
end
local record = Armor.record(item)
local weightGmst = armorTypeGmst[record.type]
local epsilon = 0.0005
if record.weight <= weightGmst * core.getGMST('fLightMaxMod') + epsilon then
return 'lightarmor'
elseif record.weight <= weightGmst * core.getGMST('fMedMaxMod') + epsilon then
return 'mediumarmor'
else
return 'heavyarmor'
end
end
local function getSkillAdjustedArmorRating(item, actor)
local record = Armor.record(item)
local skillid = I.Combat.getArmorSkill(item)
local skill = getSkill(actor, skillid)
if record.weight == 0 then
return record.baseArmor
end
return record.baseArmor * skill / core.getGMST('iBaseArmorSkill')
end
local function getEffectiveArmorRating(item, actor)
local record = Armor.record(item)
local rating = getSkillAdjustedArmorRating(item, actor)
if record.health and record.health ~= 0 then
rating = rating * (types.Item.itemData(item).condition / record.health)
end
return rating
end
local function getArmorRating(actor)
local magicShield = Actor.activeEffects(actor):getEffect(core.magic.EFFECT_TYPE.Shield).magnitude
if Creature.objectIsInstance(actor) then
return magicShield
end
local equipment = Actor.getEquipment(actor)
local ratings = {}
local unarmored = getSkill(actor, 'unarmored')
local fUnarmoredBase1 = core.getGMST('fUnarmoredBase1')
local fUnarmoredBase2 = core.getGMST('fUnarmoredBase2')
for _, v in pairs(armorSlots) do
if equipment[v] and Armor.objectIsInstance(equipment[v]) then
ratings[v] = I.Combat.getEffectiveArmorRating(equipment[v], actor)
else
-- Unarmored
ratings[v] = (fUnarmoredBase1 * unarmored) * (fUnarmoredBase2 * unarmored)
end
end
return ratings[Actor.EQUIPMENT_SLOT.Cuirass] * 0.3
+ ratings[Actor.EQUIPMENT_SLOT.CarriedLeft] * 0.1
+ ratings[Actor.EQUIPMENT_SLOT.Helmet] * 0.1
+ ratings[Actor.EQUIPMENT_SLOT.Greaves] * 0.1
+ ratings[Actor.EQUIPMENT_SLOT.Boots] * 0.1
+ ratings[Actor.EQUIPMENT_SLOT.LeftPauldron] * 0.1
+ ratings[Actor.EQUIPMENT_SLOT.RightPauldron] * 0.1
+ ratings[Actor.EQUIPMENT_SLOT.LeftGauntlet] * 0.05
+ ratings[Actor.EQUIPMENT_SLOT.RightGauntlet] * 0.05
+ magicShield
end
local function adjustDamageForArmor(damage, actor)
local armor = I.Combat.getArmorRating(actor)
local x = damage / (damage + armor)
return damage * math.max(x, core.getGMST('fCombatArmorMinMult'))
end
local function pickRandomArmor(actor)
local slot = nil
local roll = math.random(0, 99) -- randIntUniform(0, 100)
if roll >= 90 then
slot = Actor.EQUIPMENT_SLOT.CarriedLeft
local item = Actor.getEquipment(actor, slot)
local haveShield = item and Armor.objectIsInstance(item)
if settings:get('redistributeShieldHitsWhenNotWearingShield') and not haveShield then
if roll >= 95 then
slot = Actor.EQUIPMENT_SLOT.Cuirass
else
slot = Actor.EQUIPMENT_SLOT.LeftPauldron
end
end
elseif roll >= 85 then
slot = Actor.EQUIPMENT_SLOT.RightGauntlet
elseif roll >= 80 then
slot = Actor.EQUIPMENT_SLOT.LeftGauntlet
elseif roll >= 70 then
slot = Actor.EQUIPMENT_SLOT.RightPauldron
elseif roll >= 60 then
slot = Actor.EQUIPMENT_SLOT.LeftPauldron
elseif roll >= 50 then
slot = Actor.EQUIPMENT_SLOT.Boots
elseif roll >= 40 then
slot = Actor.EQUIPMENT_SLOT.Greaves
elseif roll >= 30 then
slot = Actor.EQUIPMENT_SLOT.Helmet
else
slot = Actor.EQUIPMENT_SLOT.Cuirass
end
return Actor.getEquipment(actor, slot)
end
local function getDamage(attack, what)
if attack.damage then
return attack.damage[what] or 0
end
end
local function setDamage(attack, what, damage)
attack.damage = attack.damage or {}
attack.damage[what] = damage
end
local function applyArmor(attack)
local healthDamage = getDamage(attack, 'health')
if healthDamage > 0 then
local healthDamageAdjusted = I.Combat.adjustDamageForArmor(healthDamage)
local diff = math.floor(healthDamageAdjusted - healthDamage)
setDamage(attack, 'health', math.max(healthDamageAdjusted, 1))
local item = I.Combat.pickRandomArmor()
local skillid = I.Combat.getArmorSkill(item)
if I.SkillProgression then
I.SkillProgression.skillUsed(skillid, {useType = I.SkillProgression.SKILL_USE_TYPES.Armor_HitByOpponent})
end
if item and Armor.objectIsInstance(item) then
local attackerIsUnarmedCreature = attack.attacker and not attack.weapon and Creature.objectIsInstance(attack.attacker)
if settings:get('unarmedCreatureAttacksDamageArmor') or not attackerIsUnarmedCreature then
core.sendGlobalEvent('ModifyItemCondition', { actor = self, item = item, amount = diff })
end
if skillid == 'lightarmor' then
core.sound.playSound3d('Light Armor Hit', self)
elseif skillid == 'mediumarmor' then
core.sound.playSound3d('Medium Armor Hit', self)
elseif skillid == 'heavyarmor' then
core.sound.playSound3d('Heavy Armor Hit', self)
else
core.sound.playSound3d('Hand To Hand Hit', self)
end
end
end
end
local function adjustDamageForDifficulty(attack, defendant)
local attackerIsPlayer = attack.attacker and Player.objectIsInstance(attack.attacker)
-- The interface guarantees defendant is never nil
local defendantIsPlayer = Player.objectIsInstance(defendant)
-- If both characters are NPCs or both characters are players then
-- difficulty settings do not apply
if attackerIsPlayer == defendantIsPlayer then return end
local fDifficultyMult = core.getGMST('fDifficultyMult')
local difficultyTerm = core.getGameDifficulty() * 0.01
local x = 0
if defendantIsPlayer then
-- Defending actor is a player
if difficultyTerm > 0 then
x = difficultyTerm * fDifficultyMult
else
x = difficultyTerm / fDifficultyMult
end
elseif attackerIsPlayer then
-- Attacking actor is a player
if difficultyTerm > 0 then
x = -difficultyTerm / fDifficultyMult
else
x = -difficultyTerm * fDifficultyMult
end
end
setDamage(attack, 'health', getDamage(attack, 'health') * (1 + x))
end
local function spawnBloodEffect(position)
if isPlayer and not settings:get('spawnBloodEffectsOnPlayer') then
return
end
local bloodEffectModel = string.format('Blood_Model_%d', math.random(0, 2)) -- randIntUniformClosed(0, 2)
-- TODO: implement a Misc::correctMeshPath equivalent instead?
-- All it ever does it append 'meshes\\' though
bloodEffectModel = 'meshes/'..core.getGMST(bloodEffectModel)
local record = self.object.type.record(self.object)
local bloodTexture = string.format('Blood_Texture_%d', record.bloodType)
bloodTexture = core.getGMST(bloodTexture)
if not bloodTexture or bloodTexture == '' then
bloodTexture = core.getGMST('Blood_Texture_0')
end
core.sendGlobalEvent('SpawnVfx', {
model = bloodEffectModel,
position = position,
options = {
mwMagicVfx = false,
particleTextureOverride = bloodTexture,
useAmbientLight = false,
},
})
end
local function onHit(data)
for i = #onHitHandlers, 1, -1 do
if onHitHandlers[i](data) == false then
return -- skip other handlers
end
end
if data.successful and not godMode() then
I.Combat.applyArmor(data)
I.Combat.adjustDamageForDifficulty(data)
if getDamage(data, 'health') > 0 then
core.sound.playSound3d('Health Damage', self)
if data.hitPos then
spawnBloodEffect(data.hitPos)
end
end
elseif data.attacker and Player.objectIsInstance(data.attacker) then
core.sound.playSound3d('miss', self)
end
Actor._onHit(self, data)
end
---
-- Table of possible attack source types
-- @type AttackSourceType
-- @field #string Magic
-- @field #string Melee
-- @field #string Ranged
-- @field #string Unspecified
---
-- @type AttackInfo
-- @field [parent=#AttackInfo] #table damage A table mapping stat name (health, fatigue, or magicka) to number. For example, {health = 50, fatigue = 10} will cause 50 damage to health and 10 to fatigue (before adjusting for armor and difficulty). This field is ignored for failed attacks.
-- @field [parent=#AttackInfo] #number strength A number between 0 and 1 representing the attack strength. This field is ignored for failed attacks.
-- @field [parent=#AttackInfo] #boolean successful Whether the attack was successful or not.
-- @field [parent=#AttackInfo] #AttackSourceType sourceType What class of attack this is.
-- @field [parent=#AttackInfo] openmw.self#ATTACK_TYPE type (Optional) Attack variant if applicable. For melee attacks this represents chop vs thrust vs slash. For unarmed creatures this implies which of its 3 possible attacks were used. For other attacks this field can be ignored.
-- @field [parent=#AttackInfo] openmw.types#Actor attacker (Optional) Attacking actor
-- @field [parent=#AttackInfo] openmw.types#Weapon weapon (Optional) Attacking weapon
-- @field [parent=#AttackInfo] openmw.types#Weapon ammo (Optional) Ammo
-- @field [parent=#AttackInfo] openmw.util#Vector3 hitPos (Optional) Where on the victim the attack is landing. Used to spawn blood effects. Blood effects are skipped if nil.
return {
--- Basic combat interface
-- @module Combat
-- @usage require('openmw.interfaces').Combat
--
--I.Combat.addOnHitHandler(function(attack)
-- -- Adds fatigue loss when hit by draining fatigue when taking health damage
-- if attack.damage.health and not attack.damage.fatigue then
-- local strengthFactor = Actor.stats.attributes.strength(self).modified / 100 * 0.66
-- local enduranceFactor = Actor.stats.attributes.endurance(self).modified / 100 * 0.34
-- local factor = 1 - math.min(strengthFactor + enduranceFactor, 1)
-- if factor > 0 then
-- attack.damage.fatigue = attack.damage.health * factor
-- end
-- end
--end)
interfaceName = 'Combat',
interface = {
--- Interface version
-- @field [parent=#Combat] #number version
version = 0,
--- Add new onHit handler for this actor
-- If `handler(attack)` returns false, other handlers for
-- the call will be skipped. where attack is the same @{#AttackInfo} passed to #Combat.onHit
-- @function [parent=#Combat] addOnHitHandler
-- @param #function handler The handler.
addOnHitHandler = function(handler)
onHitHandlers[#onHitHandlers + 1] = handler
end,
--- Calculates the character's armor rating and adjusts damage accordingly.
-- Note that this function only adjusts the number, use #Combat.applyArmor
-- to include other side effects.
-- @function [parent=#Combat] adjustDamageForArmor
-- @param #number Damage The numeric damage to adjust
-- @param openmw.core#GameObject actor (Optional) The actor to calculate the armor rating for. Defaults to self.
-- @return #number Damage adjusted for armor
adjustDamageForArmor = function(damage, actor) return adjustDamageForArmor(damage, actor or self) end,
--- Calculates a difficulty multiplier based on current difficulty settings
-- and adjusts damage accordingly. Has no effect if both this actor and the
-- attacker are NPCs, or if both are Players.
-- @function [parent=#Combat] adjustDamageForDifficulty
-- @param #Attack attack The attack to adjust
-- @param openmw.core#GameObject defendant (Optional) The defendant to make the difficulty adjustment for. Defaults to self.
adjustDamageForDifficulty = function(attack, defendant) return adjustDamageForDifficulty(attack, defendant or self) end,
--- Applies this character's armor to the attack. Adjusts damage, reduces item
-- condition accordingly, progresses armor skill, and plays the armor appropriate
-- hit sound.
-- @function [parent=#Combat] applyArmor
-- @param #Attack attack
applyArmor = applyArmor,
--- Computes this character's armor rating.
-- Note that this interface function is read by the engine to update the UI.
-- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything.
-- @function [parent=#Combat] getArmorRating
-- @param openmw.core#GameObject actor (Optional) The actor to calculate the armor rating for. Defaults to self.
-- @return #number
getArmorRating = function(actor) return getArmorRating(actor or self) end,
--- Computes this character's armor rating.
-- You can override this to return any skill you wish (including non-armor skills, if you so wish).
-- Note that this interface function is read by the engine to update the UI.
-- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything.
-- @function [parent=#Combat] getArmorSkill
-- @param openmw.core#GameObject item The item
-- @return #string The armor skill identifier, or unarmored if the item was nil or not an instace of @{openmw.types#Armor}
getArmorSkill = getArmorSkill,
--- Computes the armor rating of a single piece of @{openmw.types#Armor}, adjusted for skill
-- Note that this interface function is read by the engine to update the UI.
-- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything.
-- @function [parent=#Combat] getSkillAdjustedArmorRating
-- @param openmw.core#GameObject item The item
-- @param openmw.core#GameObject actor (Optional) The actor, defaults to self
-- @return #number
getSkillAdjustedArmorRating = function(item, actor) return getSkillAdjustedArmorRating(item, actor or self) end,
--- Computes the effective armor rating of a single piece of @{openmw.types#Armor}, adjusted for skill and item condition
-- @function [parent=#Combat] getEffectiveArmorRating
-- @param openmw.core#GameObject item The item
-- @param openmw.core#GameObject actor (Optional) The actor, defaults to self
-- @return #number
getEffectiveArmorRating = function(item, actor) return getEffectiveArmorRating(item, actor or self) end,
--- Spawns a random blood effect at the given position
-- @function [parent=#Combat] spawnBloodEffect
-- @param openmw.util#Vector3 position
spawnBloodEffect = spawnBloodEffect,
--- Hit this actor. Normally called as Hit event from the attacking actor, with the same parameters.
-- @function [parent=#Combat] onHit
-- @param #AttackInfo attackInfo
onHit = onHit,
--- Picks a random armor slot and returns the item equipped in that slot.
-- Used to pick which armor to damage / skill to increase when hit during combat.
-- @function [parent=#Combat] pickRandomArmor
-- @param openmw.core#GameObject actor (Optional) The actor to pick armor from, defaults to self
-- @return openmw.core#GameObject The armor equipped in the chosen slot. nil if nothing was equipped in that slot.
pickRandomArmor = function(actor) return pickRandomArmor(actor or self) end,
--- @{#AttackSourceType}
-- @field [parent=#Combat] #AttackSourceType ATTACK_SOURCE_TYPES Available attack source types
ATTACK_SOURCE_TYPES = {
Magic = 'magic',
Melee = 'melee',
Ranged = 'ranged',
Unspecified = 'unspecified',
},
},
eventHandlers = {
Hit = function(data) I.Combat.onHit(data) end,
},
}

View file

@ -0,0 +1 @@
require('scripts.omw.combat.common').registerSettingsPage()

View file

@ -19,5 +19,18 @@ return {
BreakInvisibility = function(data) BreakInvisibility = function(data)
Actor.activeEffects(self):remove(core.magic.EFFECT_TYPE.Invisibility) Actor.activeEffects(self):remove(core.magic.EFFECT_TYPE.Invisibility)
end, end,
Unequip = function(data)
local equipment = Actor.getEquipment(self)
if data.item then
for slot, item in pairs(equipment) do
if item == data.item then
equipment[slot] = nil
end
end
elseif data.slot then
equipment[slot] = nil
end
Actor.setEquipment(self, equipment)
end,
}, },
} }

View file

@ -22,6 +22,16 @@ local function onPlaySound3d(data)
end end
end end
local function onModifyItemCondition(data)
local itemData = Item.itemData(data.item)
itemData.condition = math.min(data.item.type.record(data.item).health, math.max(0, itemData.condition + data.amount))
-- Force unequip broken items
if data.actor and itemData.condition <= 0 then
data.actor:sendEvent('Unequip', {item = data.item})
end
end
return { return {
eventHandlers = { eventHandlers = {
SpawnVfx = function(data) SpawnVfx = function(data)
@ -35,5 +45,6 @@ return {
Unlock = function(data) Unlock = function(data)
Lockable.unlock(data.target) Lockable.unlock(data.target)
end, end,
ModifyItemCondition = onModifyItemCondition,
}, },
} }

View file

@ -690,10 +690,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor&apos;s script instantly.&lt;/p&gt;&lt;p&gt;If this setting is false, player has to wait until end of death animation in all cases. Makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source> <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor&apos;s script instantly.&lt;/p&gt;&lt;p&gt;If this setting is false, player has to wait until end of death animation in all cases. Makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Off</source> <source>Off</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -1155,10 +1151,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Classic Reflected Absorb Spells Behavior</source> <source>Classic Reflected Absorb Spells Behavior</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Unarmed Creature Attacks Damage Armor</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Affect Werewolves</source> <source>Affect Werewolves</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>

View file

@ -798,14 +798,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Classic Reflected Absorb Spells Behavior</source> <source>Classic Reflected Absorb Spells Behavior</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation></translation>
</message>
<message>
<source>Unarmed Creature Attacks Damage Armor</source>
<translation></translation>
</message>
<message> <message>
<source>Factor Strength into Hand-to-Hand Combat</source> <source>Factor Strength into Hand-to-Hand Combat</source>
<translation></translation> <translation></translation>

View file

@ -690,10 +690,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor&apos;s script instantly.&lt;/p&gt;&lt;p&gt;If this setting is false, player has to wait until end of death animation in all cases. Makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source> <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor&apos;s script instantly.&lt;/p&gt;&lt;p&gt;If this setting is false, player has to wait until end of death animation in all cases. Makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Lorsque cette option est activée, le joueur est autorisé à piller créatures et PNJ (p. ex. les créatures invoquées) durant leur animation de mort, si elles ne sont pas en combat. Dans ce cas, le jeu incrémente le conteur de mort et lance son script instantanément.&lt;/p&gt;&lt;p&gt;Lorsque cette option est désactivée, le joueur doit attendre la fin de l&apos;animation de mort. Dans ce cas, l&apos;utilisation de l&apos;exploit des créatures invoquées (piller des créatures invoquées telles que des Drémoras ou des Saintes Dorées afin d&apos;obtenir des armes de grandes valeurs) est rendu beaucoup plus ardu. Cette option entre en confit avec les Mods de mannequin. En effet, ceux-ci utilisent SkipAnim afin d&apos;éviter la fin de l&apos;animation de mort.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation> <translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Lorsque cette option est activée, le joueur est autorisé à piller créatures et PNJ (p. ex. les créatures invoquées) durant leur animation de mort, si elles ne sont pas en combat. Dans ce cas, le jeu incrémente le conteur de mort et lance son script instantanément.&lt;/p&gt;&lt;p&gt;Lorsque cette option est désactivée, le joueur doit attendre la fin de l&apos;animation de mort. Dans ce cas, l&apos;utilisation de l&apos;exploit des créatures invoquées (piller des créatures invoquées telles que des Drémoras ou des Saintes Dorées afin d&apos;obtenir des armes de grandes valeurs) est rendu beaucoup plus ardu. Cette option entre en confit avec les Mods de mannequin. En effet, ceux-ci utilisent SkipAnim afin d&apos;éviter la fin de l&apos;animation de mort.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message> </message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;L&apos;option donne aux créatures non armées la capacité d&apos;endommager les pièces d&apos;armure, comme le font les PNJ et les créatures armées.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message> <message>
<source>Off</source> <source>Off</source>
<translation>Inactif</translation> <translation>Inactif</translation>
@ -1158,10 +1154,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Classic Reflected Absorb Spells Behavior</source> <source>Classic Reflected Absorb Spells Behavior</source>
<translation>Comportement traditionnel de la réflexion des sorts d&apos;absorbtion</translation> <translation>Comportement traditionnel de la réflexion des sorts d&apos;absorbtion</translation>
</message> </message>
<message>
<source>Unarmed Creature Attacks Damage Armor</source>
<translation>L&apos;attaque des créatures non armées endomage les armures</translation>
</message>
<message> <message>
<source>Affect Werewolves</source> <source>Affect Werewolves</source>
<translation>S&apos;applique aux loups garoux</translation> <translation>S&apos;applique aux loups garoux</translation>

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