Merge branch 'master' into screenshot360

pull/456/head
Miloslav Číž 7 years ago committed by GitHub
commit db6107f12f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

1
.gitignore vendored

@ -81,6 +81,5 @@ moc_*.cxx
*ui_playpage.h *ui_playpage.h
*.[ao] *.[ao]
*.so *.so
gamecontrollerdb.txt
openmw.appdata.xml openmw.appdata.xml
venv/ venv/

@ -24,7 +24,7 @@ addons:
- llvm-toolchain-precise-3.6 - llvm-toolchain-precise-3.6
packages: [ packages: [
# Dev # Dev
clang-3.6, libunshield-dev, libtinyxml-dev, cmake, clang-3.6, libunshield-dev, libtinyxml-dev,
# Tests # Tests
libgtest-dev, google-mock, libgtest-dev, google-mock,
# Boost # Boost

@ -129,6 +129,7 @@ Programmers
Radu-Marius Popovici (rpopovici) Radu-Marius Popovici (rpopovici)
Rafael Moura (dhustkoder) Rafael Moura (dhustkoder)
rdimesio rdimesio
rexelion
riothamus riothamus
Rob Cutmore (rcutmore) Rob Cutmore (rcutmore)
Robert MacGregor (Ragora) Robert MacGregor (Ragora)
@ -152,6 +153,7 @@ Programmers
Sylvain Thesnieres (Garvek) Sylvain Thesnieres (Garvek)
t6 t6
terrorfisch terrorfisch
thegriglat
Thomas Luppi (Digmaster) Thomas Luppi (Digmaster)
Will Herrmann (Thunderforge) Will Herrmann (Thunderforge)
Tom Mason (wheybags) Tom Mason (wheybags)

@ -1,3 +1,140 @@
0.43.0
------
Bug #815: Different settings cause inconsistent underwater visibility
Bug #1452: autosave is not executed when waiting
Bug #1555: Closing containers with spacebar doesn't work after touching an item
Bug #1692: Can't close container when item is "held"
Bug #2405: Maximum distance for guards attacking hostile creatures is incorrect
Bug #2445: Spellcasting can be interrupted
Bug #2489: Keeping map open not persisted between saves
Bug #2594: 1st person view uses wrong body texture with Better bodies
Bug #2628: enablestatreviewmenu command doen't read race, class and sign values from current game
Bug #2639: Attacking flag isn't reset upon reloading
Bug #2698: Snow and rain VFX move with the player
Bug #2704: Some creature swim animations not being used
Bug #2789: Potential risk of misunderstanding using the colored "owned" crosshair feature
Bug #3045: Settings containing '#' cannot be loaded
Bug #3097: Drop() doesn't work when an item is held (with the mouse)
Bug #3110: GetDetected doesn't work without a reference
Bug #3126: Framerate nosedives when adjusting dialogue window size
Bug #3243: Ampersand in configuration files isn't escaped automatically
Bug #3365: Wrong water reflection along banks
Bug #3441: Golden saint always dispelling soul trap / spell priority issue
Bug #3528: Disposing of corpses breaks quests
Bug #3531: No FPS limit when playing bink videos even though "framerate limit" is set in settings.cfg
Bug #3647: Multi-effect spells play audio louder than in Vanilla
Bug #3656: NPCs forget where their place in the world is
Bug #3665: Music transitions are too abrupt
Bug #3679: Spell cast effect should disappear after using rest command
Bug #3684: Merchants do not restock empty soul gems if they acquire filled ones.
Bug #3694: Wrong magicka bonus applied on character creation
Bug #3706: Guards don't try to arrest the player if attacked
Bug #3709: Editor: Camera is not positioned correctly on mode switches related to orbital mode
Bug #3720: Death counter not cleaned of non-existing IDs when loading a game
Bug #3744: "Greater/lesser or equal" operators are not parsed when their signs are swapped
Bug #3749: Yagrum Bagarn moves to different position on encountering
Bug #3766: DisableLevitation does not remove visuals of preexisting effect
Bug #3787: Script commands in result box for voiced dialogue are ignored
Bug #3793: OpenMW tries to animate animated references even when they are disabled
Bug #3794: Default sound buffer size is too small for mods
Bug #3796: Mod 'Undress for me' doesn't work: NPCs re-equip everything
Bug #3798: tgm command behaviour differs from vanilla
Bug #3804: [Mod] Animated Morrowind: some animations do not loop correctly
Bug #3805: Slight enchant miscalculation
Bug #3826: Rendering problems with an image in a letter
Bug #3833: [Mod] Windows Glow: windows textures are much darker than in original game
Bug #3835: Bodyparts with multiple NiTriShapes are not handled correctly
Bug #3839: InventoryStore::purgeEffect() removes only first effect with argument ID
Bug #3843: Wrong jumping fatigue loss calculations
Bug #3850: Boethiah's voice is distorted underwater
Bug #3851: NPCs and player say things while underwater
Bug #3864: Crash when exiting to Khartag point from Ilunibi
Bug #3878: Swapping soul gems while enchanting allows constant effect enchantments using any soul gem
Bug #3879: Dialogue option: Go to jail, persists beyond quickload
Bug #3891: Journal displays empty entries
Bug #3892: Empty space before dialogue entry display
Bug #3898: (mod) PositionCell in dialogue results closes dialogue window
Bug #3906: "Could not find Data Files location" dialog can appear multiple times
Bug #3908: [Wizard] User gets stuck if they cancel out of installing from a CD
Bug #3909: Morrowind Content Language dropdown is the only element on the right half of the Settings window
Bug #3910: Launcher window can be resized so that it cuts off the scroll
Bug #3915: NC text key on nifs doesn't work
Bug #3919: Closing inventory while cursor hovers over spell (or other magic menu item) produces left click sound
Bug #3922: Combat AI should avoid enemy hits when casts Self-ranged spells
Bug #3934: [macOS] Copy/Paste from system clipboard uses Control key instead of Command key
Bug #3935: Incorrect attack strength for AI actors
Bug #3937: Combat AI: enchanted weapons have too high rating
Bug #3942: UI sounds are distorted underwater
Bug #3943: CPU/GPU usage should stop when the game is minimised
Bug #3944: Attempting to sell stolen items back to their owner does not remove them from your inventory
Bug #3955: Player's avatar rendering issues
Bug #3956: EditEffectDialog: Cancel button does not update a Range button and an Area slider properly
Bug #3957: Weird bodypart rendering if a node has reserved name
Bug #3960: Clothes with high cost (> 32768) are not handled properly
Bug #3963: When on edge of being burdened the condition doesn't lower as you run.
Bug #3971: Editor: Incorrect colour field in cell table
Bug #3974: Journal page turning doesn't produce sounds
Bug #3978: Instant opening and closing happens when using a Controller with Menus/Containers
Bug #3981: Lagging when spells are cast, especially noticeable on new landmasses such as Tamriel Rebuilt
Bug #3982: Down sounds instead of Up ones are played when trading
Bug #3987: NPCs attack after some taunting with no "Goodbye"
Bug #3991: Journal can still be opened at main menu
Bug #3995: Dispel cancels every temporary magic effect
Bug #4002: Build broken on OpenBSD with clang
Bug #4003: Reduce Render Area of Inventory Doll to Fit Within Border
Bug #4004: Manis Virmaulese attacks without saying anything
Bug #4010: AiWander: "return to the spawn position" feature does not work properly
Bug #4016: Closing menus with spacebar will still send certain assigned actions through afterwards
Bug #4017: GetPCRunning and GetPCSneaking should check that the PC is actually moving
Bug #4024: Poor music track distribution
Bug #4025: Custom spell with copy-pasted name always sorts to top of spell list
Bug #4027: Editor: OpenMW-CS misreports its own name as "OpenCS", under Mac OS
Bug #4033: Archers don't attack if the arrows have run out and there is no other weapon
Bug #4037: Editor: New greetings do not work in-game.
Bug #4049: Reloading a saved game while falling prevents damage
Bug #4056: Draw animation should not be played when player equips a new weapon
Bug #4074: Editor: Merging of LAND/LTEX records
Bug #4076: Disposition bar is not updated when "goodbye" selected in dialogue
Bug #4079: Alchemy skill increases do not take effect until next batch
Bug #4093: GetResistFire, getResistFrost and getResistShock doesn't work as in vanilla
Bug #4094: Level-up messages for levels past 20 are hardcoded not to be used
Bug #4095: Error in framelistener when take all items from a dead corpse
Bug #4096: Messagebox with the "%0.f" format should use 0 digit precision
Bug #4104: Cycling through weapons does not skip broken ones
Bug #4105: birthsign generation menu does not show full details
Bug #4107: Editor: Left pane in Preferences window is too narrow
Bug #4112: Inventory sort order is inconsistent
Bug #4113: 'Resolution not supported in fullscreen' message is inconvenient
Bug #4131: Pickpocketing behaviour is different from vanilla
Bug #4155: NPCs don't equip a second ring in some cases
Bug #4156: Snow doesn't create water ripples
Bug #4165: NPCs autoequip new clothing with the same price
Feature #452: Rain-induced water ripples
Feature #824: Fading for doors and teleport commands
Feature #933: Editor: LTEX record table
Feature #936: Editor: LAND record table
Feature #1374: AI: Resurface to breathe
Feature #2320: ess-Importer: convert projectiles
Feature #2509: Editor: highlighting occurrences of a word in a script
Feature #2748: Editor: Should use one resource manager per document
Feature #2834: Have openMW's UI remember what menu items were 'pinned' across boots.
Feature #2923: Option to show the damage of the arrows through tooltip.
Feature #3099: Disabling inventory while dragging an item forces you to drop it
Feature #3274: Editor: Script Editor - Shortcuts and context menu options for commenting code out and uncommenting code respectively
Feature #3275: Editor: User Settings- Add an option to reset settings to their default status (per category / all)
Feature #3400: Add keyboard shortcuts for menus
Feature #3492: Show success rate while enchanting
Feature #3530: Editor: Reload data files
Feature #3682: Editor: Default key binding reset
Feature #3921: Combat AI: aggro priorities
Feature #3941: Allow starting at an unnamed exterior cell with --start
Feature #3952: Add Visual Studio 2017 support
Feature #3953: Combat AI: use "WhenUsed" enchantments
Feature #4082: Leave the stack of ingredients or potions grabbed after using an ingredient/potion
Task #2258: Windows installer: launch OpenMW tickbox
Task #4152: The Windows CI script is moving files around that CMake should be dealing with
0.42.0 0.42.0
------ ------

@ -320,9 +320,9 @@ if [ -z $SKIP_DOWNLOAD ]; then
"ffmpeg-3.2.4-dev-win${BITS}.zip" "ffmpeg-3.2.4-dev-win${BITS}.zip"
# MyGUI # MyGUI
download "MyGUI 3.2.3-git" \ download "MyGUI 3.2.2" \
"http://www.lysator.liu.se/~ace/OpenMW/deps/MyGUI-3.2.3-git-msvc${MSVC_YEAR}-win${BITS}.7z" \ "http://www.lysator.liu.se/~ace/OpenMW/deps/MyGUI-3.2.2-msvc${MSVC_YEAR}-win${BITS}.7z" \
"MyGUI-3.2.3-git-msvc${MSVC_YEAR}-win${BITS}.7z" "MyGUI-3.2.2-msvc${MSVC_YEAR}-win${BITS}.7z"
# OpenAL # OpenAL
download "OpenAL-Soft 1.17.2" \ download "OpenAL-Soft 1.17.2" \
@ -350,9 +350,9 @@ if [ -z $SKIP_DOWNLOAD ]; then
fi fi
# SDL2 # SDL2
download "SDL 2.0.4" \ download "SDL 2.0.7" \
"https://www.libsdl.org/release/SDL2-devel-2.0.4-VC.zip" \ "https://www.libsdl.org/release/SDL2-devel-2.0.7-VC.zip" \
"SDL2-2.0.4.zip" "SDL2-2.0.7.zip"
fi fi
cd .. #/.. cd .. #/..
@ -474,20 +474,20 @@ cd $DEPS
echo echo
# MyGUI # MyGUI
printf "MyGUI 3.2.3-git... " printf "MyGUI 3.2.2... "
{ {
cd $DEPS_INSTALL cd $DEPS_INSTALL
if [ -d MyGUI ] && \ if [ -d MyGUI ] && \
grep "MYGUI_VERSION_MAJOR 3" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \ grep "MYGUI_VERSION_MAJOR 3" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \
grep "MYGUI_VERSION_MINOR 2" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \ grep "MYGUI_VERSION_MINOR 2" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \
grep "MYGUI_VERSION_PATCH 3" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null grep "MYGUI_VERSION_PATCH 2" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null
then then
printf "Exists. " printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then elif [ -z $SKIP_EXTRACT ]; then
rm -rf MyGUI rm -rf MyGUI
eval 7z x -y "${DEPS}/MyGUI-3.2.3-git-msvc${MSVC_YEAR}-win${BITS}.7z" $STRIP eval 7z x -y "${DEPS}/MyGUI-3.2.2-msvc${MSVC_YEAR}-win${BITS}.7z" $STRIP
mv "MyGUI-3.2.3-git-msvc${MSVC_YEAR}-win${BITS}" MyGUI mv "MyGUI-3.2.2-msvc${MSVC_YEAR}-win${BITS}" MyGUI
fi fi
export MYGUI_HOME="$(real_pwd)/MyGUI" export MYGUI_HOME="$(real_pwd)/MyGUI"
@ -632,18 +632,18 @@ cd $DEPS
echo echo
# SDL2 # SDL2
printf "SDL 2.0.4... " printf "SDL 2.0.7... "
{ {
if [ -d SDL2-2.0.4 ]; then if [ -d SDL2-2.0.7 ]; then
printf "Exists. " printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then elif [ -z $SKIP_EXTRACT ]; then
rm -rf SDL2-2.0.4 rm -rf SDL2-2.0.7
eval 7z x -y SDL2-2.0.4.zip $STRIP eval 7z x -y SDL2-2.0.7.zip $STRIP
fi fi
export SDL2DIR="$(real_pwd)/SDL2-2.0.4" export SDL2DIR="$(real_pwd)/SDL2-2.0.7"
add_runtime_dlls "$(pwd)/SDL2-2.0.4/lib/x${ARCHSUFFIX}/SDL2.dll" add_runtime_dlls "$(pwd)/SDL2-2.0.7/lib/x${ARCHSUFFIX}/SDL2.dll"
echo Done. echo Done.
} }

@ -24,16 +24,8 @@ if (USE_QT)
set_property(CACHE DESIRED_QT_VERSION PROPERTY STRINGS 4 5) set_property(CACHE DESIRED_QT_VERSION PROPERTY STRINGS 4 5)
endif() endif()
if (APPLE) # set the minimum required version across the board
# OS X build process relies on this fix: https://github.com/Kitware/CMake/commit/3df5147043d83aa09acd5c9ce31d5c602efb99db cmake_minimum_required(VERSION 3.1.0)
cmake_minimum_required(VERSION 3.1.0)
elseif (USE_QT AND DESIRED_QT_VERSION MATCHES 5)
# 2.8.11+ is required to make Qt5 happy and allow linking QtMain on Windows.
cmake_minimum_required(VERSION 2.8.11)
else()
# We probably support older versions than this.
cmake_minimum_required(VERSION 2.6)
endif()
project(OpenMW) project(OpenMW)
@ -62,7 +54,7 @@ endif()
message(STATUS "Configuring OpenMW...") message(STATUS "Configuring OpenMW...")
set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MAJOR 0)
set(OPENMW_VERSION_MINOR 42) set(OPENMW_VERSION_MINOR 43)
set(OPENMW_VERSION_RELEASE 0) set(OPENMW_VERSION_RELEASE 0)
set(OPENMW_VERSION_COMMITHASH "") set(OPENMW_VERSION_COMMITHASH "")
@ -336,6 +328,12 @@ copy_resource_file(${OpenMW_SOURCE_DIR}/files/opencs/defaultfilters
configure_resource_file(${OpenMW_SOURCE_DIR}/files/gamecontrollerdb.txt configure_resource_file(${OpenMW_SOURCE_DIR}/files/gamecontrollerdb.txt
"${OpenMW_BINARY_DIR}" "gamecontrollerdb.txt") "${OpenMW_BINARY_DIR}" "gamecontrollerdb.txt")
configure_resource_file(${OpenMW_SOURCE_DIR}/files/gamecontrollerdb_204.txt
"${OpenMW_BINARY_DIR}" "gamecontrollerdb_204.txt")
configure_resource_file(${OpenMW_SOURCE_DIR}/files/gamecontrollerdb_205.txt
"${OpenMW_BINARY_DIR}" "gamecontrollerdb_205.txt")
if (NOT WIN32 AND NOT APPLE) if (NOT WIN32 AND NOT APPLE)
configure_file(${OpenMW_SOURCE_DIR}/files/openmw.desktop configure_file(${OpenMW_SOURCE_DIR}/files/openmw.desktop
"${OpenMW_BINARY_DIR}/openmw.desktop") "${OpenMW_BINARY_DIR}/openmw.desktop")
@ -346,6 +344,7 @@ if (NOT WIN32 AND NOT APPLE)
endif() endif()
# CXX Compiler settings # CXX Compiler settings
set(CMAKE_CXX_STANDARD 11)
if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wundef -Wno-unused-parameter -std=c++11 -pedantic -Wno-long-long") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wundef -Wno-unused-parameter -std=c++11 -pedantic -Wno-long-long")
add_definitions( -DBOOST_NO_CXX11_SCOPED_ENUMS=ON ) add_definitions( -DBOOST_NO_CXX11_SCOPED_ENUMS=ON )
@ -412,7 +411,7 @@ IF(NOT WIN32 AND NOT APPLE)
#ENDIF(BUILD_MYGUI_PLUGIN) #ENDIF(BUILD_MYGUI_PLUGIN)
# Install licenses # Install licenses
INSTALL(FILES "docs/license/DejaVu Font License.txt" DESTINATION "${LICDIR}" ) INSTALL(FILES "files/mygui/DejaVu Font License.txt" DESTINATION "${LICDIR}" )
# Install icon and desktop file # Install icon and desktop file
INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.desktop" DESTINATION "${DATAROOTDIR}/applications" COMPONENT "openmw") INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.desktop" DESTINATION "${DATAROOTDIR}/applications" COMPONENT "openmw")
@ -428,6 +427,8 @@ IF(NOT WIN32 AND NOT APPLE)
INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.cfg.install" DESTINATION "${SYSCONFDIR}" RENAME "openmw.cfg" COMPONENT "openmw") INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.cfg.install" DESTINATION "${SYSCONFDIR}" RENAME "openmw.cfg" COMPONENT "openmw")
INSTALL(FILES "${OpenMW_BINARY_DIR}/resources/version" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") INSTALL(FILES "${OpenMW_BINARY_DIR}/resources/version" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw")
INSTALL(FILES "${OpenMW_BINARY_DIR}/gamecontrollerdb.txt" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") INSTALL(FILES "${OpenMW_BINARY_DIR}/gamecontrollerdb.txt" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw")
INSTALL(FILES "${OpenMW_BINARY_DIR}/gamecontrollerdb_204.txt" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw")
INSTALL(FILES "${OpenMW_BINARY_DIR}/gamecontrollerdb_205.txt" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw")
IF(BUILD_OPENCS) IF(BUILD_OPENCS)
INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw-cs.cfg" DESTINATION "${SYSCONFDIR}" COMPONENT "opencs") INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw-cs.cfg" DESTINATION "${SYSCONFDIR}" COMPONENT "opencs")
@ -443,15 +444,22 @@ if(WIN32)
FILE(GLOB dll_files_release "${OpenMW_BINARY_DIR}/Release/*.dll") FILE(GLOB dll_files_release "${OpenMW_BINARY_DIR}/Release/*.dll")
INSTALL(FILES ${dll_files_debug} DESTINATION "." CONFIGURATIONS Debug) INSTALL(FILES ${dll_files_debug} DESTINATION "." CONFIGURATIONS Debug)
INSTALL(FILES ${dll_files_release} DESTINATION "." CONFIGURATIONS Release;RelWithDebInfo;MinSizeRel) INSTALL(FILES ${dll_files_release} DESTINATION "." CONFIGURATIONS Release;RelWithDebInfo;MinSizeRel)
INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.cfg.install" DESTINATION "." RENAME "openmw.cfg") INSTALL(FILES "${OpenMW_BINARY_DIR}/Debug/openmw.cfg.install" DESTINATION "." RENAME "openmw.cfg" CONFIGURATIONS Debug)
INSTALL(FILES "${OpenMW_BINARY_DIR}/Release/openmw.cfg.install" DESTINATION "." RENAME "openmw.cfg" CONFIGURATIONS Release;RelWithDebInfo;MinSizeRel)
INSTALL(FILES "${OpenMW_SOURCE_DIR}/CHANGELOG.md" DESTINATION "." RENAME "CHANGELOG.txt") INSTALL(FILES "${OpenMW_SOURCE_DIR}/CHANGELOG.md" DESTINATION "." RENAME "CHANGELOG.txt")
INSTALL(FILES "${OpenMW_SOURCE_DIR}/README.md" DESTINATION "." RENAME "README.txt") INSTALL(FILES "${OpenMW_SOURCE_DIR}/README.md" DESTINATION "." RENAME "README.txt")
INSTALL(FILES "${OpenMW_SOURCE_DIR}/LICENSE" DESTINATION "." RENAME "LICENSE.txt")
INSTALL(FILES INSTALL(FILES
"${OpenMW_SOURCE_DIR}/Docs/license/GPL3.txt" "${OpenMW_SOURCE_DIR}/files/mygui/DejaVu Font License.txt"
"${OpenMW_SOURCE_DIR}/Docs/license/DejaVu Font License.txt"
"${OpenMW_BINARY_DIR}/settings-default.cfg"
"${OpenMW_BINARY_DIR}/gamecontrollerdb.txt"
DESTINATION ".") DESTINATION ".")
INSTALL(FILES "${OpenMW_BINARY_DIR}/Debug/settings-default.cfg" DESTINATION "." CONFIGURATIONS Debug)
INSTALL(FILES "${OpenMW_BINARY_DIR}/Release/settings-default.cfg" DESTINATION "." CONFIGURATIONS Release;RelWithDebInfo;MinSizeRel)
INSTALL(FILES "${OpenMW_BINARY_DIR}/Debug/gamecontrollerdb.txt" DESTINATION "." CONFIGURATIONS Debug)
INSTALL(FILES "${OpenMW_BINARY_DIR}/Release/gamecontrollerdb.txt" DESTINATION "." CONFIGURATIONS Release;RelWithDebInfo;MinSizeRel)
INSTALL(FILES "${OpenMW_BINARY_DIR}/Debug/gamecontrollerdb_204.txt" DESTINATION "." CONFIGURATIONS Debug)
INSTALL(FILES "${OpenMW_BINARY_DIR}/Release/gamecontrollerdb_204.txt" DESTINATION "." CONFIGURATIONS Release;RelWithDebInfo;MinSizeRel)
INSTALL(FILES "${OpenMW_BINARY_DIR}/Debug/gamecontrollerdb_205.txt" DESTINATION "." CONFIGURATIONS Debug)
INSTALL(FILES "${OpenMW_BINARY_DIR}/Release/gamecontrollerdb_205.txt" DESTINATION "." CONFIGURATIONS Release;RelWithDebInfo;MinSizeRel)
if(BUILD_MYGUI_PLUGIN) if(BUILD_MYGUI_PLUGIN)
INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Debug/Plugin_MyGUI_OpenMW_Resources.dll" DESTINATION "." CONFIGURATIONS Debug) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Debug/Plugin_MyGUI_OpenMW_Resources.dll" DESTINATION "." CONFIGURATIONS Debug)
@ -463,7 +471,9 @@ if(WIN32)
INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/Release/platforms" DESTINATION "." CONFIGURATIONS Release;RelWithDebInfo;MinSizeRel) INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/Release/platforms" DESTINATION "." CONFIGURATIONS Release;RelWithDebInfo;MinSizeRel)
ENDIF() ENDIF()
INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION ".") INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/Debug/resources" DESTINATION "." CONFIGURATIONS Debug)
INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/Release/resources" DESTINATION "." CONFIGURATIONS Release;RelWithDebInfo;MinSizeRel)
FILE(GLOB plugin_dir_debug "${OpenMW_BINARY_DIR}/Debug/osgPlugins-*") FILE(GLOB plugin_dir_debug "${OpenMW_BINARY_DIR}/Debug/osgPlugins-*")
FILE(GLOB plugin_dir_release "${OpenMW_BINARY_DIR}/Release/osgPlugins-*") FILE(GLOB plugin_dir_release "${OpenMW_BINARY_DIR}/Release/osgPlugins-*")
INSTALL(DIRECTORY ${plugin_dir_debug} DESTINATION "." CONFIGURATIONS Debug) INSTALL(DIRECTORY ${plugin_dir_debug} DESTINATION "." CONFIGURATIONS Debug)

@ -33,6 +33,7 @@ Furthermore, we advise to:
* Feel free to submit incomplete pull requests. Even if the work can not be merged yet, pull requests are a great place to collect early feedback. Just make sure to mark it as *[Incomplete]* or *[Do not merge yet]* in the title. * Feel free to submit incomplete pull requests. Even if the work can not be merged yet, pull requests are a great place to collect early feedback. Just make sure to mark it as *[Incomplete]* or *[Do not merge yet]* in the title.
* If you plan on contributing often, please read the [Developer Reference](https://wiki.openmw.org/index.php?title=Developer_Reference) on our wiki, especially the [Policies and Standards](https://wiki.openmw.org/index.php?title=Policies_and_Standards). * If you plan on contributing often, please read the [Developer Reference](https://wiki.openmw.org/index.php?title=Developer_Reference) on our wiki, especially the [Policies and Standards](https://wiki.openmw.org/index.php?title=Policies_and_Standards).
* Make sure each of your changes has a clear objective. Unnecessary changes may lead to merge conflicts, clutter the commit history and slow down review. Code formatting 'fixes' should be avoided, unless you were already changing that particular line anyway. * Make sure each of your changes has a clear objective. Unnecessary changes may lead to merge conflicts, clutter the commit history and slow down review. Code formatting 'fixes' should be avoided, unless you were already changing that particular line anyway.
* Reference the bug / feature ticket(s) in your commit message (e.g. 'Bug #123') to make it easier to keep track of what we changed for what reason. Our bugtracker will show those commits next to the ticket. If your commit message includes 'Fixes #123', that bug/feature will automatically be set to 'Closed' when your commit is merged.
Guidelines for original engine "fixes" Guidelines for original engine "fixes"
================================= =================================

@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found. the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.> {one line to give the program's name and a brief idea of what it does.}
Copyright (C) <year> <name of author> Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode: notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author> {project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details. under certain conditions; type `show c' for details.

@ -7,13 +7,13 @@ OpenMW is a recreation of the engine for the popular role-playing game Morrowind
OpenMW also comes with OpenMW-CS, a replacement for Morrowind's TES Construction Set. OpenMW also comes with OpenMW-CS, a replacement for Morrowind's TES Construction Set.
* Version: 0.42.0 * Version: 0.43.0
* License: GPLv3 (see [docs/license/GPL3.txt](https://github.com/OpenMW/openmw/blob/master/docs/license/GPL3.txt) for more information) * License: GPLv3 (see [LICENSE](https://github.com/OpenMW/openmw/blob/master/LICENSE) for more information)
* Website: http://www.openmw.org * Website: http://www.openmw.org
* IRC: #openmw on irc.freenode.net * IRC: #openmw on irc.freenode.net
Font Licenses: Font Licenses:
* DejaVuLGCSansMono.ttf: custom (see docs/license/DejaVu Font License.txt for more information) * DejaVuLGCSansMono.ttf: custom (see [files/mygui/DejaVu Font License.txt](https://github.com/OpenMW/openmw/blob/master/files/mygui/DejaVu%20Font%20License.txt) for more information)
Current Status Current Status
-------------- --------------

@ -6,6 +6,7 @@ set(LAUNCHER
playpage.cpp playpage.cpp
textslotmsgbox.cpp textslotmsgbox.cpp
settingspage.cpp settingspage.cpp
advancedpage.cpp
utils/profilescombobox.cpp utils/profilescombobox.cpp
utils/textinputdialog.cpp utils/textinputdialog.cpp
@ -21,6 +22,7 @@ set(LAUNCHER_HEADER
playpage.hpp playpage.hpp
textslotmsgbox.hpp textslotmsgbox.hpp
settingspage.hpp settingspage.hpp
advancedpage.hpp
utils/profilescombobox.hpp utils/profilescombobox.hpp
utils/textinputdialog.hpp utils/textinputdialog.hpp
@ -35,6 +37,7 @@ set(LAUNCHER_HEADER_MOC
playpage.hpp playpage.hpp
textslotmsgbox.hpp textslotmsgbox.hpp
settingspage.hpp settingspage.hpp
advancedpage.hpp
utils/textinputdialog.hpp utils/textinputdialog.hpp
utils/profilescombobox.hpp utils/profilescombobox.hpp
@ -49,6 +52,7 @@ set(LAUNCHER_UI
${CMAKE_SOURCE_DIR}/files/ui/playpage.ui ${CMAKE_SOURCE_DIR}/files/ui/playpage.ui
${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui
${CMAKE_SOURCE_DIR}/files/ui/settingspage.ui ${CMAKE_SOURCE_DIR}/files/ui/settingspage.ui
${CMAKE_SOURCE_DIR}/files/ui/advancedpage.ui
) )
source_group(launcher FILES ${LAUNCHER} ${LAUNCHER_HEADER}) source_group(launcher FILES ${LAUNCHER} ${LAUNCHER_HEADER})

@ -0,0 +1,89 @@
#include "advancedpage.hpp"
#include <components/files/configurationmanager.hpp>
Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg, Settings::Manager &engineSettings, QWidget *parent)
: QWidget(parent)
, mCfgMgr(cfg)
, mEngineSettings(engineSettings)
{
setObjectName ("AdvancedPage");
setupUi(this);
loadSettings();
}
bool Launcher::AdvancedPage::loadSettings()
{
// Game Settings
loadSettingBool(canLootDuringDeathAnimationCheckBox, "can loot during death animation", "Game");
loadSettingBool(followersAttackOnSightCheckBox, "followers attack on sight", "Game");
loadSettingBool(preventMerchantEquippingCheckBox, "prevent merchant equipping", "Game");
loadSettingBool(showEffectDurationCheckBox, "show effect duration", "Game");
loadSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game");
loadSettingBool(showMeleeInfoCheckBox, "show melee info", "Game");
loadSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game");
// Expected values are (0, 1, 2, 3)
int showOwnedIndex = mEngineSettings.getInt("show owned", "Game");
// Match the index with the option. Will default to 0 if invalid.
if (showOwnedIndex >= 0 && showOwnedIndex <= 3)
showOwnedComboBox->setCurrentIndex(showOwnedIndex);
// Input Settings
loadSettingBool(allowThirdPersonZoomCheckBox, "allow third person zoom", "Input");
loadSettingBool(grabCursorCheckBox, "grab cursor", "Input");
loadSettingBool(toggleSneakCheckBox, "toggle sneak", "Input");
// Other Settings
loadSettingBool(timePlayedCheckbox, "timeplayed", "Saves");
QString screenshotFormatString = QString::fromStdString(mEngineSettings.getString("screenshot format", "General")).toUpper();
if (screenshotFormatComboBox->findText(screenshotFormatString) == -1)
screenshotFormatComboBox->addItem(screenshotFormatString);
screenshotFormatComboBox->setCurrentIndex(screenshotFormatComboBox->findText(screenshotFormatString));
return true;
}
void Launcher::AdvancedPage::saveSettings()
{
// Ensure we only set the new settings if they changed. This is to avoid cluttering the
// user settings file (which by definition should only contain settings the user has touched)
// Game Settings
saveSettingBool(canLootDuringDeathAnimationCheckBox, "can loot during death animation", "Game");
saveSettingBool(followersAttackOnSightCheckBox, "followers attack on sight", "Game");
saveSettingBool(preventMerchantEquippingCheckBox, "prevent merchant equipping", "Game");
saveSettingBool(showEffectDurationCheckBox, "show effect duration", "Game");
saveSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game");
saveSettingBool(showMeleeInfoCheckBox, "show melee info", "Game");
saveSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game");
int showOwnedCurrentIndex = showOwnedComboBox->currentIndex();
if (showOwnedCurrentIndex != mEngineSettings.getInt("show owned", "Game"))
mEngineSettings.setInt("show owned", "Game", showOwnedCurrentIndex);
// Input Settings
saveSettingBool(allowThirdPersonZoomCheckBox, "allow third person zoom", "Input");
saveSettingBool(grabCursorCheckBox, "grab cursor", "Input");
saveSettingBool(toggleSneakCheckBox, "toggle sneak", "Input");
// Other Settings
saveSettingBool(timePlayedCheckbox, "timeplayed", "Saves");
std::string screenshotFormatString = screenshotFormatComboBox->currentText().toLower().toStdString();
if (screenshotFormatString != mEngineSettings.getString("screenshot format", "General"))
mEngineSettings.setString("screenshot format", "General", screenshotFormatString);
}
void Launcher::AdvancedPage::loadSettingBool(QCheckBox *checkbox, const std::string &setting, const std::string &group) {
if (mEngineSettings.getBool(setting, group))
checkbox->setCheckState(Qt::Checked);
}
void Launcher::AdvancedPage::saveSettingBool(QCheckBox *checkbox, const std::string &setting, const std::string &group) {
bool cValue = checkbox->checkState();
if (cValue != mEngineSettings.getBool(setting, group))
mEngineSettings.setBool(setting, group, cValue);
}

@ -0,0 +1,32 @@
#ifndef ADVANCEDPAGE_H
#define ADVANCEDPAGE_H
#include <QWidget>
#include "ui_advancedpage.h"
#include <components/settings/settings.hpp>
namespace Files { struct ConfigurationManager; }
namespace Launcher
{
class AdvancedPage : public QWidget, private Ui::AdvancedPage
{
Q_OBJECT
public:
AdvancedPage(Files::ConfigurationManager &cfg, Settings::Manager &engineSettings, QWidget *parent = 0);
bool loadSettings();
void saveSettings();
private:
Files::ConfigurationManager &mCfgMgr;
Settings::Manager &mEngineSettings;
void loadSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group);
void saveSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group);
};
}
#endif

@ -2,9 +2,7 @@
#include <components/version/version.hpp> #include <components/version/version.hpp>
#include <QLabel>
#include <QDate> #include <QDate>
#include <QTime>
#include <QMessageBox> #include <QMessageBox>
#include <QPushButton> #include <QPushButton>
#include <QFontDatabase> #include <QFontDatabase>
@ -12,8 +10,6 @@
#include <QFileDialog> #include <QFileDialog>
#include <QCloseEvent> #include <QCloseEvent>
#include <QTextCodec> #include <QTextCodec>
#include <QFile>
#include <QDir>
#include <QDebug> #include <QDebug>
@ -21,6 +17,7 @@
#include "graphicspage.hpp" #include "graphicspage.hpp"
#include "datafilespage.hpp" #include "datafilespage.hpp"
#include "settingspage.hpp" #include "settingspage.hpp"
#include "advancedpage.hpp"
using namespace Process; using namespace Process;
@ -104,6 +101,12 @@ void Launcher::MainDialog::createIcons()
settingsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); settingsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom);
settingsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); settingsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
QListWidgetItem *advancedButton = new QListWidgetItem(iconWidget);
advancedButton->setIcon(QIcon::fromTheme("emblem-system"));
advancedButton->setText(tr("Advanced"));
advancedButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom);
advancedButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
connect(iconWidget, connect(iconWidget,
SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)),
this, SLOT(changePage(QListWidgetItem*,QListWidgetItem*))); this, SLOT(changePage(QListWidgetItem*,QListWidgetItem*)));
@ -116,6 +119,7 @@ void Launcher::MainDialog::createPages()
mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this);
mGraphicsPage = new GraphicsPage(mCfgMgr, mEngineSettings, this); mGraphicsPage = new GraphicsPage(mCfgMgr, mEngineSettings, this);
mSettingsPage = new SettingsPage(mCfgMgr, mGameSettings, mLauncherSettings, this); mSettingsPage = new SettingsPage(mCfgMgr, mGameSettings, mLauncherSettings, this);
mAdvancedPage = new AdvancedPage(mCfgMgr, mEngineSettings, this);
// Set the combobox of the play page to imitate the combobox on the datafilespage // Set the combobox of the play page to imitate the combobox on the datafilespage
mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); mPlayPage->setProfilesModel(mDataFilesPage->profilesModel());
@ -126,6 +130,7 @@ void Launcher::MainDialog::createPages()
pagesWidget->addWidget(mDataFilesPage); pagesWidget->addWidget(mDataFilesPage);
pagesWidget->addWidget(mGraphicsPage); pagesWidget->addWidget(mGraphicsPage);
pagesWidget->addWidget(mSettingsPage); pagesWidget->addWidget(mSettingsPage);
pagesWidget->addWidget(mAdvancedPage);
// Select the first page // Select the first page
iconWidget->setCurrentItem(iconWidget->item(0), QItemSelectionModel::Select); iconWidget->setCurrentItem(iconWidget->item(0), QItemSelectionModel::Select);
@ -245,6 +250,9 @@ bool Launcher::MainDialog::reloadSettings()
if (!mGraphicsPage->loadSettings()) if (!mGraphicsPage->loadSettings())
return false; return false;
if (!mAdvancedPage->loadSettings())
return false;
return true; return true;
} }
@ -483,6 +491,7 @@ bool Launcher::MainDialog::writeSettings()
mDataFilesPage->saveSettings(); mDataFilesPage->saveSettings();
mGraphicsPage->saveSettings(); mGraphicsPage->saveSettings();
mSettingsPage->saveSettings(); mSettingsPage->saveSettings();
mAdvancedPage->saveSettings();
QString userPath = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str()); QString userPath = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str());
QDir dir(userPath); QDir dir(userPath);

@ -30,6 +30,7 @@ namespace Launcher
class DataFilesPage; class DataFilesPage;
class UnshieldThread; class UnshieldThread;
class SettingsPage; class SettingsPage;
class AdvancedPage;
enum FirstRunDialogResult enum FirstRunDialogResult
{ {
@ -88,6 +89,7 @@ namespace Launcher
GraphicsPage *mGraphicsPage; GraphicsPage *mGraphicsPage;
DataFilesPage *mDataFilesPage; DataFilesPage *mDataFilesPage;
SettingsPage *mSettingsPage; SettingsPage *mSettingsPage;
AdvancedPage *mAdvancedPage;
Process::ProcessInvoker *mGameInvoker; Process::ProcessInvoker *mGameInvoker;
Process::ProcessInvoker *mWizardInvoker; Process::ProcessInvoker *mWizardInvoker;

@ -252,7 +252,8 @@ endif()
if (WIN32) if (WIN32)
target_link_libraries(openmw-cs ${Boost_LOCALE_LIBRARY}) target_link_libraries(openmw-cs ${Boost_LOCALE_LIBRARY})
INSTALL(TARGETS openmw-cs RUNTIME DESTINATION ".") INSTALL(TARGETS openmw-cs RUNTIME DESTINATION ".")
INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw-cs.cfg" DESTINATION ".") INSTALL(FILES "${OpenMW_BINARY_DIR}/Debug/openmw-cs.cfg" DESTINATION "." CONFIGURATIONS Debug)
INSTALL(FILES "${OpenMW_BINARY_DIR}/Release/openmw-cs.cfg" DESTINATION "." CONFIGURATIONS Release;RelWithDebInfo;MinSizeRel)
endif() endif()
if (MSVC) if (MSVC)

@ -169,18 +169,25 @@ void CSMPrefs::State::declare()
"list go to the first/last item"); "list go to the first/last item");
declareCategory ("3D Scene Input"); declareCategory ("3D Scene Input");
declareDouble ("navi-wheel-factor", "Camera Zoom Sensitivity", 8).setRange(-100.0, 100.0);
declareDouble ("s-navi-sensitivity", "Secondary Camera Movement Sensitivity", 50.0).setRange(-1000.0, 1000.0);
declareSeparator();
declareDouble ("p-navi-free-sensitivity", "Free Camera Sensitivity", 1/650.).setPrecision(5).setRange(0.0, 1.0); declareDouble ("p-navi-free-sensitivity", "Free Camera Sensitivity", 1/650.).setPrecision(5).setRange(0.0, 1.0);
declareBool ("p-navi-free-invert", "Invert Free Camera Mouse Input", false); declareBool ("p-navi-free-invert", "Invert Free Camera Mouse Input", false);
declareDouble ("p-navi-orbit-sensitivity", "Orbit Camera Sensitivity", 1/650.).setPrecision(5).setRange(0.0, 1.0);
declareBool ("p-navi-orbit-invert", "Invert Orbit Camera Mouse Input", false);
declareDouble ("s-navi-sensitivity", "Secondary Camera Movement Sensitivity", 50.0).setRange(-1000.0, 1000.0);
declareDouble ("navi-wheel-factor", "Camera Zoom Sensitivity", 8).setRange(-100.0, 100.0);
declareDouble ("navi-free-lin-speed", "Free Camera Linear Speed", 1000.0).setRange(1.0, 10000.0); declareDouble ("navi-free-lin-speed", "Free Camera Linear Speed", 1000.0).setRange(1.0, 10000.0);
declareDouble ("navi-free-rot-speed", "Free Camera Rotational Speed", 3.14 / 2).setRange(0.001, 6.28); declareDouble ("navi-free-rot-speed", "Free Camera Rotational Speed", 3.14 / 2).setRange(0.001, 6.28);
declareDouble ("navi-free-speed-mult", "Free Camera Speed Multiplier (from Modifier)", 8).setRange(0.001, 1000.0); declareDouble ("navi-free-speed-mult", "Free Camera Speed Multiplier (from Modifier)", 8).setRange(0.001, 1000.0);
declareSeparator();
declareDouble ("p-navi-orbit-sensitivity", "Orbit Camera Sensitivity", 1/650.).setPrecision(5).setRange(0.0, 1.0);
declareBool ("p-navi-orbit-invert", "Invert Orbit Camera Mouse Input", false);
declareDouble ("navi-orbit-rot-speed", "Orbital Camera Rotational Speed", 3.14 / 4).setRange(0.001, 6.28); declareDouble ("navi-orbit-rot-speed", "Orbital Camera Rotational Speed", 3.14 / 4).setRange(0.001, 6.28);
declareDouble ("navi-orbit-speed-mult", "Orbital Camera Speed Multiplier (from Modifier)", 4).setRange(0.001, 1000.0); declareDouble ("navi-orbit-speed-mult", "Orbital Camera Speed Multiplier (from Modifier)", 4).setRange(0.001, 1000.0);
declareBool ("navi-orbit-const-roll", "Keep camera roll constant for orbital camera", true);
declareSeparator(); declareSeparator();
declareBool ("context-select", "Context Sensitive Selection", false); declareBool ("context-select", "Context Sensitive Selection", false);
declareDouble ("drag-factor", "Mouse sensitivity during drag operations", 1.0). declareDouble ("drag-factor", "Mouse sensitivity during drag operations", 1.0).
setRange (0.001, 100.0); setRange (0.001, 100.0);
@ -192,6 +199,14 @@ void CSMPrefs::State::declare()
setRange (0.001, 100.0); setRange (0.001, 100.0);
declareDouble ("rotate-factor", "Free rotation factor", 0.007).setPrecision(4).setRange(0.0001, 0.1); declareDouble ("rotate-factor", "Free rotation factor", 0.007).setPrecision(4).setRange(0.0001, 0.1);
declareCategory ("Rendering");
declareInt ("camera-fov", "Camera FOV", 90).setRange(10, 170);
declareBool ("camera-ortho", "Orthographic projection for camera", false);
declareInt ("camera-ortho-size", "Orthographic projection size parameter", 100).
setTooltip("Size of the orthographic frustum, greater value will allow the camera to see more of the world.").
setRange(10, 10000);
declareDouble ("object-marker-alpha", "Object Marker Transparency", 0.5).setPrecision(2).setRange(0,1);
declareCategory ("Tooltips"); declareCategory ("Tooltips");
declareBool ("scene", "Show Tooltips in 3D scenes", true); declareBool ("scene", "Show Tooltips in 3D scenes", true);
declareBool ("scene-hide-basic", "Hide basic 3D scenes tooltips", false); declareBool ("scene-hide-basic", "Hide basic 3D scenes tooltips", false);

@ -84,7 +84,6 @@ CSVPrefs::Dialogue::~Dialogue()
void CSVPrefs::Dialogue::closeEvent (QCloseEvent *event) void CSVPrefs::Dialogue::closeEvent (QCloseEvent *event)
{ {
QMainWindow::closeEvent (event); QMainWindow::closeEvent (event);
CSMPrefs::State::get().save(); CSMPrefs::State::get().save();
} }

@ -668,16 +668,25 @@ namespace CSVRender
mInitialized = true; mInitialized = true;
} }
void OrbitCameraController::setConstRoll(bool enabled)
{
mConstRoll = enabled;
}
void OrbitCameraController::rotateHorizontal(double value) void OrbitCameraController::rotateHorizontal(double value)
{ {
osg::Vec3d eye, center, up; osg::Vec3d eye, center, up;
getCamera()->getViewMatrixAsLookAt(eye, center, up); getCamera()->getViewMatrixAsLookAt(eye, center, up);
osg::Vec3d absoluteUp = osg::Vec3(0,0,1);
osg::Quat rotation = osg::Quat(value, up); osg::Quat rotation = osg::Quat(value, mConstRoll ? absoluteUp : up);
osg::Vec3d oldOffset = eye - mCenter; osg::Vec3d oldOffset = eye - mCenter;
osg::Vec3d newOffset = rotation * oldOffset; osg::Vec3d newOffset = rotation * oldOffset;
if (mConstRoll)
up = rotation * up;
getCamera()->setViewMatrixAsLookAt(mCenter + newOffset, mCenter, up); getCamera()->setViewMatrixAsLookAt(mCenter + newOffset, mCenter, up);
} }
@ -687,9 +696,14 @@ namespace CSVRender
getCamera()->getViewMatrixAsLookAt(eye, center, up); getCamera()->getViewMatrixAsLookAt(eye, center, up);
osg::Vec3d forward = center - eye; osg::Vec3d forward = center - eye;
osg::Quat rotation = osg::Quat(value, up ^ forward); osg::Vec3d axis = up ^ forward;
osg::Quat rotation = osg::Quat(value,axis);
osg::Vec3d oldOffset = eye - mCenter; osg::Vec3d oldOffset = eye - mCenter;
osg::Vec3d newOffset = rotation * oldOffset; osg::Vec3d newOffset = rotation * oldOffset;
if (mConstRoll)
up = rotation * up;
getCamera()->setViewMatrixAsLookAt(mCenter + newOffset, mCenter, up); getCamera()->setViewMatrixAsLookAt(mCenter + newOffset, mCenter, up);
} }

@ -160,6 +160,8 @@ namespace CSVRender
/// \brief Flag controller to be re-initialized. /// \brief Flag controller to be re-initialized.
void reset(); void reset();
void setConstRoll(bool enable);
private: private:
void initialize(); void initialize();
@ -181,6 +183,8 @@ namespace CSVRender
double mOrbitSpeed; double mOrbitSpeed;
double mOrbitSpeedMult; double mOrbitSpeedMult;
bool mConstRoll;
private slots: private slots:
void naviPrimary(bool active); void naviPrimary(bool active);

@ -3,6 +3,7 @@
#include <stdexcept> #include <stdexcept>
#include <iostream> #include <iostream>
#include <osg/Depth>
#include <osg/Group> #include <osg/Group>
#include <osg/PositionAttitudeTransform> #include <osg/PositionAttitudeTransform>
@ -21,6 +22,7 @@
#include "../../model/world/universalid.hpp" #include "../../model/world/universalid.hpp"
#include "../../model/world/commandmacro.hpp" #include "../../model/world/commandmacro.hpp"
#include "../../model/world/cellcoordinates.hpp" #include "../../model/world/cellcoordinates.hpp"
#include "../../model/prefs/state.hpp"
#include <components/resource/scenemanager.hpp> #include <components/resource/scenemanager.hpp>
#include <components/sceneutil/lightutil.hpp> #include <components/sceneutil/lightutil.hpp>
@ -220,7 +222,7 @@ osg::ref_ptr<osg::Node> CSVRender::Object::makeMoveOrScaleMarker (int axis)
for (int i=0; i<2; ++i) for (int i=0; i<2; ++i)
{ {
float length = i ? shaftLength : 0; float length = i ? shaftLength : MarkerShaftWidth;
vertices->push_back (getMarkerPosition (-MarkerShaftWidth/2, -MarkerShaftWidth/2, length, axis)); vertices->push_back (getMarkerPosition (-MarkerShaftWidth/2, -MarkerShaftWidth/2, length, axis));
vertices->push_back (getMarkerPosition (-MarkerShaftWidth/2, MarkerShaftWidth/2, length, axis)); vertices->push_back (getMarkerPosition (-MarkerShaftWidth/2, MarkerShaftWidth/2, length, axis));
@ -285,15 +287,15 @@ osg::ref_ptr<osg::Node> CSVRender::Object::makeMoveOrScaleMarker (int axis)
for (int i=0; i<8; ++i) for (int i=0; i<8; ++i)
colours->push_back (osg::Vec4f (axis==0 ? 1.0f : 0.2f, axis==1 ? 1.0f : 0.2f, colours->push_back (osg::Vec4f (axis==0 ? 1.0f : 0.2f, axis==1 ? 1.0f : 0.2f,
axis==2 ? 1.0f : 0.2f, 1.0f)); axis==2 ? 1.0f : 0.2f, mMarkerTransparency));
for (int i=8; i<8+4+1; ++i) for (int i=8; i<8+4+1; ++i)
colours->push_back (osg::Vec4f (axis==0 ? 1.0f : 0.0f, axis==1 ? 1.0f : 0.0f, colours->push_back (osg::Vec4f (axis==0 ? 1.0f : 0.0f, axis==1 ? 1.0f : 0.0f,
axis==2 ? 1.0f : 0.0f, 1.0f)); axis==2 ? 1.0f : 0.0f, mMarkerTransparency));
geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX);
geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF); setupCommonMarkerState(geometry);
osg::ref_ptr<osg::Geode> geode (new osg::Geode); osg::ref_ptr<osg::Geode> geode (new osg::Geode);
geode->addDrawable (geometry); geode->addDrawable (geometry);
@ -305,11 +307,11 @@ osg::ref_ptr<osg::Node> CSVRender::Object::makeRotateMarker (int axis)
{ {
const float Pi = 3.14159265f; const float Pi = 3.14159265f;
const float InnerRadius = mBaseNode->getBound().radius(); const float InnerRadius = std::max(MarkerShaftBaseLength, mBaseNode->getBound().radius());
const float OuterRadius = InnerRadius + MarkerShaftWidth; const float OuterRadius = InnerRadius + MarkerShaftWidth;
const float SegmentDistance = 100.f; const float SegmentDistance = 100.f;
const size_t SegmentCount = std::min(64, std::max(8, (int)(OuterRadius * 2 * Pi / SegmentDistance))); const size_t SegmentCount = std::min(64, std::max(24, (int)(OuterRadius * 2 * Pi / SegmentDistance)));
const size_t VerticesPerSegment = 4; const size_t VerticesPerSegment = 4;
const size_t IndicesPerSegment = 24; const size_t IndicesPerSegment = 24;
@ -334,6 +336,9 @@ osg::ref_ptr<osg::Node> CSVRender::Object::makeRotateMarker (int axis)
osg::ref_ptr<osg::DrawElementsUShort> primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, osg::ref_ptr<osg::DrawElementsUShort> primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES,
IndexCount); IndexCount);
// prevent some depth collision issues from overlaps
osg::Vec3f offset = getMarkerPosition(0, MarkerShaftWidth/4, 0, axis);
for (size_t i = 0; i < SegmentCount; ++i) for (size_t i = 0; i < SegmentCount; ++i)
{ {
size_t index = i * VerticesPerSegment; size_t index = i * VerticesPerSegment;
@ -344,13 +349,17 @@ osg::ref_ptr<osg::Node> CSVRender::Object::makeRotateMarker (int axis)
float outerX = OuterRadius * std::cos(i * Angle); float outerX = OuterRadius * std::cos(i * Angle);
float outerY = OuterRadius * std::sin(i * Angle); float outerY = OuterRadius * std::sin(i * Angle);
vertices->at(index++) = getMarkerPosition(innerX, innerY, MarkerShaftWidth / 2, axis); vertices->at(index++) = getMarkerPosition(innerX, innerY, MarkerShaftWidth / 2, axis) + offset;
vertices->at(index++) = getMarkerPosition(innerX, innerY, -MarkerShaftWidth / 2, axis); vertices->at(index++) = getMarkerPosition(innerX, innerY, -MarkerShaftWidth / 2, axis) + offset;
vertices->at(index++) = getMarkerPosition(outerX, outerY, MarkerShaftWidth / 2, axis); vertices->at(index++) = getMarkerPosition(outerX, outerY, MarkerShaftWidth / 2, axis) + offset;
vertices->at(index++) = getMarkerPosition(outerX, outerY, -MarkerShaftWidth / 2, axis); vertices->at(index++) = getMarkerPosition(outerX, outerY, -MarkerShaftWidth / 2, axis) + offset;
} }
colors->at(0) = osg::Vec4f (axis==0 ? 1.0f : 0.2f, axis==1 ? 1.0f : 0.2f, axis==2 ? 1.0f : 0.2f, 1.0f); colors->at(0) = osg::Vec4f (
axis==0 ? 1.0f : 0.2f,
axis==1 ? 1.0f : 0.2f,
axis==2 ? 1.0f : 0.2f,
mMarkerTransparency);
for (size_t i = 0; i < SegmentCount; ++i) for (size_t i = 0; i < SegmentCount; ++i)
{ {
@ -374,7 +383,7 @@ osg::ref_ptr<osg::Node> CSVRender::Object::makeRotateMarker (int axis)
geometry->setColorArray(colors, osg::Array::BIND_OVERALL); geometry->setColorArray(colors, osg::Array::BIND_OVERALL);
geometry->addPrimitiveSet(primitives); geometry->addPrimitiveSet(primitives);
geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF); setupCommonMarkerState(geometry);
osg::ref_ptr<osg::Geode> geode = new osg::Geode(); osg::ref_ptr<osg::Geode> geode = new osg::Geode();
geode->addDrawable (geometry); geode->addDrawable (geometry);
@ -382,6 +391,15 @@ osg::ref_ptr<osg::Node> CSVRender::Object::makeRotateMarker (int axis)
return geode; return geode;
} }
void CSVRender::Object::setupCommonMarkerState(osg::ref_ptr<osg::Geometry> geometry)
{
osg::ref_ptr<osg::StateSet> state = geometry->getOrCreateStateSet();
state->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
state->setMode(GL_BLEND, osg::StateAttribute::ON);
state->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
}
osg::Vec3f CSVRender::Object::getMarkerPosition (float x, float y, float z, int axis) osg::Vec3f CSVRender::Object::getMarkerPosition (float x, float y, float z, int axis)
{ {
switch (axis) switch (axis)
@ -399,7 +417,7 @@ osg::Vec3f CSVRender::Object::getMarkerPosition (float x, float y, float z, int
CSVRender::Object::Object (CSMWorld::Data& data, osg::Group* parentNode, CSVRender::Object::Object (CSMWorld::Data& data, osg::Group* parentNode,
const std::string& id, bool referenceable, bool forceBaseToZero) const std::string& id, bool referenceable, bool forceBaseToZero)
: mData (data), mBaseNode(0), mSelected(false), mParentNode(parentNode), mResourceSystem(data.getResourceSystem().get()), mForceBaseToZero (forceBaseToZero), : mData (data), mBaseNode(0), mSelected(false), mParentNode(parentNode), mResourceSystem(data.getResourceSystem().get()), mForceBaseToZero (forceBaseToZero),
mScaleOverride (1), mOverrideFlags (0), mSubMode (-1) mScaleOverride (1), mOverrideFlags (0), mSubMode (-1), mMarkerTransparency(0.5f)
{ {
mRootNode = new osg::PositionAttitudeTransform; mRootNode = new osg::PositionAttitudeTransform;
@ -453,6 +471,7 @@ void CSVRender::Object::setSelected(bool selected)
else else
mRootNode->addChild(mBaseNode); mRootNode->addChild(mBaseNode);
mMarkerTransparency = CSMPrefs::get()["Rendering"]["object-marker-alpha"].toDouble();
updateMarker(); updateMarker();
} }
@ -629,6 +648,12 @@ void CSVRender::Object::setScale (float scale)
adjustTransform(); adjustTransform();
} }
void CSVRender::Object::setMarkerTransparency(float value)
{
mMarkerTransparency = value;
updateMarker();
}
void CSVRender::Object::apply (CSMWorld::CommandMacro& commands) void CSVRender::Object::apply (CSMWorld::CommandMacro& commands)
{ {
const CSMWorld::RefCollection& collection = mData.getReferences(); const CSMWorld::RefCollection& collection = mData.getReferences();

@ -4,6 +4,7 @@
#include <string> #include <string>
#include <osg/ref_ptr> #include <osg/ref_ptr>
#include <osg/Geometry>
#include <osg/Referenced> #include <osg/Referenced>
#include <components/esm/defs.hpp> #include <components/esm/defs.hpp>
@ -96,6 +97,7 @@ namespace CSVRender
int mOverrideFlags; int mOverrideFlags;
osg::ref_ptr<osg::Node> mMarker[3]; osg::ref_ptr<osg::Node> mMarker[3];
int mSubMode; int mSubMode;
float mMarkerTransparency;
/// Not implemented /// Not implemented
Object (const Object&); Object (const Object&);
@ -121,6 +123,9 @@ namespace CSVRender
osg::ref_ptr<osg::Node> makeMoveOrScaleMarker (int axis); osg::ref_ptr<osg::Node> makeMoveOrScaleMarker (int axis);
osg::ref_ptr<osg::Node> makeRotateMarker (int axis); osg::ref_ptr<osg::Node> makeRotateMarker (int axis);
/// Sets up a stateset with properties common to all marker types.
void setupCommonMarkerState(osg::ref_ptr<osg::Geometry> geometry);
osg::Vec3f getMarkerPosition (float x, float y, float z, int axis); osg::Vec3f getMarkerPosition (float x, float y, float z, int axis);
public: public:
@ -179,6 +184,8 @@ namespace CSVRender
/// Set override scale /// Set override scale
void setScale (float scale); void setScale (float scale);
void setMarkerTransparency(float value);
/// Apply override changes via command and end edit mode /// Apply override changes via command and end edit mode
void apply (CSMWorld::CommandMacro& commands); void apply (CSMWorld::CommandMacro& commands);

@ -56,6 +56,7 @@ RenderWidget::RenderWidget(QWidget *parent, Qt::WindowFlags f)
traits->vsync = false; traits->vsync = false;
mView = new osgViewer::View; mView = new osgViewer::View;
updateCameraParameters( traits->width / static_cast<double>(traits->height) );
osg::ref_ptr<osgQt::GraphicsWindowQt> window = new osgQt::GraphicsWindowQt(traits.get()); osg::ref_ptr<osgQt::GraphicsWindowQt> window = new osgQt::GraphicsWindowQt(traits.get());
QLayout* layout = new QHBoxLayout(this); QLayout* layout = new QHBoxLayout(this);
@ -66,7 +67,6 @@ RenderWidget::RenderWidget(QWidget *parent, Qt::WindowFlags f)
mView->getCamera()->setGraphicsContext(window); mView->getCamera()->setGraphicsContext(window);
mView->getCamera()->setClearColor( osg::Vec4(0.2, 0.2, 0.6, 1.0) ); mView->getCamera()->setClearColor( osg::Vec4(0.2, 0.2, 0.6, 1.0) );
mView->getCamera()->setViewport( new osg::Viewport(0, 0, traits->width, traits->height) ); mView->getCamera()->setViewport( new osg::Viewport(0, 0, traits->width, traits->height) );
mView->getCamera()->setProjectionMatrixAsPerspective(30.0f, static_cast<double>(traits->width)/static_cast<double>(traits->height), 1.0f, 10000.0f );
SceneUtil::LightManager* lightMgr = new SceneUtil::LightManager; SceneUtil::LightManager* lightMgr = new SceneUtil::LightManager;
lightMgr->setStartLight(1); lightMgr->setStartLight(1);
@ -188,6 +188,8 @@ SceneWidget::SceneWidget(std::shared_ptr<Resource::ResourceSystem> resourceSyste
mOrbitCamControl->setPickingMask(Mask_Reference | Mask_Terrain); mOrbitCamControl->setPickingMask(Mask_Reference | Mask_Terrain);
mOrbitCamControl->setConstRoll( CSMPrefs::get()["3D Scene Input"]["navi-orbit-const-roll"].isTrue() );
// we handle lighting manually // we handle lighting manually
mView->setLightingMode(osgViewer::View::NO_LIGHT); mView->setLightingMode(osgViewer::View::NO_LIGHT);
@ -370,6 +372,40 @@ void SceneWidget::settingChanged (const CSMPrefs::Setting *setting)
{ {
mOrbitCamControl->setOrbitSpeedMultiplier(setting->toDouble()); mOrbitCamControl->setOrbitSpeedMultiplier(setting->toDouble());
} }
else if (*setting=="3D Scene Input/navi-orbit-const-roll")
{
mOrbitCamControl->setConstRoll(setting->isTrue());
}
else if (*setting=="Rendering/camera-fov" ||
*setting=="Rendering/camera-ortho" ||
*setting=="Rendering/camera-ortho-size")
{
updateCameraParameters();
}
}
void RenderWidget::updateCameraParameters(double overrideAspect)
{
const float nearDist = 1.0;
const float farDist = 1000.0;
if (CSMPrefs::get()["Rendering"]["camera-ortho"].isTrue())
{
const float size = CSMPrefs::get()["Rendering"]["camera-ortho-size"].toInt();
const float aspect = overrideAspect >= 0.0 ? overrideAspect : (width() / static_cast<double>(height()));
const float halfH = size * 10.0;
const float halfW = halfH * aspect;
mView->getCamera()->setProjectionMatrixAsOrtho(
-halfW, halfW, -halfH, halfH, nearDist, farDist);
}
else
{
mView->getCamera()->setProjectionMatrixAsPerspective(
CSMPrefs::get()["Rendering"]["camera-fov"].toInt(),
static_cast<double>(width())/static_cast<double>(height()),
nearDist, farDist);
}
} }
void SceneWidget::selectNavigationMode (const std::string& mode) void SceneWidget::selectNavigationMode (const std::string& mode)

@ -64,6 +64,8 @@ namespace CSVRender
osg::ref_ptr<osgViewer::View> mView; osg::ref_ptr<osgViewer::View> mView;
osg::ref_ptr<osg::Group> mRootNode; osg::ref_ptr<osg::Group> mRootNode;
void updateCameraParameters(double overrideAspect = -1.0);
QTimer mTimer; QTimer mTimer;
protected slots: protected slots:

@ -51,6 +51,7 @@ CSVRender::WorldspaceWidget::WorldspaceWidget (CSMDoc::Document& document, QWidg
, mToolTipPos (-1, -1) , mToolTipPos (-1, -1)
, mShowToolTips(false) , mShowToolTips(false)
, mToolTipDelay(0) , mToolTipDelay(0)
, mInConstructor(true)
{ {
setAcceptDrops(true); setAcceptDrops(true);
@ -114,6 +115,8 @@ CSVRender::WorldspaceWidget::WorldspaceWidget (CSMDoc::Document& document, QWidg
CSMPrefs::Shortcut* abortShortcut = new CSMPrefs::Shortcut("scene-edit-abort", this); CSMPrefs::Shortcut* abortShortcut = new CSMPrefs::Shortcut("scene-edit-abort", this);
connect(abortShortcut, SIGNAL(activated()), this, SLOT(abortDrag())); connect(abortShortcut, SIGNAL(activated()), this, SLOT(abortDrag()));
mInConstructor = false;
} }
CSVRender::WorldspaceWidget::~WorldspaceWidget () CSVRender::WorldspaceWidget::~WorldspaceWidget ()
@ -128,6 +131,17 @@ void CSVRender::WorldspaceWidget::settingChanged (const CSMPrefs::Setting *setti
mDragWheelFactor = setting->toDouble(); mDragWheelFactor = setting->toDouble();
else if (*setting=="3D Scene Input/drag-shift-factor") else if (*setting=="3D Scene Input/drag-shift-factor")
mDragShiftFactor = setting->toDouble(); mDragShiftFactor = setting->toDouble();
else if (*setting=="Rendering/object-marker-alpha" && !mInConstructor)
{
float alpha = setting->toDouble();
// getSelection is virtual, thus this can not be called from the constructor
auto selection = getSelection(Mask_Reference);
for (osg::ref_ptr<TagBase> tag : selection)
{
if (auto objTag = dynamic_cast<ObjectTag*>(tag.get()))
objTag->mObject->setMarkerTransparency(alpha);
}
}
else if (*setting=="Tooltips/scene-delay") else if (*setting=="Tooltips/scene-delay")
mToolTipDelay = setting->toInt(); mToolTipDelay = setting->toInt();
else if (*setting=="Tooltips/scene") else if (*setting=="Tooltips/scene")

@ -65,6 +65,7 @@ namespace CSVRender
QPoint mToolTipPos; QPoint mToolTipPos;
bool mShowToolTips; bool mShowToolTips;
int mToolTipDelay; int mToolTipDelay;
bool mInConstructor;
public: public:

@ -159,9 +159,8 @@ if (ANDROID)
dl dl
z z
${OPENSCENEGRAPH_LIBRARIES} ${OPENSCENEGRAPH_LIBRARIES}
${OSG_PLUGINS} freetype
jpeg jpeg
gif
png png
) )
endif (ANDROID) endif (ANDROID)
@ -190,6 +189,8 @@ if(APPLE)
configure_file("${OpenMW_BINARY_DIR}/settings-default.cfg" ${BUNDLE_RESOURCES_DIR} COPYONLY) configure_file("${OpenMW_BINARY_DIR}/settings-default.cfg" ${BUNDLE_RESOURCES_DIR} COPYONLY)
configure_file("${OpenMW_BINARY_DIR}/openmw.cfg" ${BUNDLE_RESOURCES_DIR} COPYONLY) configure_file("${OpenMW_BINARY_DIR}/openmw.cfg" ${BUNDLE_RESOURCES_DIR} COPYONLY)
configure_file("${OpenMW_BINARY_DIR}/gamecontrollerdb.txt" ${BUNDLE_RESOURCES_DIR} COPYONLY) configure_file("${OpenMW_BINARY_DIR}/gamecontrollerdb.txt" ${BUNDLE_RESOURCES_DIR} COPYONLY)
configure_file("${OpenMW_BINARY_DIR}/gamecontrollerdb_204.txt" ${BUNDLE_RESOURCES_DIR} COPYONLY)
configure_file("${OpenMW_BINARY_DIR}/gamecontrollerdb_205.txt" ${BUNDLE_RESOURCES_DIR} COPYONLY)
add_custom_command(TARGET openmw add_custom_command(TARGET openmw
POST_BUILD POST_BUILD

@ -1,6 +1,9 @@
int stderr = 0; // Hack: fix linker error
#ifdef __ANDROID__
#include "SDL_main.h" #include "SDL_main.h"
#include <SDL_gamecontroller.h>
#include <SDL_mouse.h>
#include <SDL_events.h>
/******************************************************************************* /*******************************************************************************
Functions called by JNI Functions called by JNI
@ -14,13 +17,37 @@ extern int argcData;
extern const char **argvData; extern const char **argvData;
void releaseArgv(); void releaseArgv();
int Java_org_libsdl_app_SDLActivity_getMouseX(JNIEnv *env, jclass cls, jobject obj) {
int ret = 0;
SDL_GetMouseState(&ret, NULL);
return ret;
}
int Java_org_libsdl_app_SDLActivity_getMouseY(JNIEnv *env, jclass cls, jobject obj) {
int ret = 0;
SDL_GetMouseState(NULL, &ret);
return ret;
}
int Java_org_libsdl_app_SDLActivity_isMouseShown(JNIEnv *env, jclass cls, jobject obj) {
return SDL_ShowCursor(SDL_QUERY);
}
int Java_org_libsdl_app_SDLActivity_nativeInit(JNIEnv* env, jclass cls, int Java_org_libsdl_app_SDLActivity_nativeInit(JNIEnv* env, jclass cls,
jobject obj) { jobject obj) {
setenv("OPENMW_DECOMPRESS_TEXTURES", "1", 1);
SDL_Android_Init(env, cls); SDL_Android_Init(env, cls);
SDL_SetMainReady(); SDL_SetMainReady();
// On Android, we use a virtual controller with guid="Virtual"
SDL_GameControllerAddMapping("5669727475616c000000000000000000,Virtual,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b16,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4");
/* Run the application code! */ /* Run the application code! */
int status; int status;
@ -33,5 +60,3 @@ int Java_org_libsdl_app_SDLActivity_nativeInit(JNIEnv* env, jclass cls,
return status; return status;
} }
#endif /* __ANDROID__ */

@ -79,12 +79,14 @@ void OMW::Engine::executeLocalScripts()
} }
} }
void OMW::Engine::frame(float frametime) bool OMW::Engine::frame(float frametime)
{ {
try try
{ {
mStartTick = mViewer->getStartTick(); mStartTick = mViewer->getStartTick();
mEnvironment.setFrameDuration(frametime);
// update input // update input
mEnvironment.getInputManager()->update(frametime, false); mEnvironment.getInputManager()->update(frametime, false);
@ -92,7 +94,7 @@ void OMW::Engine::frame(float frametime)
// If we are not currently rendering, then RenderItems will not be reused resulting in a memory leak upon changing widget textures (fixed in MyGUI 3.3.2), // If we are not currently rendering, then RenderItems will not be reused resulting in a memory leak upon changing widget textures (fixed in MyGUI 3.3.2),
// and destroyed widgets will not be deleted (not fixed yet, https://github.com/MyGUI/mygui/issues/21) // and destroyed widgets will not be deleted (not fixed yet, https://github.com/MyGUI/mygui/issues/21)
if (!mEnvironment.getInputManager()->isWindowVisible()) if (!mEnvironment.getInputManager()->isWindowVisible())
return; return false;
// sound // sound
if (mUseSound) if (mUseSound)
@ -187,8 +189,9 @@ void OMW::Engine::frame(float frametime)
} }
catch (const std::exception& e) catch (const std::exception& e)
{ {
std::cerr << "Error in framelistener: " << e.what() << std::endl; std::cerr << "Error in frame: " << e.what() << std::endl;
} }
return true;
} }
OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) OMW::Engine::Engine(Files::ConfigurationManager& configurationManager)
@ -474,8 +477,20 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings)
} }
// find correct path to the game controller bindings // find correct path to the game controller bindings
const std::string localdefault = mCfgMgr.getLocalPath().string() + "/gamecontrollerdb.txt"; // File format for controller mappings is different for SDL <= 2.0.4, 2.0.5, and >= 2.0.6
const std::string globaldefault = mCfgMgr.getGlobalPath().string() + "/gamecontrollerdb.txt"; SDL_version linkedSdlVersion;
SDL_GetVersion(&linkedSdlVersion);
std::string controllerFileName;
if (linkedSdlVersion.major == 2 && linkedSdlVersion.minor == 0 && linkedSdlVersion.patch <= 4) {
controllerFileName = "gamecontrollerdb_204.txt";
} else if (linkedSdlVersion.major == 2 && linkedSdlVersion.minor == 0 && linkedSdlVersion.patch == 5) {
controllerFileName = "gamecontrollerdb_205.txt";
} else {
controllerFileName = "gamecontrollerdb.txt";
}
const std::string localdefault = mCfgMgr.getLocalPath().string() + "/" + controllerFileName;
const std::string globaldefault = mCfgMgr.getGlobalPath().string() + "/" + controllerFileName;
std::string gameControllerdb; std::string gameControllerdb;
if (boost::filesystem::exists(localdefault)) if (boost::filesystem::exists(localdefault))
gameControllerdb = localdefault; gameControllerdb = localdefault;
@ -689,17 +704,9 @@ void OMW::Engine::go()
frameTimer.setStartTick(); frameTimer.setStartTick();
dt = std::min(dt, 0.2); dt = std::min(dt, 0.2);
bool guiActive = mEnvironment.getWindowManager()->isGuiMode();
if (!guiActive)
simulationTime += dt;
mViewer->advance(simulationTime); mViewer->advance(simulationTime);
mEnvironment.setFrameDuration(dt); if (!frame(dt))
frame(dt);
if (!mEnvironment.getInputManager()->isWindowVisible())
{ {
OpenThreads::Thread::microSleep(5000); OpenThreads::Thread::microSleep(5000);
continue; continue;
@ -712,6 +719,10 @@ void OMW::Engine::go()
mEnvironment.getWorld()->updateWindowManager(); mEnvironment.getWorld()->updateWindowManager();
mViewer->renderingTraversals(); mViewer->renderingTraversals();
bool guiActive = mEnvironment.getWindowManager()->isGuiMode();
if (!guiActive)
simulationTime += dt;
} }
mEnvironment.limitFrameRate(frameTimer.time_s()); mEnvironment.limitFrameRate(frameTimer.time_s());

@ -119,7 +119,7 @@ namespace OMW
void executeLocalScripts(); void executeLocalScripts();
void frame (float dt); bool frame (float dt);
/// Load settings from various files, returns the path to the user settings file /// Load settings from various files, returns the path to the user settings file
std::string loadSettings (Settings::Manager & settings); std::string loadSettings (Settings::Manager & settings);

@ -290,7 +290,11 @@ private:
}; };
#endif #endif
#ifdef ANDROID
extern "C" int SDL_main(int argc, char**argv)
#else
int main(int argc, char**argv) int main(int argc, char**argv)
#endif
{ {
#if defined(__APPLE__) #if defined(__APPLE__)
setenv("OSG_GL_TEXTURE_STORAGE", "OFF", 0); setenv("OSG_GL_TEXTURE_STORAGE", "OFF", 0);

@ -140,7 +140,7 @@ namespace MWBase
/// Utility to check if taking this item is illegal and calling commitCrime if so /// Utility to check if taking this item is illegal and calling commitCrime if so
/// @param container The container the item is in; may be empty for an item in the world /// @param container The container the item is in; may be empty for an item in the world
virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container, virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container,
int count) = 0; int count, bool alarm = true) = 0;
/// Utility to check if opening (i.e. unlocking) this object is illegal and calling commitCrime if so /// Utility to check if opening (i.e. unlocking) this object is illegal and calling commitCrime if so
virtual void objectOpened (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item) = 0; virtual void objectOpened (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item) = 0;
/// Attempt sleeping in a bed. If this is illegal, call commitCrime. /// Attempt sleeping in a bed. If this is illegal, call commitCrime.
@ -191,6 +191,9 @@ namespace MWBase
virtual void getObjectsInRange (const osg::Vec3f& position, float radius, std::vector<MWWorld::Ptr>& objects) = 0; virtual void getObjectsInRange (const osg::Vec3f& position, float radius, std::vector<MWWorld::Ptr>& objects) = 0;
virtual void getActorsInRange(const osg::Vec3f &position, float radius, std::vector<MWWorld::Ptr> &objects) = 0; virtual void getActorsInRange(const osg::Vec3f &position, float radius, std::vector<MWWorld::Ptr> &objects) = 0;
/// Check if there are actors in selected range
virtual bool isAnyActorInRange(const osg::Vec3f &position, float radius) = 0;
///Returns the list of actors which are siding with the given actor in fights ///Returns the list of actors which are siding with the given actor in fights
/**ie AiFollow or AiEscort is active and the target is the actor **/ /**ie AiFollow or AiEscort is active and the target is the actor **/
virtual std::list<MWWorld::Ptr> getActorsSidingWith(const MWWorld::Ptr& actor) = 0; virtual std::list<MWWorld::Ptr> getActorsSidingWith(const MWWorld::Ptr& actor) = 0;
@ -238,6 +241,7 @@ namespace MWBase
/// Has the player stolen this item from the given owner? /// Has the player stolen this item from the given owner?
virtual bool isItemStolenFrom(const std::string& itemid, const std::string& ownerid) = 0; virtual bool isItemStolenFrom(const std::string& itemid, const std::string& ownerid) = 0;
virtual bool isBoundItem(const MWWorld::Ptr& item) = 0;
virtual bool isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) = 0; virtual bool isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) = 0;
/// Turn actor into werewolf or normal form. /// Turn actor into werewolf or normal form.
@ -250,7 +254,6 @@ namespace MWBase
virtual void cleanupSummonedCreature(const MWWorld::Ptr& caster, int creatureActorId) = 0; virtual void cleanupSummonedCreature(const MWWorld::Ptr& caster, int creatureActorId) = 0;
virtual void confiscateStolenItemToOwner(const MWWorld::Ptr &player, const MWWorld::Ptr &item, const MWWorld::Ptr& victim, int count) = 0; virtual void confiscateStolenItemToOwner(const MWWorld::Ptr &player, const MWWorld::Ptr &item, const MWWorld::Ptr& victim, int count) = 0;
virtual bool isAttackPrepairing(const MWWorld::Ptr& ptr) = 0; virtual bool isAttackPrepairing(const MWWorld::Ptr& ptr) = 0;
virtual bool isRunning(const MWWorld::Ptr& ptr) = 0; virtual bool isRunning(const MWWorld::Ptr& ptr) = 0;
virtual bool isSneaking(const MWWorld::Ptr& ptr) = 0; virtual bool isSneaking(const MWWorld::Ptr& ptr) = 0;

@ -1129,6 +1129,15 @@ namespace MWClass
else else
{ {
ratings[i] = it->getClass().getEffectiveArmorRating(*it, ptr); ratings[i] = it->getClass().getEffectiveArmorRating(*it, ptr);
// Take in account armor condition
const bool hasHealth = it->getClass().hasItemHealth(*it);
if (hasHealth)
{
int armorHealth = it->getClass().getItemHealth(*it);
int armorMaxHealth = it->getClass().getItemMaxHealth(*it);
ratings[i] *= (float(armorHealth) / armorMaxHealth);
}
} }
} }

@ -77,6 +77,7 @@ namespace MWDialogue
void DialogueManager::parseText (const std::string& text) void DialogueManager::parseText (const std::string& text)
{ {
updateActorKnownTopics();
std::vector<HyperTextParser::Token> hypertext = HyperTextParser::parseHyperText(text); std::vector<HyperTextParser::Token> hypertext = HyperTextParser::parseHyperText(text);
for (std::vector<HyperTextParser::Token>::iterator tok = hypertext.begin(); tok != hypertext.end(); ++tok) for (std::vector<HyperTextParser::Token>::iterator tok = hypertext.begin(); tok != hypertext.end(); ++tok)
@ -145,18 +146,12 @@ namespace MWDialogue
// TODO play sound // TODO play sound
} }
// first topics update so that parseText knows the keywords to highlight
updateActorKnownTopics();
parseText (info->mResponse);
MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(),mActor); MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(),mActor);
callback->addResponse("", Interpreter::fixDefinesDialog(info->mResponse, interpreterContext)); callback->addResponse("", Interpreter::fixDefinesDialog(info->mResponse, interpreterContext));
executeScript (info->mResultScript, mActor); executeScript (info->mResultScript, mActor);
mLastTopic = it->mId; mLastTopic = it->mId;
// update topics again to accommodate changes resulting from executeScript parseText (info->mResponse);
updateActorKnownTopics();
return true; return true;
} }
@ -252,8 +247,6 @@ namespace MWDialogue
const ESM::DialInfo* info = filter.search(dialogue, true); const ESM::DialInfo* info = filter.search(dialogue, true);
if (info) if (info)
{ {
parseText (info->mResponse);
std::string title; std::string title;
if (dialogue.mType==ESM::Dialogue::Persuasion) if (dialogue.mType==ESM::Dialogue::Persuasion)
{ {
@ -292,6 +285,8 @@ namespace MWDialogue
executeScript (info->mResultScript, mActor); executeScript (info->mResultScript, mActor);
parseText (info->mResponse);
mLastTopic = topic; mLastTopic = topic;
} }
} }
@ -391,7 +386,7 @@ namespace MWDialogue
{ {
Filter filter (mActor, mChoice, mTalkedTo); Filter filter (mActor, mChoice, mTalkedTo);
if (dialogue->mType == ESM::Dialogue::Topic || dialogue->mType == ESM::Dialogue::Greeting) if (dialogue->mType == ESM::Dialogue::Topic || dialogue->mType == ESM::Dialogue::Greeting)
{ {
if (const ESM::DialInfo *info = filter.search (*dialogue, true)) if (const ESM::DialInfo *info = filter.search (*dialogue, true))
{ {
@ -405,15 +400,18 @@ namespace MWDialogue
MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(),mActor); MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(),mActor);
callback->addResponse("", Interpreter::fixDefinesDialog(text, interpreterContext)); callback->addResponse("", Interpreter::fixDefinesDialog(text, interpreterContext));
// Make sure the returned DialInfo is from the Dialogue we supplied. If could also be from the Info refusal group, if (dialogue->mType == ESM::Dialogue::Topic)
// in which case it should not be added to the journal.
for (ESM::Dialogue::InfoContainer::const_iterator iter = dialogue->mInfo.begin();
iter!=dialogue->mInfo.end(); ++iter)
{ {
if (iter->mId == info->mId) // Make sure the returned DialInfo is from the Dialogue we supplied. If could also be from the Info refusal group,
// in which case it should not be added to the journal
for (ESM::Dialogue::InfoContainer::const_iterator iter = dialogue->mInfo.begin();
iter!=dialogue->mInfo.end(); ++iter)
{ {
MWBase::Environment::get().getJournal()->addTopic (Misc::StringUtils::lowerCase(mLastTopic), info->mId, mActor); if (iter->mId == info->mId)
break; {
MWBase::Environment::get().getJournal()->addTopic (Misc::StringUtils::lowerCase(mLastTopic), info->mId, mActor);
break;
}
} }
} }

@ -83,8 +83,10 @@ namespace MWDialogue
if (i->mTopic == id && i->mInfoId == infoId) if (i->mTopic == id && i->mInfoId == infoId)
{ {
if (getJournalIndex(id) < index) if (getJournalIndex(id) < index)
{
setJournalIndex(id, index); setJournalIndex(id, index);
MWBase::Environment::get().getWindowManager()->messageBox ("#{sJournalEntry}"); MWBase::Environment::get().getWindowManager()->messageBox ("#{sJournalEntry}");
}
return; return;
} }

@ -8,7 +8,9 @@
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
#include "../mwworld/player.hpp"
#include "widgets.hpp" #include "widgets.hpp"
@ -70,8 +72,14 @@ namespace MWGui
updateBirths(); updateBirths();
updateSpells(); updateSpells();
MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mBirthList); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mBirthList);
}
// Show the current birthsign by default
const std::string &signId =
MWBase::Environment::get().getWorld()->getPlayer().getBirthSign();
if (!signId.empty())
setBirthId(signId);
}
void BirthDialog::setBirthId(const std::string &birthId) void BirthDialog::setBirthId(const std::string &birthId)
{ {

@ -16,6 +16,7 @@ class BookPageImpl;
static bool ucsSpace (int codePoint); static bool ucsSpace (int codePoint);
static bool ucsLineBreak (int codePoint); static bool ucsLineBreak (int codePoint);
static bool ucsCarriageReturn (int codePoint);
static bool ucsBreakingSpace (int codePoint); static bool ucsBreakingSpace (int codePoint);
struct BookTypesetter::Style { virtual ~Style () {} }; struct BookTypesetter::Style { virtual ~Style () {} };
@ -280,7 +281,7 @@ struct TypesetBookImpl::Typesetter : BookTypesetter
style.mActiveColour = fontColour; style.mActiveColour = fontColour;
style.mNormalColour = fontColour; style.mNormalColour = fontColour;
style.mInteractiveId = 0; style.mInteractiveId = 0;
return &style; return &style;
} }
@ -342,7 +343,7 @@ struct TypesetBookImpl::Typesetter : BookTypesetter
writeImpl (static_cast <StyleImpl*> (style), begin_, end_); writeImpl (static_cast <StyleImpl*> (style), begin_, end_);
} }
void lineBreak (float margin) void lineBreak (float margin)
{ {
assert (margin == 0); //TODO: figure out proper behavior here... assert (margin == 0); //TODO: figure out proper behavior here...
@ -352,7 +353,7 @@ struct TypesetBookImpl::Typesetter : BookTypesetter
mRun = NULL; mRun = NULL;
mLine = NULL; mLine = NULL;
} }
void sectionBreak (int margin) void sectionBreak (int margin)
{ {
add_partial_text(); add_partial_text();
@ -1188,6 +1189,9 @@ public:
{ {
Utf8Stream::UnicodeChar code_point = stream.consume (); Utf8Stream::UnicodeChar code_point = stream.consume ();
if (ucsCarriageReturn (code_point))
continue;
if (!ucsSpace (code_point)) if (!ucsSpace (code_point))
glyphStream.emitGlyph (code_point); glyphStream.emitGlyph (code_point);
else else
@ -1331,6 +1335,11 @@ static bool ucsLineBreak (int codePoint)
return codePoint == '\n'; return codePoint == '\n';
} }
static bool ucsCarriageReturn (int codePoint)
{
return codePoint == '\r';
}
static bool ucsSpace (int codePoint) static bool ucsSpace (int codePoint)
{ {
switch (codePoint) switch (codePoint)

@ -7,6 +7,9 @@
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwmechanics/actorutil.hpp"
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
#include "tooltips.hpp" #include "tooltips.hpp"
@ -131,8 +134,16 @@ namespace MWGui
updateClasses(); updateClasses();
updateStats(); updateStats();
MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mClassList); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mClassList);
}
// Show the current class by default
MWWorld::Ptr player = MWMechanics::getPlayer();
const std::string &classId =
player.get<ESM::NPC>()->mBase->mClass;
if (!classId.empty())
setClassId(classId);
}
void PickClassDialog::setClassId(const std::string &classId) void PickClassDialog::setClassId(const std::string &classId)
{ {

@ -1,5 +1,7 @@
#include "companionwindow.hpp" #include "companionwindow.hpp"
#include <cmath>
#include <MyGUI_InputManager.h> #include <MyGUI_InputManager.h>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
@ -129,7 +131,7 @@ void CompanionWindow::updateEncumbranceBar()
return; return;
float capacity = mPtr.getClass().getCapacity(mPtr); float capacity = mPtr.getClass().getCapacity(mPtr);
float encumbrance = mPtr.getClass().getEncumbrance(mPtr); float encumbrance = mPtr.getClass().getEncumbrance(mPtr);
mEncumbranceBar->setValue(static_cast<int>(encumbrance), static_cast<int>(capacity)); mEncumbranceBar->setValue(std::ceil(encumbrance), static_cast<int>(capacity));
if (mModel && mModel->hasProfit(mPtr)) if (mModel && mModel->hasProfit(mPtr))
{ {

@ -8,12 +8,10 @@
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/dialoguemanager.hpp"
#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/mechanicsmanager.hpp"
#include "../mwmechanics/actorutil.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/inventorystore.hpp" #include "../mwworld/inventorystore.hpp"
#include "../mwmechanics/pickpocket.hpp"
#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/creaturestats.hpp"
#include "countdialog.hpp" #include "countdialog.hpp"
@ -33,7 +31,6 @@ namespace MWGui
ContainerWindow::ContainerWindow(DragAndDrop* dragAndDrop) ContainerWindow::ContainerWindow(DragAndDrop* dragAndDrop)
: WindowBase("openmw_container_window.layout") : WindowBase("openmw_container_window.layout")
, mDragAndDrop(dragAndDrop) , mDragAndDrop(dragAndDrop)
, mPickpocketDetected(false)
, mSortModel(NULL) , mSortModel(NULL)
, mModel(NULL) , mModel(NULL)
, mSelectedItem(-1) , mSelectedItem(-1)
@ -55,10 +52,9 @@ namespace MWGui
void ContainerWindow::onItemSelected(int index) void ContainerWindow::onItemSelected(int index)
{ {
if (mDragAndDrop->mIsOnDragAndDrop) if (mDragAndDrop->mIsOnDragAndDrop && mModel)
{ {
if (mModel && mModel->allowedToInsertItems()) dropItem();
dropItem();
return; return;
} }
@ -100,39 +96,20 @@ namespace MWGui
void ContainerWindow::dropItem() void ContainerWindow::dropItem()
{ {
if (mPtr.getTypeName() == typeid(ESM::Container).name()) bool success = mModel->onDropItem(mDragAndDrop->mItem.mBase, mDragAndDrop->mDraggedCount);
{
// check container organic flag
MWWorld::LiveCellRef<ESM::Container>* ref = mPtr.get<ESM::Container>();
if (ref->mBase->mFlags & ESM::Container::Organic)
{
MWBase::Environment::get().getWindowManager()->
messageBox("#{sContentsMessage2}");
return;
}
// check that we don't exceed container capacity if (success)
MWWorld::Ptr item = mDragAndDrop->mItem.mBase; mDragAndDrop->drop(mModel, mItemView);
float weight = item.getClass().getWeight(item) * mDragAndDrop->mDraggedCount;
if (mPtr.getClass().getCapacity(mPtr) < mPtr.getClass().getEncumbrance(mPtr) + weight)
{
MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage3}");
return;
}
}
mDragAndDrop->drop(mModel, mItemView);
} }
void ContainerWindow::onBackgroundSelected() void ContainerWindow::onBackgroundSelected()
{ {
if (mDragAndDrop->mIsOnDragAndDrop && mModel && mModel->allowedToInsertItems()) if (mDragAndDrop->mIsOnDragAndDrop && mModel)
dropItem(); dropItem();
} }
void ContainerWindow::setPtr(const MWWorld::Ptr& container) void ContainerWindow::setPtr(const MWWorld::Ptr& container)
{ {
mPickpocketDetected = false;
mPtr = container; mPtr = container;
bool loot = mPtr.getClass().isActor() && mPtr.getClass().getCreatureStats(mPtr).isDead(); bool loot = mPtr.getClass().isActor() && mPtr.getClass().getCreatureStats(mPtr).isDead();
@ -142,8 +119,7 @@ namespace MWGui
if (mPtr.getClass().isNpc() && !loot) if (mPtr.getClass().isNpc() && !loot)
{ {
// we are stealing stuff // we are stealing stuff
MWWorld::Ptr player = MWMechanics::getPlayer(); mModel = new PickpocketItemModel(mPtr, new InventoryItemModel(container),
mModel = new PickpocketItemModel(player, new InventoryItemModel(container),
!mPtr.getClass().getCreatureStats(mPtr).getKnockedDown()); !mPtr.getClass().getCreatureStats(mPtr).getKnockedDown());
} }
else else
@ -178,24 +154,8 @@ namespace MWGui
{ {
WindowBase::onClose(); WindowBase::onClose();
if (dynamic_cast<PickpocketItemModel*>(mModel) if (mModel)
// Make sure we were actually closed, rather than just temporarily hidden (e.g. console or main menu opened) mModel->onClose();
&& !MWBase::Environment::get().getWindowManager()->containsMode(GM_Container)
// If it was already detected while taking an item, no need to check now
&& !mPickpocketDetected
)
{
MWWorld::Ptr player = MWMechanics::getPlayer();
MWMechanics::Pickpocket pickpocket(player, mPtr);
if (pickpocket.finish())
{
MWBase::Environment::get().getMechanicsManager()->commitCrime(
player, mPtr, MWBase::MechanicsManager::OT_Pickpocket, 0, true);
MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container);
mPickpocketDetected = true;
return;
}
}
} }
void ContainerWindow::onCloseButtonClicked(MyGUI::Widget* _sender) void ContainerWindow::onCloseButtonClicked(MyGUI::Widget* _sender)
@ -271,32 +231,7 @@ namespace MWGui
bool ContainerWindow::onTakeItem(const ItemStack &item, int count) bool ContainerWindow::onTakeItem(const ItemStack &item, int count)
{ {
MWWorld::Ptr player = MWMechanics::getPlayer(); return mModel->onTakeItem(item.mBase, count);
// TODO: move to ItemModels
if (dynamic_cast<PickpocketItemModel*>(mModel)
&& !mPtr.getClass().getCreatureStats(mPtr).getKnockedDown())
{
MWMechanics::Pickpocket pickpocket(player, mPtr);
if (pickpocket.pick(item.mBase, count))
{
MWBase::Environment::get().getMechanicsManager()->commitCrime(
player, mPtr, MWBase::MechanicsManager::OT_Pickpocket, 0, true);
MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container);
mPickpocketDetected = true;
return false;
}
else
player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, 1);
}
else
{
// Looting a dead corpse is considered OK
if (mPtr.getClass().isActor() && mPtr.getClass().getCreatureStats(mPtr).isDead())
return true;
else
MWBase::Environment::get().getMechanicsManager()->itemTaken(player, item.mBase, mPtr, count);
}
return true;
} }
} }

@ -44,8 +44,6 @@ namespace MWGui
private: private:
DragAndDrop* mDragAndDrop; DragAndDrop* mDragAndDrop;
bool mPickpocketDetected;
MWGui::ItemView* mItemView; MWGui::ItemView* mItemView;
SortFilterItemModel* mSortModel; SortFilterItemModel* mSortModel;
ItemModel* mModel; ItemModel* mModel;

@ -2,12 +2,16 @@
#include <algorithm> #include <algorithm>
#include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/actorutil.hpp"
#include "../mwworld/containerstore.hpp" #include "../mwworld/containerstore.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwbase/world.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/actorutil.hpp"
@ -185,5 +189,51 @@ void ContainerItemModel::update()
} }
} }
} }
bool ContainerItemModel::onDropItem(const MWWorld::Ptr &item, int count)
{
if (mItemSources.empty())
return false;
MWWorld::Ptr target = mItemSources[0];
if (target.getTypeName() != typeid(ESM::Container).name())
return true;
// check container organic flag
MWWorld::LiveCellRef<ESM::Container>* ref = target.get<ESM::Container>();
if (ref->mBase->mFlags & ESM::Container::Organic)
{
MWBase::Environment::get().getWindowManager()->
messageBox("#{sContentsMessage2}");
return false;
}
// check that we don't exceed container capacity
float weight = item.getClass().getWeight(item) * count;
if (target.getClass().getCapacity(target) < target.getClass().getEncumbrance(target) + weight)
{
MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage3}");
return false;
}
return true;
}
bool ContainerItemModel::onTakeItem(const MWWorld::Ptr &item, int count)
{
if (mItemSources.empty())
return false;
MWWorld::Ptr target = mItemSources[0];
// Looting a dead corpse is considered OK
if (target.getClass().isActor() && target.getClass().getCreatureStats(target).isDead())
return true;
MWWorld::Ptr player = MWMechanics::getPlayer();
MWBase::Environment::get().getMechanicsManager()->itemTaken(player, item, target, count);
return true;
}
} }

@ -18,6 +18,10 @@ namespace MWGui
ContainerItemModel (const MWWorld::Ptr& source); ContainerItemModel (const MWWorld::Ptr& source);
virtual bool allowedToUseItems() const; virtual bool allowedToUseItems() const;
virtual bool onDropItem(const MWWorld::Ptr &item, int count);
virtual bool onTakeItem(const MWWorld::Ptr &item, int count);
virtual ItemStack getItem (ModelIndex index); virtual ItemStack getItem (ModelIndex index);
virtual ModelIndex getIndex (ItemStack item); virtual ModelIndex getIndex (ItemStack item);
virtual size_t getItemCount(); virtual size_t getItemCount();

@ -45,6 +45,11 @@ namespace MWGui
mWindow->addResponse(title, text, mNeedMargin); mWindow->addResponse(title, text, mNeedMargin);
} }
void updateTopics()
{
mWindow->updateTopics();
}
private: private:
DialogueWindow* mWindow; DialogueWindow* mWindow;
bool mNeedMargin; bool mNeedMargin;
@ -91,6 +96,7 @@ namespace MWGui
type = MWBase::MechanicsManager::PT_Bribe1000; type = MWBase::MechanicsManager::PT_Bribe1000;
MWBase::Environment::get().getDialogueManager()->persuade(type, mCallback.get()); MWBase::Environment::get().getDialogueManager()->persuade(type, mCallback.get());
mCallback->updateTopics();
setVisible(false); setVisible(false);
} }
@ -395,6 +401,8 @@ namespace MWGui
else if (topic == gmst.find("sRepair")->getString()) else if (topic == gmst.find("sRepair")->getString())
MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_MerchantRepair, mPtr); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_MerchantRepair, mPtr);
} }
else
updateTopics();
} }
} }
@ -432,7 +440,9 @@ namespace MWGui
setTitle(mPtr.getClass().getName(mPtr)); setTitle(mPtr.getClass().getName(mPtr));
updateTopicsPane(); updateTopics();
updateTopicsPane(); // force update for new services
updateDisposition(); updateDisposition();
restock(); restock();
} }
@ -620,11 +630,13 @@ namespace MWGui
void DialogueWindow::onTopicActivated(const std::string &topicId) void DialogueWindow::onTopicActivated(const std::string &topicId)
{ {
MWBase::Environment::get().getDialogueManager()->keywordSelected(topicId, mCallback.get()); MWBase::Environment::get().getDialogueManager()->keywordSelected(topicId, mCallback.get());
updateTopics();
} }
void DialogueWindow::onChoiceActivated(int id) void DialogueWindow::onChoiceActivated(int id)
{ {
MWBase::Environment::get().getDialogueManager()->questionAnswered(id, mCallback.get()); MWBase::Environment::get().getDialogueManager()->questionAnswered(id, mCallback.get());
updateTopics();
} }
void DialogueWindow::onGoodbyeActivated() void DialogueWindow::onGoodbyeActivated()

@ -131,8 +131,9 @@ namespace MWGui
void onFrame(float dt); void onFrame(float dt);
void clear() { resetReference(); } void clear() { resetReference(); }
protected:
void updateTopics(); void updateTopics();
protected:
void updateTopicsPane(); void updateTopicsPane();
bool isCompanion(const MWWorld::Ptr& actor); bool isCompanion(const MWWorld::Ptr& actor);
bool isCompanion(); bool isCompanion();

@ -9,6 +9,9 @@
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/inventorystore.hpp" #include "../mwworld/inventorystore.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"
namespace MWGui namespace MWGui
{ {
@ -116,4 +119,16 @@ void InventoryItemModel::update()
} }
} }
bool InventoryItemModel::onTakeItem(const MWWorld::Ptr &item, int count)
{
// Looting a dead corpse is considered OK
if (mActor.getClass().isActor() && mActor.getClass().getCreatureStats(mActor).isDead())
return true;
MWWorld::Ptr player = MWMechanics::getPlayer();
MWBase::Environment::get().getMechanicsManager()->itemTaken(player, item, mActor, count);
return true;
}
} }

@ -15,6 +15,8 @@ namespace MWGui
virtual ModelIndex getIndex (ItemStack item); virtual ModelIndex getIndex (ItemStack item);
virtual size_t getItemCount(); virtual size_t getItemCount();
virtual bool onTakeItem(const MWWorld::Ptr &item, int count);
virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool setNewOwner=false); virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool setNewOwner=false);
virtual void removeItem (const ItemStack& item, size_t count); virtual void removeItem (const ItemStack& item, size_t count);

@ -1,5 +1,6 @@
#include "inventorywindow.hpp" #include "inventorywindow.hpp"
#include <cmath>
#include <stdexcept> #include <stdexcept>
#include <MyGUI_Window.h> #include <MyGUI_Window.h>
@ -598,7 +599,7 @@ namespace MWGui
float capacity = player.getClass().getCapacity(player); float capacity = player.getClass().getCapacity(player);
float encumbrance = player.getClass().getEncumbrance(player); float encumbrance = player.getClass().getEncumbrance(player);
mTradeModel->adjustEncumbrance(encumbrance); mTradeModel->adjustEncumbrance(encumbrance);
mEncumbranceBar->setValue(static_cast<int>(encumbrance), static_cast<int>(capacity)); mEncumbranceBar->setValue(std::ceil(encumbrance), static_cast<int>(capacity));
} }
void InventoryWindow::onFrame(float dt) void InventoryWindow::onFrame(float dt)
@ -657,12 +658,16 @@ namespace MWGui
MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::Ptr player = MWMechanics::getPlayer();
MWBase::Environment::get().getWorld()->breakInvisibility(player); MWBase::Environment::get().getWorld()->breakInvisibility(player);
if (!object.getRefData().activate())
return;
MWBase::Environment::get().getMechanicsManager()->itemTaken(player, object, MWWorld::Ptr(), count); MWBase::Environment::get().getMechanicsManager()->itemTaken(player, object, MWWorld::Ptr(), count);
// add to player inventory // add to player inventory
// can't use ActionTake here because we need an MWWorld::Ptr to the newly inserted object // can't use ActionTake here because we need an MWWorld::Ptr to the newly inserted object
MWWorld::Ptr newObject = *player.getClass().getContainerStore (player).add (object, object.getRefData().getCount(), player); MWWorld::Ptr newObject = *player.getClass().getContainerStore (player).add (object, object.getRefData().getCount(), player);
// remove from world // remove from world
MWBase::Environment::get().getWorld()->deleteObject (object); MWBase::Environment::get().getWorld()->deleteObject (object);

@ -9,6 +9,7 @@
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"
namespace MWGui namespace MWGui
{ {
@ -23,38 +24,7 @@ namespace MWGui
if (base.getClass().getEnchantment(base) != "") if (base.getClass().getEnchantment(base) != "")
mFlags |= Flag_Enchanted; mFlags |= Flag_Enchanted;
static std::set<std::string> boundItemIDCache; if (MWBase::Environment::get().getMechanicsManager()->isBoundItem(base))
// If this is empty then we haven't executed the GMST cache logic yet; or there isn't any sMagicBound* GMST's for some reason
if (boundItemIDCache.empty())
{
// Build a list of known bound item ID's
const MWWorld::Store<ESM::GameSetting> &gameSettings = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
for (MWWorld::Store<ESM::GameSetting>::iterator currentIteration = gameSettings.begin(); currentIteration != gameSettings.end(); ++currentIteration)
{
const ESM::GameSetting &currentSetting = *currentIteration;
std::string currentGMSTID = currentSetting.mId;
Misc::StringUtils::lowerCaseInPlace(currentGMSTID);
// Don't bother checking this GMST if it's not a sMagicBound* one.
const std::string& toFind = "smagicbound";
if (currentGMSTID.compare(0, toFind.length(), toFind) != 0)
continue;
// All sMagicBound* GMST's should be of type string
std::string currentGMSTValue = currentSetting.getString();
Misc::StringUtils::lowerCaseInPlace(currentGMSTValue);
boundItemIDCache.insert(currentGMSTValue);
}
}
// Perform bound item check and assign the Flag_Bound bit if it passes
std::string tempItemID = base.getCellRef().getRefId();
Misc::StringUtils::lowerCaseInPlace(tempItemID);
if (boundItemIDCache.count(tempItemID) != 0)
mFlags |= Flag_Bound; mFlags |= Flag_Bound;
} }
@ -124,7 +94,12 @@ namespace MWGui
return true; return true;
} }
bool ItemModel::allowedToInsertItems() const bool ItemModel::onDropItem(const MWWorld::Ptr &item, int count)
{
return true;
}
bool ItemModel::onTakeItem(const MWWorld::Ptr &item, int count)
{ {
return true; return true;
} }
@ -198,4 +173,18 @@ namespace MWGui
mSourceModel = sourceModel; mSourceModel = sourceModel;
} }
void ProxyItemModel::onClose()
{
mSourceModel->onClose();
}
bool ProxyItemModel::onDropItem(const MWWorld::Ptr &item, int count)
{
return mSourceModel->onDropItem(item, count);
}
bool ProxyItemModel::onTakeItem(const MWWorld::Ptr &item, int count)
{
return mSourceModel->onTakeItem(item, count);
}
} }

@ -72,9 +72,11 @@ namespace MWGui
/// Is the player allowed to use items from this item model? (default true) /// Is the player allowed to use items from this item model? (default true)
virtual bool allowedToUseItems() const; virtual bool allowedToUseItems() const;
virtual void onClose()
/// Is the player allowed to insert items into this model? (default true) {
virtual bool allowedToInsertItems() const; }
virtual bool onDropItem(const MWWorld::Ptr &item, int count);
virtual bool onTakeItem(const MWWorld::Ptr &item, int count);
private: private:
ItemModel(const ItemModel&); ItemModel(const ItemModel&);
@ -91,6 +93,10 @@ namespace MWGui
bool allowedToUseItems() const; bool allowedToUseItems() const;
virtual void onClose();
virtual bool onDropItem(const MWWorld::Ptr &item, int count);
virtual bool onTakeItem(const MWWorld::Ptr &item, int count);
virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool setNewOwner=false); virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool setNewOwner=false);
virtual void removeItem (const ItemStack& item, size_t count); virtual void removeItem (const ItemStack& item, size_t count);
virtual ModelIndex getIndex (ItemStack item); virtual ModelIndex getIndex (ItemStack item);

@ -5,8 +5,10 @@
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "textcolours.hpp" #include <components/fontloader/fontloader.hpp>
#include <components/misc/utf8stream.hpp>
#include "textcolours.hpp"
namespace namespace
{ {
@ -154,8 +156,8 @@ MWGui::BookTypesetter::Utf8Span to_utf8_span (char const * text)
typedef TypesetBook::Ptr book; typedef TypesetBook::Ptr book;
JournalBooks::JournalBooks (JournalViewModel::Ptr model) : JournalBooks::JournalBooks (JournalViewModel::Ptr model, ToUTF8::FromType encoding) :
mModel (model) mModel (model), mEncoding(encoding)
{ {
} }
@ -217,34 +219,82 @@ book JournalBooks::createQuestBook (const std::string& questName)
} }
book JournalBooks::createTopicIndexBook () book JournalBooks::createTopicIndexBook ()
{
bool isRussian = (mEncoding == ToUTF8::WINDOWS_1251);
BookTypesetter::Ptr typesetter = isRussian ? createCyrillicJournalIndex() : createLatinJournalIndex();
return typesetter->complete ();
}
BookTypesetter::Ptr JournalBooks::createLatinJournalIndex ()
{ {
BookTypesetter::Ptr typesetter = BookTypesetter::create (92, 250); BookTypesetter::Ptr typesetter = BookTypesetter::create (92, 250);
typesetter->setSectionAlignment (BookTypesetter::AlignCenter); typesetter->setSectionAlignment (BookTypesetter::AlignCenter);
BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); char ch = 'A';
BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black);
for (int i = 0; i < 26; ++i) for (int i = 0; i < 26; ++i)
{ {
char ch = 'A' + i;
char buffer [32]; char buffer [32];
sprintf (buffer, "( %c )", ch); sprintf (buffer, "( %c )", ch);
const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours();
BookTypesetter::Style* style = typesetter->createHotStyle (body, textColours.journalTopic, BookTypesetter::Style* style = typesetter->createHotStyle (body, textColours.journalTopic,
textColours.journalTopicOver, textColours.journalTopicOver,
textColours.journalTopicPressed, ch); textColours.journalTopicPressed, (uint32_t) ch);
if (i == 13) if (i == 13)
typesetter->sectionBreak (); typesetter->sectionBreak ();
typesetter->write (style, to_utf8_span (buffer)); typesetter->write (style, to_utf8_span (buffer));
typesetter->lineBreak (); typesetter->lineBreak ();
ch++;
} }
return typesetter->complete (); return typesetter;
}
BookTypesetter::Ptr JournalBooks::createCyrillicJournalIndex ()
{
BookTypesetter::Ptr typesetter = BookTypesetter::create (92, 250);
typesetter->setSectionAlignment (BookTypesetter::AlignCenter);
BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black);
unsigned char ch[2] = {0xd0, 0x90}; // CYRILLIC CAPITAL A is a 0xd090 in UTF-8
for (int i = 0; i < 32; ++i)
{
char buffer [32];
sprintf(buffer, "( %c%c )", ch[0], ch[1]);
Utf8Stream stream ((char*) ch);
uint32_t first = stream.peek();
const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours();
BookTypesetter::Style* style = typesetter->createHotStyle (body, textColours.journalTopic,
textColours.journalTopicOver,
textColours.journalTopicPressed, first);
ch[1]++;
// Words can not be started with these characters
if (i == 26 || i == 28)
continue;
if (i == 15)
typesetter->sectionBreak ();
typesetter->write (style, to_utf8_span (buffer));
typesetter->lineBreak ();
}
return typesetter;
} }
BookTypesetter::Ptr JournalBooks::createTypesetter () BookTypesetter::Ptr JournalBooks::createTypesetter ()

@ -4,6 +4,8 @@
#include "bookpage.hpp" #include "bookpage.hpp"
#include "journalviewmodel.hpp" #include "journalviewmodel.hpp"
#include <components/to_utf8/to_utf8.hpp>
namespace MWGui namespace MWGui
{ {
MWGui::BookTypesetter::Utf8Span to_utf8_span (char const * text); MWGui::BookTypesetter::Utf8Span to_utf8_span (char const * text);
@ -13,7 +15,7 @@ namespace MWGui
typedef TypesetBook::Ptr Book; typedef TypesetBook::Ptr Book;
JournalViewModel::Ptr mModel; JournalViewModel::Ptr mModel;
JournalBooks (JournalViewModel::Ptr model); JournalBooks (JournalViewModel::Ptr model, ToUTF8::FromType encoding);
Book createEmptyJournalBook (); Book createEmptyJournalBook ();
Book createJournalBook (); Book createJournalBook ();
@ -22,8 +24,12 @@ namespace MWGui
Book createQuestBook (const std::string& questName); Book createQuestBook (const std::string& questName);
Book createTopicIndexBook (); Book createTopicIndexBook ();
ToUTF8::FromType mEncoding;
private: private:
BookTypesetter::Ptr createTypesetter (); BookTypesetter::Ptr createTypesetter ();
BookTypesetter::Ptr createLatinJournalIndex ();
BookTypesetter::Ptr createCyrillicJournalIndex ();
}; };
} }

@ -6,6 +6,8 @@
#include <MyGUI_LanguageManager.h> #include <MyGUI_LanguageManager.h>
#include <components/translation/translation.hpp> #include <components/translation/translation.hpp>
#include <components/misc/stringops.hpp>
#include <components/misc/utf8stream.hpp>
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwbase/journal.hpp" #include "../mwbase/journal.hpp"
@ -305,18 +307,37 @@ struct JournalViewModelImpl : JournalViewModel
visitor (toUtf8Span (topic.getName())); visitor (toUtf8Span (topic.getName()));
} }
void visitTopicNamesStartingWith (char character, std::function < void (const std::string&) > visitor) const void visitTopicNamesStartingWith (uint32_t character, std::function < void (const std::string&) > visitor) const
{ {
MWBase::Journal * journal = MWBase::Environment::get().getJournal(); MWBase::Journal * journal = MWBase::Environment::get().getJournal();
for (MWBase::Journal::TTopicIter i = journal->topicBegin (); i != journal->topicEnd (); ++i) for (MWBase::Journal::TTopicIter i = journal->topicBegin (); i != journal->topicEnd (); ++i)
{ {
if (i->first [0] != Misc::StringUtils::toLower(character)) Utf8Stream stream (i->first.c_str());
uint32_t first = toUpper(stream.peek());
if (first != character)
continue; continue;
visitor (i->second.getName()); visitor (i->second.getName());
} }
}
static uint32_t toUpper(uint32_t ch)
{
// Russian alphabet
if (ch >= 0x0430 && ch < 0x0450)
ch -= 0x20;
// Cyrillic IO character
if (ch == 0x0451)
ch -= 0x50;
// Latin alphabet
if (ch >= 0x61 && ch < 0x80)
ch -= 0x20;
return ch;
} }
struct TopicEntryImpl : BaseEntry <MWDialogue::Topic::TEntryIter, TopicEntry> struct TopicEntryImpl : BaseEntry <MWDialogue::Topic::TEntryIter, TopicEntry>

@ -75,8 +75,8 @@ namespace MWGui
/// provides the name of the topic specified by its id /// provides the name of the topic specified by its id
virtual void visitTopicName (TopicId topicId, std::function <void (Utf8Span)> visitor) const = 0; virtual void visitTopicName (TopicId topicId, std::function <void (Utf8Span)> visitor) const = 0;
/// walks over the topics whose names start with the specified character providing the topics name /// walks over the topics whose names start with the character
virtual void visitTopicNamesStartingWith (char character, std::function < void (const std::string&) > visitor) const = 0; virtual void visitTopicNamesStartingWith (uint32_t character, std::function < void (const std::string&) > visitor) const = 0;
/// walks over the topic entries for the topic specified by its identifier /// walks over the topic entries for the topic specified by its identifier
virtual void visitTopicEntries (TopicId topicId, std::function <void (TopicEntry const &)> visitor) const = 0; virtual void visitTopicEntries (TopicId topicId, std::function <void (TopicEntry const &)> visitor) const = 0;

@ -100,8 +100,8 @@ namespace
return getWidget <MWGui::BookPage> (name); return getWidget <MWGui::BookPage> (name);
} }
JournalWindowImpl (MWGui::JournalViewModel::Ptr Model, bool questList) JournalWindowImpl (MWGui::JournalViewModel::Ptr Model, bool questList, ToUTF8::FromType encoding)
: JournalBooks (Model), JournalWindow() : JournalBooks (Model, encoding), JournalWindow()
{ {
center(); center();
@ -161,6 +161,15 @@ namespace
Gui::ImageButton* showActiveButton = getWidget<Gui::ImageButton>(ShowActiveBTN); Gui::ImageButton* showActiveButton = getWidget<Gui::ImageButton>(ShowActiveBTN);
Gui::ImageButton* showAllButton = getWidget<Gui::ImageButton>(ShowAllBTN); Gui::ImageButton* showAllButton = getWidget<Gui::ImageButton>(ShowAllBTN);
Gui::ImageButton* questsButton = getWidget<Gui::ImageButton>(QuestsBTN); Gui::ImageButton* questsButton = getWidget<Gui::ImageButton>(QuestsBTN);
Gui::ImageButton* nextButton = getWidget<Gui::ImageButton>(NextPageBTN);
if (nextButton->getSize().width == 64)
{
// english button has a 7 pixel wide strip of garbage on its right edge
nextButton->setSize(64-7, nextButton->getSize().height);
nextButton->setImageCoord(MyGUI::IntCoord(0,0,64-7,nextButton->getSize().height));
}
if (!questList) if (!questList)
{ {
// If tribunal is not installed (-> no options button), we still want the Topics button available, // If tribunal is not installed (-> no options button), we still want the Topics button available,
@ -176,6 +185,8 @@ namespace
showActiveButton->setVisible(false); showActiveButton->setVisible(false);
showAllButton->setVisible(false); showAllButton->setVisible(false);
questsButton->setVisible(false); questsButton->setVisible(false);
adjustButton(TopicsBTN);
} }
else else
{ {
@ -188,23 +199,25 @@ namespace
adjustButton(ShowActiveBTN); adjustButton(ShowActiveBTN);
adjustButton(OptionsBTN); adjustButton(OptionsBTN);
adjustButton(QuestsBTN); adjustButton(QuestsBTN);
} adjustButton(TopicsBTN);
int topicsWidth = getWidget<MyGUI::Widget>(TopicsBTN)->getSize().width;
Gui::ImageButton* nextButton = getWidget<Gui::ImageButton>(NextPageBTN); int cancelLeft = getWidget<MyGUI::Widget>(CancelBTN)->getPosition().left;
if (nextButton->getSize().width == 64) int cancelRight = getWidget<MyGUI::Widget>(CancelBTN)->getPosition().left + getWidget<MyGUI::Widget>(CancelBTN)->getSize().width;
{
// english button has a 7 pixel wide strip of garbage on its right edge
nextButton->setSize(64-7, nextButton->getSize().height);
nextButton->setImageCoord(MyGUI::IntCoord(0,0,64-7,nextButton->getSize().height));
}
adjustButton(TopicsBTN); getWidget<MyGUI::Widget>(QuestsBTN)->setPosition(cancelRight, getWidget<MyGUI::Widget>(QuestsBTN)->getPosition().top);
int topicsWidth = getWidget<MyGUI::Widget>(TopicsBTN)->getSize().width;
int cancelLeft = getWidget<MyGUI::Widget>(CancelBTN)->getPosition().left;
int cancelRight = getWidget<MyGUI::Widget>(CancelBTN)->getPosition().left + getWidget<MyGUI::Widget>(CancelBTN)->getSize().width;
getWidget<MyGUI::Widget>(TopicsBTN)->setPosition(cancelLeft - topicsWidth, getWidget<MyGUI::Widget>(TopicsBTN)->getPosition().top); // Usually Topics, Quests, and Cancel buttons have the 64px width, so we can place the Topics left-up from the Cancel button, and the Quests right-up from the Cancel button.
getWidget<MyGUI::Widget>(QuestsBTN)->setPosition(cancelRight, getWidget<MyGUI::Widget>(QuestsBTN)->getPosition().top); // But in some installations, e.g. German one, the Topics button has the 128px width, so we should place it exactly left from the Quests button.
if (topicsWidth == 64)
{
getWidget<MyGUI::Widget>(TopicsBTN)->setPosition(cancelLeft - topicsWidth, getWidget<MyGUI::Widget>(TopicsBTN)->getPosition().top);
}
else
{
int questLeft = getWidget<MyGUI::Widget>(QuestsBTN)->getPosition().left;
getWidget<MyGUI::Widget>(TopicsBTN)->setPosition(questLeft - topicsWidth, getWidget<MyGUI::Widget>(TopicsBTN)->getPosition().top);
}
}
mQuestMode = false; mQuestMode = false;
mAllQuests = false; mAllQuests = false;
@ -464,7 +477,7 @@ namespace
MWBase::Environment::get().getWindowManager()->playSound("book page"); MWBase::Environment::get().getWindowManager()->playSound("book page");
} }
void notifyIndexLinkClicked (MWGui::TypesetBook::InteractiveId character) void notifyIndexLinkClicked (MWGui::TypesetBook::InteractiveId index)
{ {
setVisible (LeftTopicIndex, false); setVisible (LeftTopicIndex, false);
setVisible (RightTopicIndex, false); setVisible (RightTopicIndex, false);
@ -477,7 +490,7 @@ namespace
AddNamesToList add(list); AddNamesToList add(list);
mModel->visitTopicNamesStartingWith((char) character, add); mModel->visitTopicNamesStartingWith(index, add);
list->adjustSize(); list->adjustSize();
@ -632,9 +645,9 @@ namespace
} }
// glue the implementation to the interface // glue the implementation to the interface
MWGui::JournalWindow * MWGui::JournalWindow::create (JournalViewModel::Ptr Model, bool questList) MWGui::JournalWindow * MWGui::JournalWindow::create (JournalViewModel::Ptr Model, bool questList, ToUTF8::FromType encoding)
{ {
return new JournalWindowImpl (Model, questList); return new JournalWindowImpl (Model, questList, encoding);
} }
MWGui::JournalWindow::JournalWindow() MWGui::JournalWindow::JournalWindow()

@ -3,6 +3,8 @@
#include "windowbase.hpp" #include "windowbase.hpp"
#include <components/to_utf8/to_utf8.hpp>
#include <memory> #include <memory>
namespace MWBase { class WindowManager; } namespace MWBase { class WindowManager; }
@ -16,7 +18,7 @@ namespace MWGui
JournalWindow(); JournalWindow();
/// construct a new instance of the one JournalWindow implementation /// construct a new instance of the one JournalWindow implementation
static JournalWindow * create (std::shared_ptr <JournalViewModel> Model, bool questList); static JournalWindow * create (std::shared_ptr <JournalViewModel> Model, bool questList, ToUTF8::FromType encoding);
/// destroy this instance of the JournalWindow implementation /// destroy this instance of the JournalWindow implementation
virtual ~JournalWindow () {}; virtual ~JournalWindow () {};

@ -163,7 +163,6 @@ namespace MWGui
if (mViewer->getIncrementalCompileOperation()) if (mViewer->getIncrementalCompileOperation())
{ {
mViewer->getIncrementalCompileOperation()->setMaximumNumOfObjectsToCompilePerFrame(100); mViewer->getIncrementalCompileOperation()->setMaximumNumOfObjectsToCompilePerFrame(100);
mViewer->getIncrementalCompileOperation()->setTargetFrameRate(getTargetFrameRate());
} }
// Assign dummy bounding sphere callback to avoid the bounding sphere of the entire scene being recomputed after each frame of loading // Assign dummy bounding sphere callback to avoid the bounding sphere of the entire scene being recomputed after each frame of loading

@ -207,8 +207,6 @@ namespace MWGui
, mMessageBoxManager(parMessageBoxManager) , mMessageBoxManager(parMessageBoxManager)
, mButtonPressed(-1) , mButtonPressed(-1)
{ {
setVisible(true);
int textPadding = 10; // padding between text-widget and main-widget int textPadding = 10; // padding between text-widget and main-widget
int textButtonPadding = 10; // padding between the text-widget und the button-widget int textButtonPadding = 10; // padding between the text-widget und the button-widget
int buttonLeftPadding = 10; // padding between the buttons if horizontal int buttonLeftPadding = 10; // padding between the buttons if horizontal
@ -358,7 +356,11 @@ namespace MWGui
mMessageWidget->setCoord(messageWidgetCoord); mMessageWidget->setCoord(messageWidgetCoord);
} }
// Set key focus to "Ok" button setVisible(true);
}
MyGUI::Widget* InteractiveMessageBox::getDefaultKeyFocus()
{
std::vector<std::string> keywords { "sOk", "sYes" }; std::vector<std::string> keywords { "sOk", "sYes" };
for(std::vector<MyGUI::Button*>::const_iterator button = mButtons.begin(); button != mButtons.end(); ++button) for(std::vector<MyGUI::Button*>::const_iterator button = mButtons.begin(); button != mButtons.end(); ++button)
{ {
@ -366,11 +368,11 @@ namespace MWGui
{ {
if(Misc::StringUtils::ciEqual(MyGUI::LanguageManager::getInstance().replaceTags("#{" + keyword + "}"), (*button)->getCaption())) if(Misc::StringUtils::ciEqual(MyGUI::LanguageManager::getInstance().replaceTags("#{" + keyword + "}"), (*button)->getCaption()))
{ {
MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(*button); return *button;
return;
} }
} }
} }
return NULL;
} }
void InteractiveMessageBox::mousePressed (MyGUI::Widget* pressed) void InteractiveMessageBox::mousePressed (MyGUI::Widget* pressed)

@ -28,6 +28,8 @@ namespace MWGui
bool createInteractiveMessageBox (const std::string& message, const std::vector<std::string>& buttons); bool createInteractiveMessageBox (const std::string& message, const std::vector<std::string>& buttons);
bool isInteractiveMessageBox (); bool isInteractiveMessageBox ();
const InteractiveMessageBox* getInteractiveMessageBox() const { return mInterMessageBoxe; }
/// Remove all message boxes /// Remove all message boxes
void clear(); void clear();
@ -77,6 +79,8 @@ namespace MWGui
void mousePressed (MyGUI::Widget* _widget); void mousePressed (MyGUI::Widget* _widget);
int readPressedButton (); int readPressedButton ();
MyGUI::Widget* getDefaultKeyFocus() override;
virtual bool exit() { return false; } virtual bool exit() { return false; }
bool mMarkedToDelete; bool mMarkedToDelete;

@ -3,15 +3,26 @@
#include <components/misc/rng.hpp> #include <components/misc/rng.hpp>
#include <components/esm/loadskil.hpp> #include <components/esm/loadskil.hpp>
#include "../mwmechanics/actorutil.hpp"
#include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/pickpocket.hpp"
#include "../mwworld/containerstore.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/windowmanager.hpp"
namespace MWGui namespace MWGui
{ {
PickpocketItemModel::PickpocketItemModel(const MWWorld::Ptr& thief, ItemModel *sourceModel, bool hideItems) PickpocketItemModel::PickpocketItemModel(const MWWorld::Ptr& actor, ItemModel *sourceModel, bool hideItems)
: mActor(actor), mPickpocketDetected(false)
{ {
MWWorld::Ptr player = MWMechanics::getPlayer();
mSourceModel = sourceModel; mSourceModel = sourceModel;
int chance = thief.getClass().getSkill(thief, ESM::Skill::Sneak); int chance = player.getClass().getSkill(player, ESM::Skill::Sneak);
mSourceModel->update(); mSourceModel->update();
@ -66,13 +77,63 @@ namespace MWGui
void PickpocketItemModel::removeItem (const ItemStack &item, size_t count) void PickpocketItemModel::removeItem (const ItemStack &item, size_t count)
{ {
ProxyItemModel::removeItem(item, count); ProxyItemModel::removeItem(item, count);
/// \todo check if player is detected
} }
bool PickpocketItemModel::allowedToInsertItems() const bool PickpocketItemModel::onDropItem(const MWWorld::Ptr &item, int count)
{ {
// don't allow "reverse pickpocket" (yet) // don't allow "reverse pickpocket" (it will be handled by scripts after 1.0)
return false; return false;
} }
void PickpocketItemModel::onClose()
{
// Make sure we were actually closed, rather than just temporarily hidden (e.g. console or main menu opened)
if (MWBase::Environment::get().getWindowManager()->containsMode(GM_Container)
// If it was already detected while taking an item, no need to check now
|| mPickpocketDetected)
return;
MWWorld::Ptr player = MWMechanics::getPlayer();
MWMechanics::Pickpocket pickpocket(player, mActor);
if (pickpocket.finish())
{
MWBase::Environment::get().getMechanicsManager()->commitCrime(
player, mActor, MWBase::MechanicsManager::OT_Pickpocket, 0, true);
MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container);
mPickpocketDetected = true;
}
}
bool PickpocketItemModel::onTakeItem(const MWWorld::Ptr &item, int count)
{
if (mActor.getClass().getCreatureStats(mActor).getKnockedDown())
return mSourceModel->onTakeItem(item, count);
bool success = stealItem(item, count);
if (success)
{
MWWorld::Ptr player = MWMechanics::getPlayer();
MWBase::Environment::get().getMechanicsManager()->itemTaken(player, item, mActor, count, false);
}
return success;
}
bool PickpocketItemModel::stealItem(const MWWorld::Ptr &item, int count)
{
MWWorld::Ptr player = MWMechanics::getPlayer();
MWMechanics::Pickpocket pickpocket(player, mActor);
if (pickpocket.pick(item, count))
{
MWBase::Environment::get().getMechanicsManager()->commitCrime(
player, mActor, MWBase::MechanicsManager::OT_Pickpocket, 0, true);
MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container);
mPickpocketDetected = true;
return false;
}
else
player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, 1);
return true;
}
} }

@ -17,7 +17,14 @@ namespace MWGui
virtual size_t getItemCount(); virtual size_t getItemCount();
virtual void update(); virtual void update();
virtual void removeItem (const ItemStack& item, size_t count); virtual void removeItem (const ItemStack& item, size_t count);
virtual bool allowedToInsertItems() const; virtual void onClose();
virtual bool onDropItem(const MWWorld::Ptr &item, int count);
virtual bool onTakeItem(const MWWorld::Ptr &item, int count);
protected:
MWWorld::Ptr mActor;
bool mPickpocketDetected;
bool stealItem(const MWWorld::Ptr &item, int count);
private: private:
std::vector<ItemStack> mHiddenItems; std::vector<ItemStack> mHiddenItems;

@ -311,4 +311,18 @@ namespace MWGui
std::sort(mItems.begin(), mItems.end(), cmp); std::sort(mItems.begin(), mItems.end(), cmp);
} }
void SortFilterItemModel::onClose()
{
mSourceModel->onClose();
}
bool SortFilterItemModel::onDropItem(const MWWorld::Ptr &item, int count)
{
return mSourceModel->onDropItem(item, count);
}
bool SortFilterItemModel::onTakeItem(const MWWorld::Ptr &item, int count)
{
return mSourceModel->onTakeItem(item, count);
}
} }

@ -29,6 +29,10 @@ namespace MWGui
/// Use ItemStack::Type for sorting? /// Use ItemStack::Type for sorting?
void setSortByType(bool sort) { mSortByType = sort; } void setSortByType(bool sort) { mSortByType = sort; }
void onClose();
bool onDropItem(const MWWorld::Ptr &item, int count);
bool onTakeItem(const MWWorld::Ptr &item, int count);
static const int Category_Weapon = (1<<1); static const int Category_Weapon = (1<<1);
static const int Category_Apparel = (1<<2); static const int Category_Apparel = (1<<2);
static const int Category_Misc = (1<<3); static const int Category_Misc = (1<<3);

@ -60,7 +60,7 @@ namespace MWGui
{ {
newSpell.mType = Spell::Type_Spell; newSpell.mType = Spell::Type_Spell;
std::string cost = std::to_string(spell->mData.mCost); std::string cost = std::to_string(spell->mData.mCost);
std::string chance = std::to_string(int(MWMechanics::getSpellSuccessChance(spell, mActor))); std::string chance = std::to_string(int(MWMechanics::getSpellSuccessChance(spell, mActor, NULL, true, true)));
newSpell.mCostColumn = cost + "/" + chance; newSpell.mCostColumn = cost + "/" + chance;
} }
else else

@ -96,6 +96,12 @@ namespace MWGui
return (!mTimeAdvancer.isRunning()); //Only exit if not currently waiting return (!mTimeAdvancer.isRunning()); //Only exit if not currently waiting
} }
void WaitDialog::clear()
{
mSleeping = false;
mTimeAdvancer.stop();
}
void WaitDialog::onOpen() void WaitDialog::onOpen()
{ {
if (mTimeAdvancer.isRunning()) if (mTimeAdvancer.isRunning())

@ -33,6 +33,8 @@ namespace MWGui
virtual bool exit(); virtual bool exit();
virtual void clear();
void onFrame(float dt); void onFrame(float dt);
bool getSleeping() { return mTimeAdvancer.isRunning() && mSleeping; } bool getSleeping() { return mTimeAdvancer.isRunning() && mSleeping; }

@ -193,6 +193,7 @@ namespace MWGui
, mRestAllowed(true) , mRestAllowed(true)
, mFallbackMap(fallbackMap) , mFallbackMap(fallbackMap)
, mShowOwned(0) , mShowOwned(0)
, mEncoding(encoding)
, mVersionDescription(versionDescription) , mVersionDescription(versionDescription)
{ {
float uiScale = Settings::Manager::getFloat("scaling factor", "GUI"); float uiScale = Settings::Manager::getFloat("scaling factor", "GUI");
@ -346,7 +347,7 @@ namespace MWGui
mGuiModeStates[GM_Console] = GuiModeState(mConsole); mGuiModeStates[GM_Console] = GuiModeState(mConsole);
bool questList = mResourceSystem->getVFS()->exists("textures/tx_menubook_options_over.dds"); bool questList = mResourceSystem->getVFS()->exists("textures/tx_menubook_options_over.dds");
JournalWindow* journal = JournalWindow::create(JournalViewModel::create (), questList); JournalWindow* journal = JournalWindow::create(JournalViewModel::create (), questList, mEncoding);
mWindows.push_back(journal); mWindows.push_back(journal);
mGuiModeStates[GM_Journal] = GuiModeState(journal); mGuiModeStates[GM_Journal] = GuiModeState(journal);
mGuiModeStates[GM_Journal].mCloseSound = "book close"; mGuiModeStates[GM_Journal].mCloseSound = "book close";
@ -530,6 +531,7 @@ namespace MWGui
delete mDragAndDrop; delete mDragAndDrop;
delete mSoulgemDialog; delete mSoulgemDialog;
delete mCursorManager; delete mCursorManager;
delete mToolTips;
cleanupGarbage(); cleanupGarbage();
@ -872,6 +874,22 @@ namespace MWGui
window->onFrame(frameDuration); window->onFrame(frameDuration);
} }
// Make sure message boxes are always in front
// This is an awful workaround for a series of awfully interwoven issues that couldn't be worked around
// in a better way because of an impressive number of even more awfully interwoven issues.
if (mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox() && mCurrentModals.back() != mMessageBoxManager->getInteractiveMessageBox())
{
std::vector<WindowModal*>::iterator found = std::find(mCurrentModals.begin(), mCurrentModals.end(), mMessageBoxManager->getInteractiveMessageBox());
if (found != mCurrentModals.end())
{
WindowModal* msgbox = *found;
std::swap(*found, mCurrentModals.back());
MyGUI::InputManager::getInstance().addWidgetModal(msgbox->mMainWidget);
mKeyboardNavigation->setModalWindow(msgbox->mMainWidget);
mKeyboardNavigation->setDefaultFocus(msgbox->mMainWidget, msgbox->getDefaultKeyFocus());
}
}
if (!mCurrentModals.empty()) if (!mCurrentModals.empty())
mCurrentModals.back()->onFrame(frameDuration); mCurrentModals.back()->onFrame(frameDuration);
@ -2068,5 +2086,4 @@ namespace MWGui
for (unsigned int i=0; i<mWindows.size(); ++i) for (unsigned int i=0; i<mWindows.size(); ++i)
mWindows[i]->setVisible(visible); mWindows[i]->setVisible(visible);
} }
} }

@ -511,6 +511,8 @@ namespace MWGui
int mShowOwned; int mShowOwned;
ToUTF8::FromType mEncoding;
std::string mVersionDescription; std::string mVersionDescription;
MWGui::TextColours mTextColours; MWGui::TextColours mTextColours;

@ -176,8 +176,25 @@ namespace MWInput
} }
} }
bool isLeftOrRightButton(int action, ICS::InputControlSystem* ics, int deviceId, bool joystick)
{
int mouseBinding = ics->getMouseButtonBinding(ics->getControl(action), ICS::Control::INCREASE);
if (mouseBinding != ICS_MAX_DEVICE_BUTTONS)
return true;
int buttonBinding = ics->getJoystickButtonBinding(ics->getControl(action), deviceId, ICS::Control::INCREASE);
if (joystick && (buttonBinding == 0 || buttonBinding == 1))
return true;
return false;
}
void InputManager::handleGuiArrowKey(int action) void InputManager::handleGuiArrowKey(int action)
{ {
if (SDL_IsTextInputActive())
return;
if (isLeftOrRightButton(action, mInputBinder, mFakeDeviceID, mJoystickLastUsed))
return;
MyGUI::KeyCode key; MyGUI::KeyCode key;
switch (action) switch (action)
{ {
@ -232,7 +249,10 @@ namespace MWInput
if (mControlSwitch["playercontrols"]) if (mControlSwitch["playercontrols"])
{ {
if (action == A_Use) if (action == A_Use)
mPlayer->setAttackingOrSpell(currentValue != 0); {
MWMechanics::DrawState_ state = MWBase::Environment::get().getWorld()->getPlayer().getDrawState();
mPlayer->setAttackingOrSpell(currentValue != 0 && state != MWMechanics::DrawState_Nothing);
}
else if (action == A_Jump) else if (action == A_Jump)
mAttemptJump = (currentValue == 1.0 && previousValue == 0.0); mAttemptJump = (currentValue == 1.0 && previousValue == 0.0);
} }
@ -1143,7 +1163,10 @@ namespace MWInput
void InputManager::activate() void InputManager::activate()
{ {
if (MWBase::Environment::get().getWindowManager()->isGuiMode()) if (MWBase::Environment::get().getWindowManager()->isGuiMode())
MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Return, 0); {
if (!SDL_IsTextInputActive() && !isLeftOrRightButton(A_Activate, mInputBinder, mFakeDeviceID, mJoystickLastUsed))
MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Return, 0);
}
else if (mControlSwitch["playercontrols"]) else if (mControlSwitch["playercontrols"])
mPlayer->activate(); mPlayer->activate();
} }
@ -1268,13 +1291,13 @@ namespace MWInput
clearAllKeyBindings(control); clearAllKeyBindings(control);
if (defaultKeyBindings.find(i) != defaultKeyBindings.end() if (defaultKeyBindings.find(i) != defaultKeyBindings.end()
&& !mInputBinder->isKeyBound(defaultKeyBindings[i])) && (force || !mInputBinder->isKeyBound(defaultKeyBindings[i])))
{ {
control->setInitialValue(0.0f); control->setInitialValue(0.0f);
mInputBinder->addKeyBinding(control, defaultKeyBindings[i], ICS::Control::INCREASE); mInputBinder->addKeyBinding(control, defaultKeyBindings[i], ICS::Control::INCREASE);
} }
else if (defaultMouseButtonBindings.find(i) != defaultMouseButtonBindings.end() else if (defaultMouseButtonBindings.find(i) != defaultMouseButtonBindings.end()
&& !mInputBinder->isMouseButtonBound(defaultMouseButtonBindings[i])) && (force || !mInputBinder->isMouseButtonBound(defaultMouseButtonBindings[i])))
{ {
control->setInitialValue(0.0f); control->setInitialValue(0.0f);
mInputBinder->addMouseButtonBinding (control, defaultMouseButtonBindings[i], ICS::Control::INCREASE); mInputBinder->addMouseButtonBinding (control, defaultMouseButtonBindings[i], ICS::Control::INCREASE);
@ -1348,12 +1371,12 @@ namespace MWInput
clearAllControllerBindings(control); clearAllControllerBindings(control);
if (defaultButtonBindings.find(i) != defaultButtonBindings.end() if (defaultButtonBindings.find(i) != defaultButtonBindings.end()
&& !mInputBinder->isJoystickButtonBound(mFakeDeviceID, defaultButtonBindings[i])) && (force || !mInputBinder->isJoystickButtonBound(mFakeDeviceID, defaultButtonBindings[i])))
{ {
control->setInitialValue(0.0f); control->setInitialValue(0.0f);
mInputBinder->addJoystickButtonBinding(control, mFakeDeviceID, defaultButtonBindings[i], ICS::Control::INCREASE); mInputBinder->addJoystickButtonBinding(control, mFakeDeviceID, defaultButtonBindings[i], ICS::Control::INCREASE);
} }
else if (defaultAxisBindings.find(i) != defaultAxisBindings.end() && !mInputBinder->isJoystickAxisBound(mFakeDeviceID, defaultAxisBindings[i])) else if (defaultAxisBindings.find(i) != defaultAxisBindings.end() && (force || !mInputBinder->isJoystickAxisBound(mFakeDeviceID, defaultAxisBindings[i])))
{ {
control->setValue(0.5f); control->setValue(0.5f);
control->setInitialValue(0.5f); control->setInitialValue(0.5f);

@ -1009,7 +1009,8 @@ namespace MWMechanics
if (player.getClass().getNpcStats(player).isWerewolf()) if (player.getClass().getNpcStats(player).isWerewolf())
return; return;
if (ptr.getClass().isClass(ptr, "Guard") && creatureStats.getAiSequence().getTypeId() != AiPackage::TypeIdPursue && !creatureStats.getAiSequence().isInCombat()) if (ptr.getClass().isClass(ptr, "Guard") && creatureStats.getAiSequence().getTypeId() != AiPackage::TypeIdPursue && !creatureStats.getAiSequence().isInCombat()
&& creatureStats.getMagicEffects().get(ESM::MagicEffect::CalmHumanoid).getMagnitude() == 0)
{ {
const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore();
static const int cutoff = esmStore.get<ESM::GameSetting>().find("iCrimeThreshold")->getInt(); static const int cutoff = esmStore.get<ESM::GameSetting>().find("iCrimeThreshold")->getInt();
@ -1148,6 +1149,46 @@ namespace MWMechanics
} }
} }
void Actors::updateCombatMusic ()
{
MWWorld::Ptr player = getPlayer();
int hostilesCount = 0; // need to know this to play Battle music
for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter)
{
if (!iter->first.getClass().getCreatureStats(iter->first).isDead())
{
bool inProcessingRange = (player.getRefData().getPosition().asVec3() - iter->first.getRefData().getPosition().asVec3()).length2()
<= sqrAiProcessingDistance;
if (MWBase::Environment::get().getMechanicsManager()->isAIActive() && inProcessingRange)
{
if (iter->first != player)
{
MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first);
if (stats.getAiSequence().isInCombat() && !stats.isDead()) hostilesCount++;
}
}
}
}
// check if we still have any player enemies to switch music
static int currentMusic = 0;
if (currentMusic != 1 && hostilesCount == 0 && !(player.getClass().getCreatureStats(player).isDead() &&
MWBase::Environment::get().getSoundManager()->isMusicPlaying()))
{
MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore"));
currentMusic = 1;
}
else if (currentMusic != 2 && hostilesCount > 0)
{
MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Battle"));
currentMusic = 2;
}
}
void Actors::update (float duration, bool paused) void Actors::update (float duration, bool paused)
{ {
if(!paused) if(!paused)
@ -1165,8 +1206,6 @@ namespace MWMechanics
MWWorld::Ptr player = getPlayer(); MWWorld::Ptr player = getPlayer();
int hostilesCount = 0; // need to know this to play Battle music
/// \todo move update logic to Actor class where appropriate /// \todo move update logic to Actor class where appropriate
std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr> > cachedAllies; // will be filled as engageCombat iterates std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr> > cachedAllies; // will be filled as engageCombat iterates
@ -1174,14 +1213,12 @@ namespace MWMechanics
// AI and magic effects update // AI and magic effects update
for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter)
{ {
float distSqr = (player.getRefData().getPosition().asVec3() - iter->first.getRefData().getPosition().asVec3()).length2();
// AI processing is only done within distance of 7168 units to the player. Note the "AI distance" slider doesn't affect this // AI processing is only done within distance of 7168 units to the player. Note the "AI distance" slider doesn't affect this
// (it only does some throttling for targets beyond the "AI distance", so doesn't give any guarantees as to whether AI will be enabled or not) // (it only does some throttling for targets beyond the "AI distance", so doesn't give any guarantees as to whether AI will be enabled or not)
// This distance could be made configurable later, but the setting must be marked with a big warning: // This distance could be made configurable later, but the setting must be marked with a big warning:
// using higher values will make a quest in Bloodmoon harder or impossible to complete (bug #1876) // using higher values will make a quest in Bloodmoon harder or impossible to complete (bug #1876)
bool inProcessingRange = (player.getRefData().getPosition().asVec3() - iter->first.getRefData().getPosition().asVec3()).length2() bool inProcessingRange = distSqr <= sqrAiProcessingDistance;
<= sqrAiProcessingDistance;
iter->second->getCharacterController()->setActive(inProcessingRange);
if (iter->first == player) if (iter->first == player)
iter->second->getCharacterController()->setAttackingOrSpell(MWBase::Environment::get().getWorld()->getPlayer().getAttackingOrSpell()); iter->second->getCharacterController()->setAttackingOrSpell(MWBase::Environment::get().getWorld()->getPlayer().getAttackingOrSpell());
@ -1230,8 +1267,13 @@ namespace MWMechanics
float sqrHeadTrackDistance = std::numeric_limits<float>::max(); float sqrHeadTrackDistance = std::numeric_limits<float>::max();
MWWorld::Ptr headTrackTarget; MWWorld::Ptr headTrackTarget;
MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first);
// Unconsious actor can not track target // Unconsious actor can not track target
if (!iter->first.getClass().getCreatureStats(iter->first).getKnockedDown()) // Also actors in combat and pursue mode do not bother to headtrack
if (!stats.getKnockedDown() &&
!stats.getAiSequence().isInCombat() &&
!stats.getAiSequence().hasPackage(AiPackage::TypeIdPursue))
{ {
for(PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) for(PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it)
{ {
@ -1239,8 +1281,9 @@ namespace MWMechanics
continue; continue;
updateHeadTracking(iter->first, it->first, headTrackTarget, sqrHeadTrackDistance); updateHeadTracking(iter->first, it->first, headTrackTarget, sqrHeadTrackDistance);
} }
iter->second->getCharacterController()->setHeadTrackTarget(headTrackTarget);
} }
iter->second->getCharacterController()->setHeadTrackTarget(headTrackTarget);
} }
if (iter->first.getClass().isNpc() && iter->first != player) if (iter->first.getClass().isNpc() && iter->first != player)
@ -1251,8 +1294,6 @@ namespace MWMechanics
CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first);
if (isConscious(iter->first)) if (isConscious(iter->first))
stats.getAiSequence().execute(iter->first, *iter->second->getCharacterController(), iter->second->getAiState(), duration); stats.getAiSequence().execute(iter->first, *iter->second->getCharacterController(), iter->second->getAiState(), duration);
if (stats.getAiSequence().isInCombat() && !stats.isDead()) hostilesCount++;
} }
} }
@ -1283,9 +1324,24 @@ namespace MWMechanics
CharacterController* playerCharacter = NULL; CharacterController* playerCharacter = NULL;
for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter)
{ {
if (iter->first != player && const float animationDistance = aiProcessingDistance + 400; // Slightly larger than AI distance so there is time to switch back to the idle animation.
(player.getRefData().getPosition().asVec3() - iter->first.getRefData().getPosition().asVec3()).length2() const float distSqr = (player.getRefData().getPosition().asVec3() - iter->first.getRefData().getPosition().asVec3()).length2();
> sqrAiProcessingDistance) bool isPlayer = iter->first == player;
bool inAnimationRange = isPlayer || (animationDistance == 0 || distSqr <= animationDistance*animationDistance);
int activeFlag = 1; // Can be changed back to '2' to keep updating bounding boxes off screen (more accurate, but slower)
if (isPlayer)
activeFlag = 2;
int active = inAnimationRange ? activeFlag : 0;
bool canFly = iter->first.getClass().canFly(iter->first);
if (canFly)
{
// Keep animating flying creatures so they don't just hover in-air
inAnimationRange = true;
active = std::max(1, active);
}
iter->second->getCharacterController()->setActive(active);
if (!inAnimationRange)
continue; continue;
if (iter->first.getClass().getCreatureStats(iter->first).isParalyzed()) if (iter->first.getClass().getCreatureStats(iter->first).isParalyzed())
@ -1325,21 +1381,6 @@ namespace MWMechanics
killDeadActors(); killDeadActors();
// check if we still have any player enemies to switch music
static int currentMusic = 0;
if (currentMusic != 1 && hostilesCount == 0 && !(player.getClass().getCreatureStats(player).isDead() &&
MWBase::Environment::get().getSoundManager()->isMusicPlaying()))
{
MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore"));
currentMusic = 1;
}
else if (currentMusic != 2 && hostilesCount > 0)
{
MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Battle"));
currentMusic = 2;
}
static float sneakTimer = 0.f; // times update of sneak icon static float sneakTimer = 0.f; // times update of sneak icon
// if player is in sneak state see if anyone detects him // if player is in sneak state see if anyone detects him
@ -1406,6 +1447,8 @@ namespace MWMechanics
MWBase::Environment::get().getWindowManager()->setSneakVisibility(false); MWBase::Environment::get().getWindowManager()->setSneakVisibility(false);
} }
} }
updateCombatMusic();
} }
void Actors::killDeadActors() void Actors::killDeadActors()
@ -1621,6 +1664,17 @@ namespace MWMechanics
} }
} }
bool Actors::isAnyObjectInRange(const osg::Vec3f& position, float radius)
{
for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter)
{
if ((iter->first.getRefData().getPosition().asVec3() - position).length2() <= radius*radius)
return true;
}
return false;
}
std::list<MWWorld::Ptr> Actors::getActorsSidingWith(const MWWorld::Ptr& actor) std::list<MWWorld::Ptr> Actors::getActorsSidingWith(const MWWorld::Ptr& actor)
{ {
std::list<MWWorld::Ptr> list; std::list<MWWorld::Ptr> list;

@ -81,6 +81,9 @@ namespace MWMechanics
void dropActors (const MWWorld::CellStore *cellStore, const MWWorld::Ptr& ignore); void dropActors (const MWWorld::CellStore *cellStore, const MWWorld::Ptr& ignore);
///< Deregister all actors (except for \a ignore) in the given cell. ///< Deregister all actors (except for \a ignore) in the given cell.
void updateCombatMusic();
///< Update combat music state
void update (float duration, bool paused); void update (float duration, bool paused);
///< Update actor stats and store desired velocity vectors in \a movement ///< Update actor stats and store desired velocity vectors in \a movement
@ -115,15 +118,17 @@ namespace MWMechanics
bool isRunning(const MWWorld::Ptr& ptr); bool isRunning(const MWWorld::Ptr& ptr);
bool isSneaking(const MWWorld::Ptr& ptr); bool isSneaking(const MWWorld::Ptr& ptr);
void forceStateUpdate(const MWWorld::Ptr &ptr); void forceStateUpdate(const MWWorld::Ptr &ptr);
bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false); bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false);
void skipAnimation(const MWWorld::Ptr& ptr); void skipAnimation(const MWWorld::Ptr& ptr);
bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName); bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName);
void persistAnimationStates(); void persistAnimationStates();
void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector<MWWorld::Ptr>& out); void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector<MWWorld::Ptr>& out);
bool isAnyObjectInRange(const osg::Vec3f& position, float radius);
void cleanupSummonedCreature (CreatureStats& casterStats, int creatureActorId); void cleanupSummonedCreature (CreatureStats& casterStats, int creatureActorId);
///Returns the list of actors which are siding with the given actor in fights ///Returns the list of actors which are siding with the given actor in fights

@ -13,6 +13,7 @@
#include "../mwrender/animation.hpp" #include "../mwrender/animation.hpp"
#include "pathgrid.hpp"
#include "creaturestats.hpp" #include "creaturestats.hpp"
#include "steering.hpp" #include "steering.hpp"
#include "movement.hpp" #include "movement.hpp"
@ -372,7 +373,7 @@ namespace MWMechanics
int closestPointIndex = PathFinder::GetClosestPoint(pathgrid, localPos); int closestPointIndex = PathFinder::GetClosestPoint(pathgrid, localPos);
for (int i = 0; i < static_cast<int>(pathgrid->mPoints.size()); i++) for (int i = 0; i < static_cast<int>(pathgrid->mPoints.size()); i++)
{ {
if (i != closestPointIndex && storage.mCell->isPointConnected(closestPointIndex, i)) if (i != closestPointIndex && getPathGridGraph(storage.mCell).isPointConnected(closestPointIndex, i))
{ {
points.push_back(pathgrid->mPoints[static_cast<size_t>(i)]); points.push_back(pathgrid->mPoints[static_cast<size_t>(i)]);
} }
@ -490,6 +491,22 @@ namespace MWMechanics
void AiCombatStorage::startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target) void AiCombatStorage::startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target)
{ {
// get the range of the target's weapon
MWWorld::Ptr targetWeapon = MWWorld::Ptr();
const MWWorld::Class& targetClass = target.getClass();
if (targetClass.hasInventoryStore(target))
{
MWMechanics::WeaponType weapType = WeapType_None;
MWWorld::ContainerStoreIterator weaponSlot =
MWMechanics::getActiveWeapon(targetClass.getCreatureStats(target), targetClass.getInventoryStore(target), &weapType);
if (weapType != WeapType_PickProbe && weapType != WeapType_Spell && weapType != WeapType_None && weapType != WeapType_HandToHand)
targetWeapon = *weaponSlot;
}
bool targetUsesRanged = false;
float rangeAttackOfTarget = ActionWeapon(targetWeapon).getCombatRange(targetUsesRanged);
if (mMovement.mPosition[0] || mMovement.mPosition[1]) if (mMovement.mPosition[0] || mMovement.mPosition[1])
{ {
mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability();
@ -498,28 +515,6 @@ namespace MWMechanics
// dodge movements (for NPCs and bipedal creatures) // dodge movements (for NPCs and bipedal creatures)
else if (actor.getClass().isBipedal(actor)) else if (actor.getClass().isBipedal(actor))
{ {
// get the range of the target's weapon
float rangeAttackOfTarget = 0.f;
MWWorld::Ptr targetWeapon = MWWorld::Ptr();
const MWWorld::Class& targetClass = target.getClass();
if (targetClass.hasInventoryStore(target))
{
MWMechanics::WeaponType weapType = WeapType_None;
MWWorld::ContainerStoreIterator weaponSlot =
MWMechanics::getActiveWeapon(targetClass.getCreatureStats(target), targetClass.getInventoryStore(target), &weapType);
if (weapType != WeapType_PickProbe && weapType != WeapType_Spell && weapType != WeapType_None && weapType != WeapType_HandToHand)
targetWeapon = *weaponSlot;
}
std::shared_ptr<Action> targetWeaponAction (new ActionWeapon(targetWeapon));
if (targetWeaponAction.get())
{
bool isRangedCombat = false;
rangeAttackOfTarget = targetWeaponAction->getCombatRange(isRangedCombat);
}
// apply sideway movement (kind of dodging) with some probability // apply sideway movement (kind of dodging) with some probability
// if actor is within range of target's weapon // if actor is within range of target's weapon
if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25) if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25)
@ -530,16 +525,18 @@ namespace MWMechanics
} }
} }
// Below behavior for backing up during ranged combat differs from vanilla. // Backing up behaviour
// Vanilla is observed as backing up only as far as fCombatDistance or // Actor backs up slightly further away than opponent's weapon range
// opponent's weapon range, or not backing up if opponent is also using a ranged weapon // (in vanilla - only as far as oponent's weapon range),
if (isDistantCombat && distToTarget < rangeAttack / 4) // or not at all if opponent is using a ranged weapon
if (isDistantCombat)
{ {
// actor should not back up into water // actor should not back up into water
if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(actor), 0.5f)) if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(actor), 0.5f))
return; return;
mMovement.mPosition[1] = -1; if (!targetUsesRanged && distToTarget <= rangeAttackOfTarget*1.5) // Don't back up if the target is wielding ranged weapon
mMovement.mPosition[1] = -1;
} }
} }

@ -115,6 +115,7 @@ namespace MWMechanics
isRanged = false; isRanged = false;
static const float fCombatDistance = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fCombatDistance")->getFloat(); static const float fCombatDistance = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fCombatDistance")->getFloat();
static const float fProjectileMaxSpeed = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fProjectileMaxSpeed")->getFloat();
if (mWeapon.isEmpty()) if (mWeapon.isEmpty())
{ {
@ -128,7 +129,7 @@ namespace MWMechanics
if (weapon->mData.mType >= ESM::Weapon::MarksmanBow) if (weapon->mData.mType >= ESM::Weapon::MarksmanBow)
{ {
isRanged = true; isRanged = true;
return 1000.f; return fProjectileMaxSpeed;
} }
else else
return weapon->mData.mReach * fCombatDistance; return weapon->mData.mReach * fCombatDistance;
@ -181,29 +182,11 @@ namespace MWMechanics
} }
} }
float bestArrowRating = 0;
MWWorld::Ptr bestArrow; MWWorld::Ptr bestArrow;
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) float bestArrowRating = rateAmmo(actor, enemy, bestArrow, ESM::Weapon::Arrow);
{
float rating = rateWeapon(*it, actor, enemy, ESM::Weapon::Arrow);
if (rating > bestArrowRating)
{
bestArrowRating = rating;
bestArrow = *it;
}
}
float bestBoltRating = 0;
MWWorld::Ptr bestBolt; MWWorld::Ptr bestBolt;
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) float bestBoltRating = rateAmmo(actor, enemy, bestBolt, ESM::Weapon::Bolt);
{
float rating = rateWeapon(*it, actor, enemy, ESM::Weapon::Bolt);
if (rating > bestBoltRating)
{
bestBoltRating = rating;
bestBolt = *it;
}
}
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it)
{ {
@ -276,25 +259,9 @@ namespace MWMechanics
} }
} }
float bestArrowRating = 0; float bestArrowRating = rateAmmo(actor, enemy, ESM::Weapon::Arrow);
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it)
{
float rating = rateWeapon(*it, actor, enemy, ESM::Weapon::Arrow);
if (rating > bestArrowRating)
{
bestArrowRating = rating;
}
}
float bestBoltRating = 0; float bestBoltRating = rateAmmo(actor, enemy, ESM::Weapon::Bolt);
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it)
{
float rating = rateWeapon(*it, actor, enemy, ESM::Weapon::Bolt);
if (rating > bestBoltRating)
{
bestBoltRating = rating;
}
}
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it)
{ {

@ -14,6 +14,7 @@
#include "../mwworld/cellstore.hpp" #include "../mwworld/cellstore.hpp"
#include "../mwworld/inventorystore.hpp" #include "../mwworld/inventorystore.hpp"
#include "pathgrid.hpp"
#include "creaturestats.hpp" #include "creaturestats.hpp"
#include "movement.hpp" #include "movement.hpp"
#include "steering.hpp" #include "steering.hpp"
@ -100,14 +101,23 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr
{ {
bool wasShortcutting = mIsShortcutting; bool wasShortcutting = mIsShortcutting;
bool destInLOS = false; bool destInLOS = false;
if (getTypeId() != TypeIdWander) // prohibit shortcuts for AiWander
mIsShortcutting = shortcutPath(start, dest, actor, &destInLOS); // try to shortcut first const MWWorld::Class& actorClass = actor.getClass();
MWBase::World* world = MWBase::Environment::get().getWorld();
// check if actor can move along z-axis
bool actorCanMoveByZ = (actorClass.canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(actor))
|| world->isFlying(actor);
// Prohibit shortcuts for AiWander, if the actor can not move in 3 dimensions.
if (actorCanMoveByZ || getTypeId() != TypeIdWander)
mIsShortcutting = shortcutPath(start, dest, actor, &destInLOS, actorCanMoveByZ); // try to shortcut first
if (!mIsShortcutting) if (!mIsShortcutting)
{ {
if (wasShortcutting || doesPathNeedRecalc(dest, actor.getCell())) // if need to rebuild path if (wasShortcutting || doesPathNeedRecalc(dest, actor.getCell())) // if need to rebuild path
{ {
mPathFinder.buildSyncedPath(start, dest, actor.getCell()); mPathFinder.buildSyncedPath(start, dest, actor.getCell(), getPathGridGraph(actor.getCell()));
mRotateOnTheRunChecks = 3; mRotateOnTheRunChecks = 3;
// give priority to go directly on target if there is minimal opportunity // give priority to go directly on target if there is minimal opportunity
@ -220,20 +230,23 @@ void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor, float dur
} }
} }
bool MWMechanics::AiPackage::shortcutPath(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor, bool *destInLOS) const MWMechanics::PathgridGraph& MWMechanics::AiPackage::getPathGridGraph(const MWWorld::CellStore *cell)
{ {
const MWWorld::Class& actorClass = actor.getClass(); const ESM::CellId& id = cell->getCell()->getCellId();
MWBase::World* world = MWBase::Environment::get().getWorld(); // static cache is OK for now, pathgrids can never change during runtime
typedef std::map<ESM::CellId, std::unique_ptr<MWMechanics::PathgridGraph> > CacheMap;
// check if actor can move along z-axis static CacheMap cache;
bool actorCanMoveByZ = (actorClass.canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(actor)) CacheMap::iterator found = cache.find(id);
|| world->isFlying(actor); if (found == cache.end())
{
// don't use pathgrid when actor can move in 3 dimensions cache.insert(std::make_pair(id, std::unique_ptr<MWMechanics::PathgridGraph>(new MWMechanics::PathgridGraph(cell))));
bool isPathClear = actorCanMoveByZ; }
return *cache[id].get();
}
if (!isPathClear bool MWMechanics::AiPackage::shortcutPath(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor, bool *destInLOS, bool isPathClear)
&& (!mShortcutProhibited || (PathFinder::MakeOsgVec3(mShortcutFailPos) - PathFinder::MakeOsgVec3(startPoint)).length() >= PATHFIND_SHORTCUT_RETRY_DIST)) {
if (!mShortcutProhibited || (PathFinder::MakeOsgVec3(mShortcutFailPos) - PathFinder::MakeOsgVec3(startPoint)).length() >= PATHFIND_SHORTCUT_RETRY_DIST)
{ {
// check if target is clearly visible // check if target is clearly visible
isPathClear = !MWBase::Environment::get().getWorld()->castRay( isPathClear = !MWBase::Environment::get().getWorld()->castRay(

@ -27,6 +27,7 @@ namespace MWMechanics
const float AI_REACTION_TIME = 0.25f; const float AI_REACTION_TIME = 0.25f;
class CharacterController; class CharacterController;
class PathgridGraph;
/// \brief Base class for AI packages /// \brief Base class for AI packages
class AiPackage class AiPackage
@ -110,7 +111,7 @@ namespace MWMechanics
/// If a shortcut is possible then path will be cleared and filled with the destination point. /// If a shortcut is possible then path will be cleared and filled with the destination point.
/// \param destInLOS If not NULL function will return ray cast check result /// \param destInLOS If not NULL function will return ray cast check result
/// \return If can shortcut the path /// \return If can shortcut the path
bool shortcutPath(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor, bool *destInLOS); bool shortcutPath(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor, bool *destInLOS, bool isPathClear);
/// Check if the way to the destination is clear, taking into account actor speed /// Check if the way to the destination is clear, taking into account actor speed
bool checkWayIsClearForActor(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor); bool checkWayIsClearForActor(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor);
@ -119,6 +120,8 @@ namespace MWMechanics
void evadeObstacles(const MWWorld::Ptr& actor, float duration, const ESM::Position& pos); void evadeObstacles(const MWWorld::Ptr& actor, float duration, const ESM::Position& pos);
const PathgridGraph& getPathGridGraph(const MWWorld::CellStore* cell);
// TODO: all this does not belong here, move into temporary storage // TODO: all this does not belong here, move into temporary storage
PathFinder mPathFinder; PathFinder mPathFinder;
ObstacleCheck mObstacleCheck; ObstacleCheck mObstacleCheck;

@ -50,8 +50,13 @@ bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characte
//Set the target desition from the actor //Set the target desition from the actor
ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos;
ESM::Position aPos = actor.getRefData().getPosition();
if (pathTo(actor, dest, duration, 100)) { float pathTolerance = 100.0;
if (pathTo(actor, dest, duration, pathTolerance) &&
std::abs(dest.mZ - aPos.pos[2]) < pathTolerance) // check the true distance in case the target is far away in Z-direction
{
target.getClass().activate(target,actor).get()->execute(actor); //Arrest player when reached target.getClass().activate(target,actor).get()->execute(actor); //Arrest player when reached
return true; return true;
} }

@ -17,6 +17,7 @@
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
#include "../mwworld/cellstore.hpp" #include "../mwworld/cellstore.hpp"
#include "pathgrid.hpp"
#include "creaturestats.hpp" #include "creaturestats.hpp"
#include "steering.hpp" #include "steering.hpp"
#include "movement.hpp" #include "movement.hpp"
@ -217,7 +218,7 @@ namespace MWMechanics
ESM::Pathgrid::Point dest(PathFinder::MakePathgridPoint(mDestination)); ESM::Pathgrid::Point dest(PathFinder::MakePathgridPoint(mDestination));
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos)); ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos));
mPathFinder.buildSyncedPath(start, dest, actor.getCell()); mPathFinder.buildSyncedPath(start, dest, actor.getCell(), getPathGridGraph(actor.getCell()));
if (mPathFinder.isPathConstructed()) if (mPathFinder.isPathConstructed())
storage.setState(Wander_Walking); storage.setState(Wander_Walking);
@ -264,9 +265,20 @@ namespace MWMechanics
getAllowedNodes(actor, currentCell->getCell(), storage); getAllowedNodes(actor, currentCell->getCell(), storage);
} }
bool actorCanMoveByZ = (actor.getClass().canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(actor))
|| MWBase::Environment::get().getWorld()->isFlying(actor);
if(actorCanMoveByZ && mDistance > 0) {
// Typically want to idle for a short time before the next wander
if (Misc::Rng::rollDice(100) >= 92 && storage.mState != Wander_Walking) {
wanderNearStart(actor, storage, mDistance);
}
storage.mCanWanderAlongPathGrid = false;
}
// If the package has a wander distance but no pathgrid is available, // If the package has a wander distance but no pathgrid is available,
// randomly idle or wander near spawn point // randomly idle or wander near spawn point
if(storage.mAllowedNodes.empty() && mDistance > 0 && !storage.mIsWanderingManually) { else if(storage.mAllowedNodes.empty() && mDistance > 0 && !storage.mIsWanderingManually) {
// Typically want to idle for a short time before the next wander // Typically want to idle for a short time before the next wander
if (Misc::Rng::rollDice(100) >= 96) { if (Misc::Rng::rollDice(100) >= 96) {
wanderNearStart(actor, storage, mDistance); wanderNearStart(actor, storage, mDistance);
@ -349,7 +361,7 @@ namespace MWMechanics
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos)); ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos));
// don't take shortcuts for wandering // don't take shortcuts for wandering
mPathFinder.buildSyncedPath(start, dest, actor.getCell()); mPathFinder.buildSyncedPath(start, dest, actor.getCell(), getPathGridGraph(actor.getCell()));
if (mPathFinder.isPathConstructed()) if (mPathFinder.isPathConstructed())
{ {
@ -372,7 +384,7 @@ namespace MWMechanics
do { do {
// Determine a random location within radius of original position // Determine a random location within radius of original position
const float pi = 3.14159265359f; const float pi = 3.14159265359f;
const float wanderRadius = Misc::Rng::rollClosedProbability() * wanderDistance; const float wanderRadius = (0.2f + Misc::Rng::rollClosedProbability() * 0.8f) * wanderDistance;
const float randomDirection = Misc::Rng::rollClosedProbability() * 2.0f * pi; const float randomDirection = Misc::Rng::rollClosedProbability() * 2.0f * pi;
const float destinationX = mInitialActorPosition.x() + wanderRadius * std::cos(randomDirection); const float destinationX = mInitialActorPosition.x() + wanderRadius * std::cos(randomDirection);
const float destinationY = mInitialActorPosition.y() + wanderRadius * std::sin(randomDirection); const float destinationY = mInitialActorPosition.y() + wanderRadius * std::sin(randomDirection);
@ -383,7 +395,7 @@ namespace MWMechanics
// Check if land creature will walk onto water or if water creature will swim onto land // Check if land creature will walk onto water or if water creature will swim onto land
if ((!isWaterCreature && !destinationIsAtWater(actor, mDestination)) || if ((!isWaterCreature && !destinationIsAtWater(actor, mDestination)) ||
(isWaterCreature && !destinationThroughGround(currentPositionVec3f, mDestination))) { (isWaterCreature && !destinationThroughGround(currentPositionVec3f, mDestination))) {
mPathFinder.buildSyncedPath(currentPosition, destinationPosition, actor.getCell()); mPathFinder.buildSyncedPath(currentPosition, destinationPosition, actor.getCell(), getPathGridGraph(actor.getCell()));
mPathFinder.addPointToPath(destinationPosition); mPathFinder.addPointToPath(destinationPosition);
if (mPathFinder.isPathConstructed()) if (mPathFinder.isPathConstructed())
@ -660,13 +672,14 @@ namespace MWMechanics
{ {
unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size()); unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size());
ESM::Pathgrid::Point dest(storage.mAllowedNodes[randNode]); ESM::Pathgrid::Point dest(storage.mAllowedNodes[randNode]);
ToWorldCoordinates(dest, storage.mCell->getCell()); ToWorldCoordinates(dest, storage.mCell->getCell());
// actor position is already in world coordinates // actor position is already in world coordinates
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(actorPos)); ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(actorPos));
// don't take shortcuts for wandering // don't take shortcuts for wandering
mPathFinder.buildSyncedPath(start, dest, actor.getCell()); mPathFinder.buildSyncedPath(start, dest, actor.getCell(), getPathGridGraph(actor.getCell()));
if (mPathFinder.isPathConstructed()) if (mPathFinder.isPathConstructed())
{ {
@ -799,20 +812,80 @@ namespace MWMechanics
int index = Misc::Rng::rollDice(storage.mAllowedNodes.size()); int index = Misc::Rng::rollDice(storage.mAllowedNodes.size());
ESM::Pathgrid::Point dest = storage.mAllowedNodes[index]; ESM::Pathgrid::Point dest = storage.mAllowedNodes[index];
state.moveIn(new AiWanderStorage()); ESM::Pathgrid::Point worldDest = dest;
ToWorldCoordinates(worldDest, actor.getCell()->getCell());
bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::MakeOsgVec3(worldDest), 60);
// add offset only if the selected pathgrid is occupied by another actor
if (isPathGridOccupied)
{
ESM::Pathgrid::PointList points;
getNeighbouringNodes(dest, actor.getCell(), points);
// there are no neighbouring nodes, nowhere to move
if (points.empty())
return;
int initialSize = points.size();
bool isOccupied = false;
// AI will try to move the NPC towards every neighboring node until suitable place will be found
for (int i = 0; i < initialSize; i++)
{
int randomIndex = Misc::Rng::rollDice(points.size());
ESM::Pathgrid::Point connDest = points[randomIndex];
// add an offset towards random neighboring node
osg::Vec3f dir = PathFinder::MakeOsgVec3(connDest) - PathFinder::MakeOsgVec3(dest);
float length = dir.length();
dir.normalize();
for (int j = 1; j <= 3; j++)
{
// move for 5-15% towards random neighboring node
dest = PathFinder::MakePathgridPoint(PathFinder::MakeOsgVec3(dest) + dir * (j * 5 * length / 100.f));
worldDest = dest;
ToWorldCoordinates(worldDest, actor.getCell()->getCell());
isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::MakeOsgVec3(worldDest), 60);
if (!isOccupied)
break;
}
if (!isOccupied)
break;
// Will try an another neighboring node
points.erase(points.begin()+randomIndex);
}
// there is no free space, nowhere to move
if (isOccupied)
return;
}
// place above to prevent moving inside objects, e.g. stairs, because a vector between pathgrids can be underground.
// Adding 20 in adjustPosition() is not enough.
dest.mZ += 60;
dest.mX += OffsetToPreventOvercrowding();
dest.mY += OffsetToPreventOvercrowding();
ToWorldCoordinates(dest, actor.getCell()->getCell()); ToWorldCoordinates(dest, actor.getCell()->getCell());
state.moveIn(new AiWanderStorage());
MWBase::Environment::get().getWorld()->moveObject(actor, static_cast<float>(dest.mX), MWBase::Environment::get().getWorld()->moveObject(actor, static_cast<float>(dest.mX),
static_cast<float>(dest.mY), static_cast<float>(dest.mZ)); static_cast<float>(dest.mY), static_cast<float>(dest.mZ));
actor.getClass().adjustPosition(actor, false); actor.getClass().adjustPosition(actor, false);
} }
int AiWander::OffsetToPreventOvercrowding() void AiWander::getNeighbouringNodes(ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points)
{ {
return static_cast<int>(20 * (Misc::Rng::rollProbability() * 2.0f - 1.0f)); const ESM::Pathgrid *pathgrid =
MWBase::Environment::get().getWorld()->getStore().get<ESM::Pathgrid>().search(*currentCell->getCell());
int index = PathFinder::GetClosestPoint(pathgrid, PathFinder::MakeOsgVec3(dest));
getPathGridGraph(currentCell).getNeighbouringPoints(index, points);
} }
void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell, AiWanderStorage& storage) void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell, AiWanderStorage& storage)
@ -853,7 +926,7 @@ namespace MWMechanics
{ {
osg::Vec3f nodePos(PathFinder::MakeOsgVec3(pathgrid->mPoints[counter])); osg::Vec3f nodePos(PathFinder::MakeOsgVec3(pathgrid->mPoints[counter]));
if((npcPos - nodePos).length2() <= mDistance * mDistance && if((npcPos - nodePos).length2() <= mDistance * mDistance &&
cellStore->isPointConnected(closestPointIndex, counter)) getPathGridGraph(cellStore).isPointConnected(closestPointIndex, counter))
{ {
storage.mAllowedNodes.push_back(pathgrid->mPoints[counter]); storage.mAllowedNodes.push_back(pathgrid->mPoints[counter]);
pointIndex = counter; pointIndex = counter;

@ -104,6 +104,8 @@ namespace MWMechanics
bool mHasDestination; bool mHasDestination;
osg::Vec3f mDestination; osg::Vec3f mDestination;
void getNeighbouringNodes(ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points);
void getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell, AiWanderStorage& storage); void getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell, AiWanderStorage& storage);
void trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes, const PathFinder& pathfinder); void trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes, const PathFinder& pathfinder);

@ -254,6 +254,7 @@ void CharacterController::refreshHitRecoilAnims()
|| mPtr.getClass().getCreatureStats(mPtr).getFatigue().getBase() == 0) || mPtr.getClass().getCreatureStats(mPtr).getFatigue().getBase() == 0)
&& mAnimation->hasAnimation("knockout")) && mAnimation->hasAnimation("knockout"))
{ {
mTimeUntilWake = Misc::Rng::rollClosedProbability() * 2 + 1; // Wake up after 1 to 3 seconds
if (isSwimming && mAnimation->hasAnimation("swimknockout")) if (isSwimming && mAnimation->hasAnimation("swimknockout"))
{ {
mHitState = CharState_SwimKnockOut; mHitState = CharState_SwimKnockOut;
@ -338,7 +339,8 @@ void CharacterController::refreshHitRecoilAnims()
mPtr.getClass().getCreatureStats(mPtr).setBlock(false); mPtr.getClass().getCreatureStats(mPtr).setBlock(false);
mHitState = CharState_None; mHitState = CharState_None;
} }
else if (isKnockedOut() && mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() > 0) else if (isKnockedOut() && mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() > 0
&& mTimeUntilWake <= 0)
{ {
mHitState = isSwimming ? CharState_SwimKnockDown : CharState_KnockDown; mHitState = isSwimming ? CharState_SwimKnockDown : CharState_KnockDown;
mAnimation->disable(mCurrentHit); mAnimation->disable(mCurrentHit);
@ -372,21 +374,29 @@ void CharacterController::refreshJumpAnims(const WeaponInfo* weap, JumpingState
} }
} }
if(mJumpState == JumpState_InAir) if (!mCurrentJump.empty())
{ {
mAnimation->disable(mCurrentJump); mAnimation->disable(mCurrentJump);
mCurrentJump = jumpAnimName; mCurrentJump.clear();
if (mAnimation->hasAnimation("jump")) }
mAnimation->play(mCurrentJump, Priority_Jump, jumpmask, false,
if(mJumpState == JumpState_InAir)
{
if (mAnimation->hasAnimation(jumpAnimName))
{
mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, false,
1.0f, (startAtLoop?"loop start":"start"), "stop", 0.0f, ~0ul); 1.0f, (startAtLoop?"loop start":"start"), "stop", 0.0f, ~0ul);
mCurrentJump = jumpAnimName;
}
} }
else else if (mJumpState == JumpState_Landing)
{ {
mAnimation->disable(mCurrentJump); if (mAnimation->hasAnimation(jumpAnimName))
mCurrentJump.clear(); {
if (mAnimation->hasAnimation("jump"))
mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true, mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true,
1.0f, "loop stop", "stop", 0.0f, 0); 1.0f, "loop stop", "stop", 0.0f, 0);
mCurrentJump = jumpAnimName;
}
} }
} }
} }
@ -759,6 +769,7 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim
, mSecondsOfRunning(0) , mSecondsOfRunning(0)
, mTurnAnimationThreshold(0) , mTurnAnimationThreshold(0)
, mAttackingOrSpell(false) , mAttackingOrSpell(false)
, mTimeUntilWake(0.f)
{ {
if(!mAnimation) if(!mAnimation)
return; return;
@ -1267,7 +1278,16 @@ bool CharacterController::updateWeaponState()
bool animPlaying; bool animPlaying;
if(mAttackingOrSpell) if(mAttackingOrSpell)
{ {
mIdleState = CharState_None; MWWorld::Ptr player = getPlayer();
// We should reset player's idle animation in the first-person mode.
if (mPtr == player && MWBase::Environment::get().getWorld()->isFirstPerson())
mIdleState = CharState_None;
// In other cases we should not break swim and sneak animations
if (mIdleState != CharState_IdleSneak && mIdleState != CharState_IdleSwim)
mIdleState = CharState_None;
if(mUpperBodyState == UpperCharState_WeapEquiped && (mHitState == CharState_None || mHitState == CharState_Block)) if(mUpperBodyState == UpperCharState_WeapEquiped && (mHitState == CharState_None || mHitState == CharState_Block))
{ {
MWBase::Environment::get().getWorld()->breakInvisibility(mPtr); MWBase::Environment::get().getWorld()->breakInvisibility(mPtr);
@ -1277,7 +1297,7 @@ bool CharacterController::updateWeaponState()
// Unset casting flag, otherwise pressing the mouse button down would // Unset casting flag, otherwise pressing the mouse button down would
// continue casting every frame if there is no animation // continue casting every frame if there is no animation
mAttackingOrSpell = false; mAttackingOrSpell = false;
if (mPtr == getPlayer()) if (mPtr == player)
{ {
MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false); MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false);
} }
@ -1287,7 +1307,7 @@ bool CharacterController::updateWeaponState()
// For the player, set the spell we want to cast // For the player, set the spell we want to cast
// This has to be done at the start of the casting animation, // This has to be done at the start of the casting animation,
// *not* when selecting a spell in the GUI (otherwise you could change the spell mid-animation) // *not* when selecting a spell in the GUI (otherwise you could change the spell mid-animation)
if (mPtr == getPlayer()) if (mPtr == player)
{ {
std::string selectedSpell = MWBase::Environment::get().getWindowManager()->getSelectedSpell(); std::string selectedSpell = MWBase::Environment::get().getWindowManager()->getSelectedSpell();
stats.getSpells().setSelectedSpell(selectedSpell); stats.getSpells().setSelectedSpell(selectedSpell);
@ -1299,7 +1319,7 @@ bool CharacterController::updateWeaponState()
MWMechanics::CastSpell cast(mPtr, NULL); MWMechanics::CastSpell cast(mPtr, NULL);
cast.playSpellCastingEffects(spellid); cast.playSpellCastingEffects(spellid);
const ESM::Spell *spell = store.get<ESM::Spell>().find(spellid); const ESM::Spell *spell = store.get<ESM::Spell>().find(spellid);
const ESM::ENAMstruct &lastEffect = spell->mEffects.mList.back(); const ESM::ENAMstruct &lastEffect = spell->mEffects.mList.back();
const ESM::MagicEffect *effect; const ESM::MagicEffect *effect;
@ -1629,6 +1649,9 @@ void CharacterController::update(float duration)
updateMagicEffects(); updateMagicEffects();
if (isKnockedOut())
mTimeUntilWake -= duration;
bool godmode = mPtr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); bool godmode = mPtr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
if(!cls.isActor()) if(!cls.isActor())
@ -1692,7 +1715,6 @@ void CharacterController::update(float duration)
mHasMovedInXY = std::abs(vec.x())+std::abs(vec.y()) > 0.0f; mHasMovedInXY = std::abs(vec.x())+std::abs(vec.y()) > 0.0f;
isrunning = isrunning && mHasMovedInXY; isrunning = isrunning && mHasMovedInXY;
// advance athletics // advance athletics
if(mHasMovedInXY && mPtr == getPlayer()) if(mHasMovedInXY && mPtr == getPlayer())
{ {
@ -1775,7 +1797,7 @@ void CharacterController::update(float duration)
vec.y() *= factor; vec.y() *= factor;
vec.z() = 0.0f; vec.z() = 0.0f;
} }
else if(vec.z() > 0.0f && mJumpState == JumpState_None) else if(vec.z() > 0.0f && mJumpState != JumpState_InAir)
{ {
// Started a jump. // Started a jump.
float z = cls.getJump(mPtr); float z = cls.getJump(mPtr);
@ -1847,7 +1869,8 @@ void CharacterController::update(float duration)
} }
else else
{ {
jumpstate = JumpState_None; jumpstate = mAnimation->isPlaying(mCurrentJump) ? JumpState_Landing : JumpState_None;
vec.z() = 0.0f; vec.z() = 0.0f;
inJump = false; inJump = false;
@ -1877,9 +1900,15 @@ void CharacterController::update(float duration)
else if(rot.z() != 0.0f && !sneak && !(mPtr == getPlayer() && MWBase::Environment::get().getWorld()->isFirstPerson())) else if(rot.z() != 0.0f && !sneak && !(mPtr == getPlayer() && MWBase::Environment::get().getWorld()->isFirstPerson()))
{ {
if(rot.z() > 0.0f) if(rot.z() > 0.0f)
{
movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight; movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight;
mAnimation->disable(mCurrentJump);
}
else if(rot.z() < 0.0f) else if(rot.z() < 0.0f)
{
movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft; movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft;
mAnimation->disable(mCurrentJump);
}
} }
} }
@ -2363,7 +2392,7 @@ float CharacterController::getAttackStrength() const
return mAttackStrength; return mAttackStrength;
} }
void CharacterController::setActive(bool active) void CharacterController::setActive(int active)
{ {
mAnimation->setActive(active); mAnimation->setActive(active);
} }

@ -204,6 +204,8 @@ class CharacterController : public MWRender::Animation::TextKeyListener
bool mAttackingOrSpell; bool mAttackingOrSpell;
float mTimeUntilWake;
void setAttackTypeBasedOnMovement(); void setAttackTypeBasedOnMovement();
void refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force=false); void refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force=false);
@ -290,7 +292,7 @@ public:
float getAttackStrength() const; float getAttackStrength() const;
/// @see Animation::setActive /// @see Animation::setActive
void setActive(bool active); void setActive(int active);
/// Make this character turn its head towards \a target. To turn off head tracking, pass an empty Ptr. /// Make this character turn its head towards \a target. To turn off head tracking, pass an empty Ptr.
void setHeadTrackTarget(const MWWorld::ConstPtr& target); void setHeadTrackTarget(const MWWorld::ConstPtr& target);

@ -566,9 +566,16 @@ namespace MWMechanics
{ {
std::string itFaction = playerFactionIt->first; std::string itFaction = playerFactionIt->first;
// Ignore the faction, if a player was expelled from it.
if (playerStats.getExpelled(itFaction))
continue;
int itReaction = MWBase::Environment::get().getDialogueManager()->getFactionReaction(npcFaction, itFaction); int itReaction = MWBase::Environment::get().getDialogueManager()->getFactionReaction(npcFaction, itFaction);
if (playerFactionIt == playerStats.getFactionRanks().begin() || itReaction < reaction) if (playerFactionIt == playerStats.getFactionRanks().begin() || itReaction < reaction)
{
reaction = static_cast<float>(itReaction); reaction = static_cast<float>(itReaction);
rank = playerFactionIt->second;
}
} }
} }
else else
@ -840,6 +847,45 @@ namespace MWMechanics
mAI = true; mAI = true;
} }
bool MechanicsManager::isBoundItem(const MWWorld::Ptr& item)
{
static std::set<std::string> boundItemIDCache;
// If this is empty then we haven't executed the GMST cache logic yet; or there isn't any sMagicBound* GMST's for some reason
if (boundItemIDCache.empty())
{
// Build a list of known bound item ID's
const MWWorld::Store<ESM::GameSetting> &gameSettings = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
for (MWWorld::Store<ESM::GameSetting>::iterator currentIteration = gameSettings.begin(); currentIteration != gameSettings.end(); ++currentIteration)
{
const ESM::GameSetting &currentSetting = *currentIteration;
std::string currentGMSTID = currentSetting.mId;
Misc::StringUtils::lowerCaseInPlace(currentGMSTID);
// Don't bother checking this GMST if it's not a sMagicBound* one.
const std::string& toFind = "smagicbound";
if (currentGMSTID.compare(0, toFind.length(), toFind) != 0)
continue;
// All sMagicBound* GMST's should be of type string
std::string currentGMSTValue = currentSetting.getString();
Misc::StringUtils::lowerCaseInPlace(currentGMSTValue);
boundItemIDCache.insert(currentGMSTValue);
}
}
// Perform bound item check and assign the Flag_Bound bit if it passes
std::string tempItemID = item.getCellRef().getRefId();
Misc::StringUtils::lowerCaseInPlace(tempItemID);
if (boundItemIDCache.count(tempItemID) != 0)
return true;
return false;
}
bool MechanicsManager::isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) bool MechanicsManager::isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim)
{ {
if (target.isEmpty()) if (target.isEmpty())
@ -1024,7 +1070,7 @@ namespace MWMechanics
} }
void MechanicsManager::itemTaken(const MWWorld::Ptr &ptr, const MWWorld::Ptr &item, const MWWorld::Ptr& container, void MechanicsManager::itemTaken(const MWWorld::Ptr &ptr, const MWWorld::Ptr &item, const MWWorld::Ptr& container,
int count) int count, bool alarm)
{ {
if (ptr != getPlayer()) if (ptr != getPlayer())
return; return;
@ -1053,19 +1099,29 @@ namespace MWMechanics
return; return;
Owner owner; Owner owner;
owner.first = ownerCellRef->getOwner();
owner.second = false; owner.second = false;
if (owner.first.empty()) if (!container.isEmpty() && container.getClass().isActor())
{
// "container" is an actor inventory, so just take actor's ID
owner.first = ownerCellRef->getRefId();
}
else
{ {
owner.first = ownerCellRef->getFaction(); owner.first = ownerCellRef->getOwner();
owner.second = true; if (owner.first.empty())
{
owner.first = ownerCellRef->getFaction();
owner.second = true;
}
} }
Misc::StringUtils::lowerCaseInPlace(owner.first); Misc::StringUtils::lowerCaseInPlace(owner.first);
if (!Misc::StringUtils::ciEqual(item.getCellRef().getRefId(), MWWorld::ContainerStore::sGoldId)) if (!Misc::StringUtils::ciEqual(item.getCellRef().getRefId(), MWWorld::ContainerStore::sGoldId))
mStolenItems[Misc::StringUtils::lowerCase(item.getCellRef().getRefId())][owner] += count; mStolenItems[Misc::StringUtils::lowerCase(item.getCellRef().getRefId())][owner] += count;
commitCrime(ptr, victim, OT_Theft, item.getClass().getValue(item) * count); if (alarm)
commitCrime(ptr, victim, OT_Theft, item.getClass().getValue(item) * count);
} }
@ -1529,6 +1585,11 @@ namespace MWMechanics
mActors.getObjectsInRange(position, radius, objects); mActors.getObjectsInRange(position, radius, objects);
} }
bool MechanicsManager::isAnyActorInRange(const osg::Vec3f &position, float radius)
{
return mActors.isAnyObjectInRange(position, radius);
}
std::list<MWWorld::Ptr> MechanicsManager::getActorsSidingWith(const MWWorld::Ptr& actor) std::list<MWWorld::Ptr> MechanicsManager::getActorsSidingWith(const MWWorld::Ptr& actor)
{ {
return mActors.getActorsSidingWith(actor); return mActors.getActorsSidingWith(actor);
@ -1599,6 +1660,12 @@ namespace MWMechanics
bool MechanicsManager::isAggressive(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target) bool MechanicsManager::isAggressive(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target)
{ {
// Don't become aggressive if a calm effect is active, since it would cause combat to cycle on/off as
// combat is activated here and then canceled by the calm effect
if ((ptr.getClass().isNpc() && ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::CalmHumanoid).getMagnitude() > 0)
|| (!ptr.getClass().isNpc() && ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::CalmCreature).getMagnitude() > 0))
return false;
int disposition = 50; int disposition = 50;
if (ptr.getClass().isNpc()) if (ptr.getClass().isNpc())
disposition = getDerivedDisposition(ptr, true); disposition = getDerivedDisposition(ptr, true);

@ -135,7 +135,7 @@ namespace MWMechanics
/// Utility to check if taking this item is illegal and calling commitCrime if so /// Utility to check if taking this item is illegal and calling commitCrime if so
/// @param container The container the item is in; may be empty for an item in the world /// @param container The container the item is in; may be empty for an item in the world
virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container, virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container,
int count); int count, bool alarm = true);
/// Utility to check if opening (i.e. unlocking) this object is illegal and calling commitCrime if so /// Utility to check if opening (i.e. unlocking) this object is illegal and calling commitCrime if so
virtual void objectOpened (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item); virtual void objectOpened (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item);
/// Attempt sleeping in a bed. If this is illegal, call commitCrime. /// Attempt sleeping in a bed. If this is illegal, call commitCrime.
@ -158,6 +158,9 @@ namespace MWMechanics
virtual void getObjectsInRange (const osg::Vec3f& position, float radius, std::vector<MWWorld::Ptr>& objects); virtual void getObjectsInRange (const osg::Vec3f& position, float radius, std::vector<MWWorld::Ptr>& objects);
virtual void getActorsInRange(const osg::Vec3f &position, float radius, std::vector<MWWorld::Ptr> &objects); virtual void getActorsInRange(const osg::Vec3f &position, float radius, std::vector<MWWorld::Ptr> &objects);
/// Check if there are actors in selected range
virtual bool isAnyActorInRange(const osg::Vec3f &position, float radius);
virtual std::list<MWWorld::Ptr> getActorsSidingWith(const MWWorld::Ptr& actor); virtual std::list<MWWorld::Ptr> getActorsSidingWith(const MWWorld::Ptr& actor);
virtual std::list<MWWorld::Ptr> getActorsFollowing(const MWWorld::Ptr& actor); virtual std::list<MWWorld::Ptr> getActorsFollowing(const MWWorld::Ptr& actor);
virtual std::list<int> getActorsFollowingIndices(const MWWorld::Ptr& actor); virtual std::list<int> getActorsFollowingIndices(const MWWorld::Ptr& actor);
@ -204,6 +207,8 @@ namespace MWMechanics
/// Has the player stolen this item from the given owner? /// Has the player stolen this item from the given owner?
virtual bool isItemStolenFrom(const std::string& itemid, const std::string& ownerid); virtual bool isItemStolenFrom(const std::string& itemid, const std::string& ownerid);
virtual bool isBoundItem(const MWWorld::Ptr& item);
/// @return is \a ptr allowed to take/use \a target or is it a crime? /// @return is \a ptr allowed to take/use \a target or is it a crime?
virtual bool isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim); virtual bool isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim);

@ -5,16 +5,16 @@
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwworld/cellstore.hpp" #include "../mwworld/cellstore.hpp"
#include "pathgrid.hpp"
#include "coordinateconverter.hpp" #include "coordinateconverter.hpp"
namespace namespace
{ {
// Chooses a reachable end pathgrid point. start is assumed reachable. // Chooses a reachable end pathgrid point. start is assumed reachable.
std::pair<int, bool> getClosestReachablePoint(const ESM::Pathgrid* grid, std::pair<int, bool> getClosestReachablePoint(const ESM::Pathgrid* grid,
const MWWorld::CellStore *cell, const MWMechanics::PathgridGraph *graph,
const osg::Vec3f& pos, int start) const osg::Vec3f& pos, int start)
{ {
assert(grid && !grid->mPoints.empty()); assert(grid && !grid->mPoints.empty());
@ -31,7 +31,7 @@ namespace
if (potentialDistBetween < closestDistanceReachable) if (potentialDistBetween < closestDistanceReachable)
{ {
// found a closer one // found a closer one
if (cell->isPointConnected(start, counter)) if (graph->isPointConnected(start, counter))
{ {
closestDistanceReachable = potentialDistBetween; closestDistanceReachable = potentialDistBetween;
closestReachableIndex = counter; closestReachableIndex = counter;
@ -45,7 +45,7 @@ namespace
} }
// post-condition: start and endpoint must be connected // post-condition: start and endpoint must be connected
assert(cell->isPointConnected(start, closestReachableIndex)); assert(graph->isPointConnected(start, closestReachableIndex));
// AiWander has logic that depends on whether a path was created, deleting // AiWander has logic that depends on whether a path was created, deleting
// allowed nodes if not. Hence a path needs to be created even if the start // allowed nodes if not. Hence a path needs to be created even if the start
@ -120,8 +120,8 @@ namespace MWMechanics
} }
PathFinder::PathFinder() PathFinder::PathFinder()
: mPathgrid(NULL), : mPathgrid(NULL)
mCell(NULL) , mCell(NULL)
{ {
} }
@ -169,14 +169,15 @@ namespace MWMechanics
*/ */
void PathFinder::buildPath(const ESM::Pathgrid::Point &startPoint, void PathFinder::buildPath(const ESM::Pathgrid::Point &startPoint,
const ESM::Pathgrid::Point &endPoint, const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell) const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph)
{ {
mPath.clear(); mPath.clear();
// TODO: consider removing mCell / mPathgrid in favor of mPathgridGraph
if(mCell != cell || !mPathgrid) if(mCell != cell || !mPathgrid)
{ {
mCell = cell; mCell = cell;
mPathgrid = MWBase::Environment::get().getWorld()->getStore().get<ESM::Pathgrid>().search(*mCell->getCell()); mPathgrid = pathgridGraph.getPathgrid();
} }
// Refer to AiWander reseach topic on openmw forums for some background. // Refer to AiWander reseach topic on openmw forums for some background.
@ -200,7 +201,7 @@ namespace MWMechanics
int startNode = GetClosestPoint(mPathgrid, startPointInLocalCoords); int startNode = GetClosestPoint(mPathgrid, startPointInLocalCoords);
osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint)); osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint));
std::pair<int, bool> endNode = getClosestReachablePoint(mPathgrid, cell, std::pair<int, bool> endNode = getClosestReachablePoint(mPathgrid, &pathgridGraph,
endPointInLocalCoords, endPointInLocalCoords,
startNode); startNode);
@ -228,7 +229,7 @@ namespace MWMechanics
} }
else else
{ {
mPath = mCell->aStarSearch(startNode, endNode.first); mPath = pathgridGraph.aStarSearch(startNode, endNode.first);
// convert supplied path to world coordinates // convert supplied path to world coordinates
for (std::list<ESM::Pathgrid::Point>::iterator iter(mPath.begin()); iter != mPath.end(); ++iter) for (std::list<ESM::Pathgrid::Point>::iterator iter(mPath.begin()); iter != mPath.end(); ++iter)
@ -301,18 +302,18 @@ namespace MWMechanics
// see header for the rationale // see header for the rationale
void PathFinder::buildSyncedPath(const ESM::Pathgrid::Point &startPoint, void PathFinder::buildSyncedPath(const ESM::Pathgrid::Point &startPoint,
const ESM::Pathgrid::Point &endPoint, const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell) const MWWorld::CellStore* cell, const MWMechanics::PathgridGraph& pathgridGraph)
{ {
if (mPath.size() < 2) if (mPath.size() < 2)
{ {
// if path has one point, then it's the destination. // if path has one point, then it's the destination.
// don't need to worry about bad path for this case // don't need to worry about bad path for this case
buildPath(startPoint, endPoint, cell); buildPath(startPoint, endPoint, cell, pathgridGraph);
} }
else else
{ {
const ESM::Pathgrid::Point oldStart(*getPath().begin()); const ESM::Pathgrid::Point oldStart(*getPath().begin());
buildPath(startPoint, endPoint, cell); buildPath(startPoint, endPoint, cell, pathgridGraph);
if (mPath.size() >= 2) if (mPath.size() >= 2)
{ {
// if 2nd waypoint of new path == 1st waypoint of old, // if 2nd waypoint of new path == 1st waypoint of old,

@ -14,6 +14,8 @@ namespace MWWorld
namespace MWMechanics namespace MWMechanics
{ {
class PathgridGraph;
float distance(const ESM::Pathgrid::Point& point, float x, float y, float); float distance(const ESM::Pathgrid::Point& point, float x, float y, float);
float distance(const ESM::Pathgrid::Point& a, const ESM::Pathgrid::Point& b); float distance(const ESM::Pathgrid::Point& a, const ESM::Pathgrid::Point& b);
float getZAngleToDir(const osg::Vec3f& dir); float getZAngleToDir(const osg::Vec3f& dir);
@ -54,7 +56,7 @@ namespace MWMechanics
void clearPath(); void clearPath();
void buildPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint, void buildPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell); const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph);
bool checkPathCompleted(float x, float y, float tolerance = PathTolerance); bool checkPathCompleted(float x, float y, float tolerance = PathTolerance);
///< \Returns true if we are within \a tolerance units of the last path point. ///< \Returns true if we are within \a tolerance units of the last path point.
@ -89,7 +91,7 @@ namespace MWMechanics
Which results in NPC "running in a circle" back to the just passed waypoint. Which results in NPC "running in a circle" back to the just passed waypoint.
*/ */
void buildSyncedPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint, void buildSyncedPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell); const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph);
void addPointToPath(const ESM::Pathgrid::Point &point) void addPointToPath(const ESM::Pathgrid::Point &point)
{ {

@ -49,7 +49,7 @@ namespace
namespace MWMechanics namespace MWMechanics
{ {
PathgridGraph::PathgridGraph() PathgridGraph::PathgridGraph(const MWWorld::CellStore *cell)
: mCell(NULL) : mCell(NULL)
, mPathgrid(NULL) , mPathgrid(NULL)
, mIsExterior(0) , mIsExterior(0)
@ -58,6 +58,7 @@ namespace MWMechanics
, mSCCId(0) , mSCCId(0)
, mSCCIndex(0) , mSCCIndex(0)
{ {
load(cell);
} }
/* /*
@ -130,6 +131,11 @@ namespace MWMechanics
return true; return true;
} }
const ESM::Pathgrid *PathgridGraph::getPathgrid() const
{
return mPathgrid;
}
// v is the pathgrid point index (some call them vertices) // v is the pathgrid point index (some call them vertices)
void PathgridGraph::recursiveStrongConnect(int v) void PathgridGraph::recursiveStrongConnect(int v)
{ {
@ -214,6 +220,16 @@ namespace MWMechanics
return (mGraph[start].componentId == mGraph[end].componentId); return (mGraph[start].componentId == mGraph[end].componentId);
} }
void PathgridGraph::getNeighbouringPoints(const int index, ESM::Pathgrid::PointList &nodes) const
{
for(int i = 0; i < static_cast<int> (mGraph[index].edges.size()); i++)
{
int neighbourIndex = mGraph[index].edges[i].index;
if (neighbourIndex != index)
nodes.push_back(mPathgrid->mPoints[neighbourIndex]);
}
}
/* /*
* NOTE: Based on buildPath2(), please check git history if interested * NOTE: Based on buildPath2(), please check git history if interested
* Should consider using a 3rd party library version (e.g. boost) * Should consider using a 3rd party library version (e.g. boost)

@ -20,14 +20,19 @@ namespace MWMechanics
class PathgridGraph class PathgridGraph
{ {
public: public:
PathgridGraph(); PathgridGraph(const MWWorld::CellStore* cell);
bool load(const MWWorld::CellStore *cell); bool load(const MWWorld::CellStore *cell);
const ESM::Pathgrid* getPathgrid() const;
// returns true if end point is strongly connected (i.e. reachable // returns true if end point is strongly connected (i.e. reachable
// from start point) both start and end are pathgrid point indexes // from start point) both start and end are pathgrid point indexes
bool isPointConnected(const int start, const int end) const; bool isPointConnected(const int start, const int end) const;
// get neighbouring nodes for index node and put them to "nodes" vector
void getNeighbouringPoints(const int index, ESM::Pathgrid::PointList &nodes) const;
// the input parameters are pathgrid point indexes // the input parameters are pathgrid point indexes
// the output list is in local (internal cells) or world (external // the output list is in local (internal cells) or world (external
// cells) coordinates // cells) coordinates

@ -115,7 +115,7 @@ namespace MWMechanics
return castChance; return castChance;
} }
float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap) float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka)
{ {
bool godmode = actor == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); bool godmode = actor == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
@ -135,6 +135,9 @@ namespace MWMechanics
if (spell->mData.mType != ESM::Spell::ST_Spell) if (spell->mData.mType != ESM::Spell::ST_Spell)
return 100; return 100;
if (checkMagicka && stats.getMagicka().getCurrent() < spell->mData.mCost && !godmode)
return 0;
if (spell->mData.mFlags & ESM::Spell::F_Always) if (spell->mData.mFlags & ESM::Spell::F_Always)
return 100; return 100;
@ -149,11 +152,11 @@ namespace MWMechanics
return std::max(0.f, std::min(100.f, castChance)); return std::max(0.f, std::min(100.f, castChance));
} }
float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap) float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka)
{ {
const ESM::Spell* spell = const ESM::Spell* spell =
MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().find(spellId); MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().find(spellId);
return getSpellSuccessChance(spell, actor, effectiveSchool, cap); return getSpellSuccessChance(spell, actor, effectiveSchool, cap, checkMagicka);
} }
@ -965,9 +968,9 @@ namespace MWMechanics
float y = roll / std::min(x, 100.f); float y = roll / std::min(x, 100.f);
y *= 0.25f * x; y *= 0.25f * x;
if (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) if (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)
effect.mDuration = static_cast<int>(y);
else
effect.mDuration = 1; effect.mDuration = 1;
else
effect.mDuration = static_cast<int>(y);
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude))
{ {
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration))

@ -31,11 +31,12 @@ namespace MWMechanics
* @param actor calculate spell success chance for this actor (depends on actor's skills) * @param actor calculate spell success chance for this actor (depends on actor's skills)
* @param effectiveSchool the spell's effective school (relevant for skill progress) will be written here * @param effectiveSchool the spell's effective school (relevant for skill progress) will be written here
* @param cap cap the result to 100%? * @param cap cap the result to 100%?
* @param checkMagicka check magicka?
* @note actor can be an NPC or a creature * @note actor can be an NPC or a creature
* @return success chance from 0 to 100 (in percent), if cap=false then chance above 100 may be returned. * @return success chance from 0 to 100 (in percent), if cap=false then chance above 100 may be returned.
*/ */
float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool = NULL, bool cap=true); float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool = NULL, bool cap=true, bool checkMagicka=false);
float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool = NULL, bool cap=true); float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool = NULL, bool cap=true, bool checkMagicka=false);
int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor); int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor);
int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor); int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor);

@ -1,4 +1,5 @@
#include "spellpriority.hpp" #include "spellpriority.hpp"
#include "weaponpriority.hpp"
#include <components/esm/loadench.hpp> #include <components/esm/loadench.hpp>
#include <components/esm/loadmgef.hpp> #include <components/esm/loadmgef.hpp>
@ -335,6 +336,12 @@ namespace MWMechanics
return 0.f; return 0.f;
break; break;
case ESM::MagicEffect::BoundLongbow:
// AI should not summon the bow if there is no suitable ammo.
if (rateAmmo(actor, enemy, ESM::Weapon::Arrow) <= 0.f)
return 0.f;
break;
case ESM::MagicEffect::RestoreHealth: case ESM::MagicEffect::RestoreHealth:
case ESM::MagicEffect::RestoreMagicka: case ESM::MagicEffect::RestoreMagicka:
case ESM::MagicEffect::RestoreFatigue: case ESM::MagicEffect::RestoreFatigue:

@ -8,6 +8,7 @@
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
#include "../mwworld/inventorystore.hpp"
#include "npcstats.hpp" #include "npcstats.hpp"
#include "combat.hpp" #include "combat.hpp"
@ -111,6 +112,33 @@ namespace MWMechanics
return rating + bonus; return rating + bonus;
} }
float rateAmmo(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy, MWWorld::Ptr &bestAmmo, ESM::Weapon::Type ammoType)
{
float bestAmmoRating = 0.f;
if (!actor.getClass().hasInventoryStore(actor))
return bestAmmoRating;
MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor);
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it)
{
float rating = rateWeapon(*it, actor, enemy, ammoType);
if (rating > bestAmmoRating)
{
bestAmmoRating = rating;
bestAmmo = *it;
}
}
return bestAmmoRating;
}
float rateAmmo(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy, ESM::Weapon::Type ammoType)
{
MWWorld::Ptr emptyPtr;
return rateAmmo(actor, enemy, emptyPtr, ammoType);
}
float vanillaRateWeaponAndAmmo(const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) float vanillaRateWeaponAndAmmo(const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy)
{ {
const MWWorld::Store<ESM::GameSetting>& gmst = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>(); const MWWorld::Store<ESM::GameSetting>& gmst = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();

@ -1,6 +1,8 @@
#ifndef OPENMW_WEAPON_PRIORITY_H #ifndef OPENMW_WEAPON_PRIORITY_H
#define OPENMW_WEAPON_PRIORITY_H #define OPENMW_WEAPON_PRIORITY_H
#include <components/esm/loadweap.hpp>
#include "../mwworld/ptr.hpp" #include "../mwworld/ptr.hpp"
namespace MWMechanics namespace MWMechanics
@ -8,6 +10,9 @@ namespace MWMechanics
float rateWeapon (const MWWorld::Ptr& item, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, float rateWeapon (const MWWorld::Ptr& item, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy,
int type=-1, float arrowRating=0.f, float boltRating=0.f); int type=-1, float arrowRating=0.f, float boltRating=0.f);
float rateAmmo(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy, MWWorld::Ptr &bestAmmo, ESM::Weapon::Type ammoType);
float rateAmmo(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy, ESM::Weapon::Type ammoType);
float vanillaRateWeaponAndAmmo(const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); float vanillaRateWeaponAndAmmo(const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy);
} }

@ -705,7 +705,7 @@ namespace MWPhysics
if (physFramerate > 0) if (physFramerate > 0)
{ {
mPhysicsDt = 1.f / physFramerate; mPhysicsDt = 1.f / physFramerate;
std::cerr << "Warning: physics framerate was overriden (a new value is " << physFramerate << ")." << std::endl; std::cerr << "Warning: physics framerate was overridden (a new value is " << physFramerate << ")." << std::endl;
} }
} }
} }

@ -486,10 +486,10 @@ namespace MWRender
return mPtr; return mPtr;
} }
void Animation::setActive(bool active) void Animation::setActive(int active)
{ {
if (mSkeleton) if (mSkeleton)
mSkeleton->setActive(active); mSkeleton->setActive(static_cast<SceneUtil::Skeleton::ActiveType>(active));
} }
void Animation::updatePtr(const MWWorld::Ptr &ptr) void Animation::updatePtr(const MWWorld::Ptr &ptr)
@ -536,7 +536,7 @@ namespace MWRender
return mKeyframes->mTextKeys; return mKeyframes->mTextKeys;
} }
void Animation::addAnimSource(const std::string &model) void Animation::addAnimSource(const std::string &model, const std::string& baseModel)
{ {
std::string kfname = model; std::string kfname = model;
Misc::StringUtils::lowerCaseInPlace(kfname); Misc::StringUtils::lowerCaseInPlace(kfname);
@ -565,7 +565,7 @@ namespace MWRender
NodeMap::const_iterator found = nodeMap.find(bonename); NodeMap::const_iterator found = nodeMap.find(bonename);
if (found == nodeMap.end()) if (found == nodeMap.end())
{ {
std::cerr << "Warning: addAnimSource: can't find bone '" + bonename << "' in " << model << " (referenced by " << kfname << ")" << std::endl; std::cerr << "Warning: addAnimSource: can't find bone '" + bonename << "' in " << baseModel << " (referenced by " << kfname << ")" << std::endl;
continue; continue;
} }
@ -625,7 +625,7 @@ namespace MWRender
float Animation::getStartTime(const std::string &groupname) const float Animation::getStartTime(const std::string &groupname) const
{ {
for(AnimSourceList::const_iterator iter(mAnimSources.begin()); iter != mAnimSources.end(); ++iter) for(AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter)
{ {
const NifOsg::TextKeyMap &keys = (*iter)->getTextKeys(); const NifOsg::TextKeyMap &keys = (*iter)->getTextKeys();
@ -638,7 +638,7 @@ namespace MWRender
float Animation::getTextKeyTime(const std::string &textKey) const float Animation::getTextKeyTime(const std::string &textKey) const
{ {
for(AnimSourceList::const_iterator iter(mAnimSources.begin()); iter != mAnimSources.end(); ++iter) for(AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter)
{ {
const NifOsg::TextKeyMap &keys = (*iter)->getTextKeys(); const NifOsg::TextKeyMap &keys = (*iter)->getTextKeys();
@ -1668,7 +1668,7 @@ namespace MWRender
{ {
setObjectRoot(model, false, false, false); setObjectRoot(model, false, false, false);
if (animated) if (animated)
addAnimSource(model); addAnimSource(model, model);
if (!ptr.getClass().getEnchantment(ptr).empty()) if (!ptr.getClass().getEnchantment(ptr).empty())
addGlow(mObjectRoot, getEnchantmentColor(ptr)); addGlow(mObjectRoot, getEnchantmentColor(ptr));

@ -309,11 +309,12 @@ protected:
*/ */
void setObjectRoot(const std::string &model, bool forceskeleton, bool baseonly, bool isCreature); void setObjectRoot(const std::string &model, bool forceskeleton, bool baseonly, bool isCreature);
/** Adds the keyframe controllers in the specified model as a new animation source. Note that the .nif /** Adds the keyframe controllers in the specified model as a new animation source.
* file extension will be replaced with .kf.
* @note Later added animation sources have the highest priority when it comes to finding a particular animation. * @note Later added animation sources have the highest priority when it comes to finding a particular animation.
* @param model The file to add the keyframes for. Note that the .nif file extension will be replaced with .kf.
* @param baseModel The filename of the mObjectRoot, only used for error messages.
*/ */
void addAnimSource(const std::string &model); void addAnimSource(const std::string &model, const std::string& baseModel);
/** Adds an additional light to the given node using the specified ESM record. */ /** Adds an additional light to the given node using the specified ESM record. */
void addExtraLight(osg::ref_ptr<osg::Group> parent, const ESM::Light *light); void addExtraLight(osg::ref_ptr<osg::Group> parent, const ESM::Light *light);
@ -346,7 +347,8 @@ public:
/// Set active flag on the object skeleton, if one exists. /// Set active flag on the object skeleton, if one exists.
/// @see SceneUtil::Skeleton::setActive /// @see SceneUtil::Skeleton::setActive
void setActive(bool active); /// 0 = Inactive, 1 = Active in place, 2 = Active
void setActive(int active);
osg::Group* getOrCreateObjectRoot(); osg::Group* getOrCreateObjectRoot();

@ -33,8 +33,8 @@ CreatureAnimation::CreatureAnimation(const MWWorld::Ptr &ptr,
setObjectRoot(model, false, false, true); setObjectRoot(model, false, false, true);
if((ref->mBase->mFlags&ESM::Creature::Bipedal)) if((ref->mBase->mFlags&ESM::Creature::Bipedal))
addAnimSource("meshes\\xbase_anim.nif"); addAnimSource("meshes\\xbase_anim.nif", model);
addAnimSource(model); addAnimSource(model, model);
} }
} }
@ -51,8 +51,8 @@ CreatureWeaponAnimation::CreatureWeaponAnimation(const MWWorld::Ptr &ptr, const
setObjectRoot(model, true, false, true); setObjectRoot(model, true, false, true);
if((ref->mBase->mFlags&ESM::Creature::Bipedal)) if((ref->mBase->mFlags&ESM::Creature::Bipedal))
addAnimSource("meshes\\xbase_anim.nif"); addAnimSource("meshes\\xbase_anim.nif", model);
addAnimSource(model); addAnimSource(model, model);
mPtr.getClass().getInventoryStore(mPtr).setInvListener(this, mPtr); mPtr.getClass().getInventoryStore(mPtr).setInvListener(this, mPtr);

@ -388,9 +388,6 @@ void NpcAnimation::rebuild()
{ {
updateNpcBase(); updateNpcBase();
if (mAlpha != 1.f)
mResourceSystem->getSceneManager()->recreateShaders(mObjectRoot);
MWBase::Environment::get().getMechanicsManager()->forceStateUpdate(mPtr); MWBase::Environment::get().getMechanicsManager()->forceStateUpdate(mPtr);
} }
@ -485,25 +482,25 @@ void NpcAnimation::updateNpcBase()
{ {
const std::string base = "meshes\\xbase_anim.nif"; const std::string base = "meshes\\xbase_anim.nif";
if (smodel != base) if (smodel != base)
addAnimSource(base); addAnimSource(base, smodel);
addAnimSource(smodel); addAnimSource(smodel, smodel);
if(!isWerewolf) if(!isWerewolf)
{ {
if(mNpc->mModel.length() > 0) if(mNpc->mModel.length() > 0)
addAnimSource(Misc::ResourceHelpers::correctActorModelPath("meshes\\" + mNpc->mModel, mResourceSystem->getVFS())); addAnimSource(Misc::ResourceHelpers::correctActorModelPath("meshes\\" + mNpc->mModel, mResourceSystem->getVFS()), smodel);
if(Misc::StringUtils::lowerCase(mNpc->mRace).find("argonian") != std::string::npos) if(Misc::StringUtils::lowerCase(mNpc->mRace).find("argonian") != std::string::npos)
addAnimSource("meshes\\xargonian_swimkna.nif"); addAnimSource("meshes\\xargonian_swimkna.nif", smodel);
} }
} }
else else
{ {
const std::string base = "meshes\\xbase_anim.1st.nif"; const std::string base = "meshes\\xbase_anim.1st.nif";
if (smodel != base) if (smodel != base)
addAnimSource(base); addAnimSource(base, smodel);
addAnimSource(smodel); addAnimSource(smodel, smodel);
mObjectRoot->setNodeMask(Mask_FirstPerson); mObjectRoot->setNodeMask(Mask_FirstPerson);
mObjectRoot->addCullCallback(new OverrideFieldOfViewCallback(mFirstPersonFieldOfView)); mObjectRoot->addCullCallback(new OverrideFieldOfViewCallback(mFirstPersonFieldOfView));
@ -651,6 +648,9 @@ void NpcAnimation::updateParts()
if (wasArrowAttached) if (wasArrowAttached)
attachArrow(); attachArrow();
if (mAlpha != 1.f)
mResourceSystem->getSceneManager()->recreateShaders(mObjectRoot);
} }
@ -917,6 +917,8 @@ void NpcAnimation::showWeapons(bool showWeapon)
attachArrow(); attachArrow();
} }
} }
if (mAlpha != 1.f)
mResourceSystem->getSceneManager()->recreateShaders(mObjectRoot);
} }
else else
{ {
@ -942,6 +944,8 @@ void NpcAnimation::showCarriedLeft(bool show)
if (iter->getTypeName() == typeid(ESM::Light).name() && mObjectParts[ESM::PRT_Shield]) if (iter->getTypeName() == typeid(ESM::Light).name() && mObjectParts[ESM::PRT_Shield])
addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), iter->get<ESM::Light>()->mBase); addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), iter->get<ESM::Light>()->mBase);
} }
if (mAlpha != 1.f)
mResourceSystem->getSceneManager()->recreateShaders(mObjectRoot);
} }
else else
removeIndividualPart(ESM::PRT_Shield); removeIndividualPart(ESM::PRT_Shield);

@ -215,7 +215,10 @@ namespace MWRender
mObjects.reset(new Objects(mResourceSystem, sceneRoot, mUnrefQueue.get())); mObjects.reset(new Objects(mResourceSystem, sceneRoot, mUnrefQueue.get()));
if (getenv("OPENMW_DONT_PRECOMPILE") == NULL) if (getenv("OPENMW_DONT_PRECOMPILE") == NULL)
{
mViewer->setIncrementalCompileOperation(new osgUtil::IncrementalCompileOperation); mViewer->setIncrementalCompileOperation(new osgUtil::IncrementalCompileOperation);
mViewer->getIncrementalCompileOperation()->setTargetFrameRate(Settings::Manager::getFloat("target framerate", "Cells"));
}
mResourceSystem->getSceneManager()->setIncrementalCompileOperation(mViewer->getIncrementalCompileOperation()); mResourceSystem->getSceneManager()->setIncrementalCompileOperation(mViewer->getIncrementalCompileOperation());
@ -233,6 +236,7 @@ namespace MWRender
else else
mTerrain.reset(new Terrain::TerrainGrid(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage, Mask_Terrain, Mask_PreCompile)); mTerrain.reset(new Terrain::TerrainGrid(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage, Mask_Terrain, Mask_PreCompile));
mTerrain->setDefaultViewer(mViewer->getCamera()); mTerrain->setDefaultViewer(mViewer->getCamera());
mTerrain->setTargetFrameRate(Settings::Manager::getFloat("target framerate", "Cells"));
mCamera.reset(new Camera(mViewer->getCamera())); mCamera.reset(new Camera(mViewer->getCamera()));

@ -1203,6 +1203,18 @@ void SkyManager::create()
mCreated = true; mCreated = true;
} }
class RainCounter : public osgParticle::ConstantRateCounter
{
public:
virtual int numParticlesToCreate(double dt) const
{
// limit dt to avoid large particle emissions if there are jumps in the simulation time
// 0.2 seconds is the same cap as used in Engine's frame loop
dt = std::min(dt, 0.2);
return ConstantRateCounter::numParticlesToCreate(dt);
}
};
class RainShooter : public osgParticle::Shooter class RainShooter : public osgParticle::Shooter
{ {
public: public:
@ -1473,7 +1485,7 @@ void SkyManager::createRain()
placer->setZRange(300, 300); placer->setZRange(300, 300);
emitter->setPlacer(placer); emitter->setPlacer(placer);
osg::ref_ptr<osgParticle::ConstantRateCounter> counter (new osgParticle::ConstantRateCounter); osg::ref_ptr<RainCounter> counter (new RainCounter);
counter->setNumberOfParticlesPerSecondToCreate(600.0); counter->setNumberOfParticlesPerSecondToCreate(600.0);
emitter->setCounter(counter); emitter->setCounter(counter);

@ -272,6 +272,12 @@ public:
void setWaterLevel(float waterLevel) void setWaterLevel(float waterLevel)
{ {
const float refractionScale = std::min(1.0f,std::max(0.0f,
Settings::Manager::getFloat("refraction scale", "Water")));
setViewMatrix(osg::Matrix::scale(1,1,refractionScale) *
osg::Matrix::translate(0,0,(1.0 - refractionScale) * waterLevel));
mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0,0,-1), osg::Vec3d(0,0, waterLevel))); mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0,0,-1), osg::Vec3d(0,0, waterLevel)));
} }

@ -463,7 +463,7 @@ namespace MWScript
const MWWorld::Ptr ref = MWBase::Environment::get().getWorld()->getPtr(name, false); const MWWorld::Ptr ref = MWBase::Environment::get().getWorld()->getPtr(name, false);
// If the objects are in different worldspaces, return a large value (just like vanilla) // If the objects are in different worldspaces, return a large value (just like vanilla)
if (ref.getCell()->getCell()->getCellId().mWorldspace != ref2.getCell()->getCell()->getCellId().mWorldspace) if (!ref.isInCell() || !ref2.isInCell() || ref.getCell()->getCell()->getCellId().mWorldspace != ref2.getCell()->getCell()->getCellId().mWorldspace)
return std::numeric_limits<float>::max(); return std::numeric_limits<float>::max();
double diff[3]; double diff[3];

@ -202,11 +202,6 @@ namespace MWScript
if (!ptr.isInCell()) if (!ptr.isInCell())
return; return;
if (ptr == MWMechanics::getPlayer())
{
MWBase::Environment::get().getWorld()->getPlayer().setTeleported(true);
}
std::string axis = runtime.getStringLiteral (runtime[0].mInteger); std::string axis = runtime.getStringLiteral (runtime[0].mInteger);
runtime.pop(); runtime.pop();
Interpreter::Type_Float pos = runtime[0].mFloat; Interpreter::Type_Float pos = runtime[0].mFloat;
@ -216,6 +211,8 @@ namespace MWScript
float ay = ptr.getRefData().getPosition().pos[1]; float ay = ptr.getRefData().getPosition().pos[1];
float az = ptr.getRefData().getPosition().pos[2]; float az = ptr.getRefData().getPosition().pos[2];
// Note: SetPos does not skip weather transitions in vanilla engine, so we do not call setTeleported(true) here.
MWWorld::Ptr updated = ptr; MWWorld::Ptr updated = ptr;
if(axis == "x") if(axis == "x")
{ {
@ -227,6 +224,17 @@ namespace MWScript
} }
else if(axis == "z") else if(axis == "z")
{ {
// We should not place actors under ground
if (ptr.getClass().isActor())
{
float terrainHeight = -std::numeric_limits<float>::max();
if (ptr.getCell()->isExterior())
terrainHeight = MWBase::Environment::get().getWorld()->getTerrainHeightAt(osg::Vec3f(ax, ay, az));
if (pos < terrainHeight)
pos = terrainHeight;
}
updated = MWBase::Environment::get().getWorld()->moveObject(ptr,ax,ay,pos); updated = MWBase::Environment::get().getWorld()->moveObject(ptr,ax,ay,pos);
} }
else else

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

Loading…
Cancel
Save