summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorwareya <wareya@gmail.com>2022-01-16 17:19:20 -0500
committerwareya <wareya@gmail.com>2022-01-16 17:19:20 -0500
commit784b1888a9de331ba9fe6493ef004036e07468de (patch)
treed82a1d4c1d6288fd687c47a799963501728f2781
parent49d2daee6a3562dc2e11c54541897c16e0cae6cd (diff)
parentfd7e0b74cc640c42d7b121da5fba0d7ec1c240f0 (diff)
Merge branch 'upstream' into movement_tweaks
-rw-r--r--.gitlab-ci.yml174
-rw-r--r--.resubmitted_merge_requests.txt8
-rw-r--r--.travis.yml80
-rw-r--r--AUTHORS.md6
-rw-r--r--CHANGELOG.md42
-rwxr-xr-xCI/before_install.android.sh4
-rwxr-xr-xCI/before_script.android.sh6
-rwxr-xr-xCI/before_script.linux.sh3
-rw-r--r--CI/before_script.msvc.sh18
-rwxr-xr-xCI/before_script.osx.sh2
-rwxr-xr-xCI/install_debian_deps.sh9
-rw-r--r--CMakeLists.txt31
-rw-r--r--README.md2
-rw-r--r--apps/benchmarks/detournavigator/navmeshtilescache.cpp32
-rw-r--r--apps/esmtool/record.cpp6
-rw-r--r--apps/essimporter/converter.cpp5
-rw-r--r--apps/launcher/advancedpage.cpp66
-rw-r--r--apps/launcher/advancedpage.hpp8
-rw-r--r--apps/launcher/datafilespage.cpp76
-rw-r--r--apps/launcher/datafilespage.hpp14
-rw-r--r--apps/launcher/graphicspage.cpp1
-rw-r--r--apps/launcher/maindialog.cpp1
-rw-r--r--apps/launcher/utils/openalutil.cpp10
-rw-r--r--apps/launcher/utils/openalutil.hpp7
-rw-r--r--apps/navmeshtool/CMakeLists.txt22
-rw-r--r--apps/navmeshtool/main.cpp209
-rw-r--r--apps/navmeshtool/navmesh.cpp212
-rw-r--r--apps/navmeshtool/navmesh.hpp23
-rw-r--r--apps/navmeshtool/worldspacedata.cpp330
-rw-r--r--apps/navmeshtool/worldspacedata.hpp97
-rw-r--r--apps/niftest/niftest.cpp14
-rw-r--r--apps/opencs/CMakeLists.txt3
-rw-r--r--apps/opencs/editor.cpp32
-rw-r--r--apps/opencs/model/filter/parser.cpp21
-rw-r--r--apps/opencs/model/prefs/state.cpp1
-rw-r--r--apps/opencs/model/world/columns.cpp2
-rw-r--r--apps/opencs/model/world/idtable.cpp7
-rw-r--r--apps/opencs/model/world/refidadapterimp.hpp16
-rw-r--r--apps/opencs/view/doc/filedialog.cpp9
-rw-r--r--apps/opencs/view/doc/filedialog.hpp2
-rw-r--r--apps/opencs/view/render/lighting.cpp32
-rw-r--r--apps/opencs/view/render/object.cpp2
-rw-r--r--apps/opencs/view/render/scenewidget.cpp5
-rw-r--r--apps/opencs/view/world/previewsubview.cpp2
-rw-r--r--apps/openmw/CMakeLists.txt12
-rw-r--r--apps/openmw/android_main.cpp2
-rw-r--r--apps/openmw/engine.cpp73
-rw-r--r--apps/openmw/main.cpp27
-rw-r--r--apps/openmw/mwbase/inputmanager.hpp6
-rw-r--r--apps/openmw/mwbase/mechanicsmanager.hpp3
-rw-r--r--apps/openmw/mwbase/windowmanager.hpp3
-rw-r--r--apps/openmw/mwbase/world.hpp5
-rw-r--r--apps/openmw/mwclass/creature.cpp6
-rw-r--r--apps/openmw/mwclass/creaturelevlist.cpp8
-rw-r--r--apps/openmw/mwclass/npc.cpp5
-rw-r--r--apps/openmw/mwdialogue/dialoguemanagerimp.cpp68
-rw-r--r--apps/openmw/mwdialogue/dialoguemanagerimp.hpp13
-rw-r--r--apps/openmw/mwdialogue/hypertextparser.cpp15
-rw-r--r--apps/openmw/mwdialogue/keywordsearch.hpp10
-rw-r--r--apps/openmw/mwgui/bookpage.cpp10
-rw-r--r--apps/openmw/mwgui/container.cpp5
-rw-r--r--apps/openmw/mwgui/container.hpp3
-rw-r--r--apps/openmw/mwgui/dialogue.cpp3
-rw-r--r--apps/openmw/mwgui/enchantingdialog.cpp2
-rw-r--r--apps/openmw/mwgui/hud.cpp15
-rw-r--r--apps/openmw/mwgui/hud.hpp2
-rw-r--r--apps/openmw/mwgui/keyboardnavigation.cpp31
-rw-r--r--apps/openmw/mwgui/mapwindow.hpp2
-rw-r--r--apps/openmw/mwgui/messagebox.cpp6
-rw-r--r--apps/openmw/mwgui/messagebox.hpp5
-rw-r--r--apps/openmw/mwgui/quickkeysmenu.cpp58
-rw-r--r--apps/openmw/mwgui/quickkeysmenu.hpp3
-rw-r--r--apps/openmw/mwgui/settingswindow.cpp47
-rw-r--r--apps/openmw/mwgui/settingswindow.hpp4
-rw-r--r--apps/openmw/mwgui/statswindow.cpp3
-rw-r--r--apps/openmw/mwgui/tooltips.cpp5
-rw-r--r--apps/openmw/mwgui/windowmanagerimp.cpp47
-rw-r--r--apps/openmw/mwgui/windowmanagerimp.hpp5
-rw-r--r--apps/openmw/mwinput/actionmanager.cpp72
-rw-r--r--apps/openmw/mwinput/actionmanager.hpp6
-rw-r--r--apps/openmw/mwinput/bindingsmanager.cpp16
-rw-r--r--apps/openmw/mwinput/bindingsmanager.hpp3
-rw-r--r--apps/openmw/mwinput/controllermanager.cpp60
-rw-r--r--apps/openmw/mwinput/controllermanager.hpp9
-rw-r--r--apps/openmw/mwinput/controlswitch.cpp18
-rw-r--r--apps/openmw/mwinput/controlswitch.hpp7
-rw-r--r--apps/openmw/mwinput/inputmanagerimp.cpp15
-rw-r--r--apps/openmw/mwinput/inputmanagerimp.hpp6
-rw-r--r--apps/openmw/mwinput/keyboardmanager.cpp11
-rw-r--r--apps/openmw/mwinput/mousemanager.cpp31
-rw-r--r--apps/openmw/mwinput/mousemanager.hpp2
-rw-r--r--apps/openmw/mwinput/sdlmappings.cpp218
-rw-r--r--apps/openmw/mwlua/actions.cpp30
-rw-r--r--apps/openmw/mwlua/actions.hpp21
-rw-r--r--apps/openmw/mwlua/asyncbindings.cpp22
-rw-r--r--apps/openmw/mwlua/camerabindings.cpp72
-rw-r--r--apps/openmw/mwlua/cellbindings.cpp8
-rw-r--r--apps/openmw/mwlua/context.hpp2
-rw-r--r--apps/openmw/mwlua/inputbindings.cpp344
-rw-r--r--apps/openmw/mwlua/localscripts.cpp4
-rw-r--r--apps/openmw/mwlua/luabindings.cpp111
-rw-r--r--apps/openmw/mwlua/luabindings.hpp7
-rw-r--r--apps/openmw/mwlua/luamanagerimp.cpp129
-rw-r--r--apps/openmw/mwlua/luamanagerimp.hpp17
-rw-r--r--apps/openmw/mwlua/nearbybindings.cpp17
-rw-r--r--apps/openmw/mwlua/objectbindings.cpp15
-rw-r--r--apps/openmw/mwlua/playerscripts.hpp5
-rw-r--r--apps/openmw/mwlua/settingsbindings.cpp7
-rw-r--r--apps/openmw/mwlua/uibindings.cpp240
-rw-r--r--apps/openmw/mwlua/userdataserializer.cpp2
-rw-r--r--apps/openmw/mwlua/worldview.cpp11
-rw-r--r--apps/openmw/mwlua/worldview.hpp21
-rw-r--r--apps/openmw/mwmechanics/activespells.cpp13
-rw-r--r--apps/openmw/mwmechanics/actors.cpp35
-rw-r--r--apps/openmw/mwmechanics/actors.hpp2
-rw-r--r--apps/openmw/mwmechanics/aiactivate.cpp7
-rw-r--r--apps/openmw/mwmechanics/aiactivate.hpp2
-rw-r--r--apps/openmw/mwmechanics/aicombat.cpp9
-rw-r--r--apps/openmw/mwmechanics/aicombat.hpp2
-rw-r--r--apps/openmw/mwmechanics/aiescort.cpp17
-rw-r--r--apps/openmw/mwmechanics/aiescort.hpp4
-rw-r--r--apps/openmw/mwmechanics/aifollow.cpp42
-rw-r--r--apps/openmw/mwmechanics/aifollow.hpp6
-rw-r--r--apps/openmw/mwmechanics/aipackage.hpp2
-rw-r--r--apps/openmw/mwmechanics/aisequence.cpp50
-rw-r--r--apps/openmw/mwmechanics/aisequence.hpp6
-rw-r--r--apps/openmw/mwmechanics/aitravel.cpp11
-rw-r--r--apps/openmw/mwmechanics/aitravel.hpp4
-rw-r--r--apps/openmw/mwmechanics/aiwander.cpp7
-rw-r--r--apps/openmw/mwmechanics/character.cpp16
-rw-r--r--apps/openmw/mwmechanics/combat.cpp7
-rw-r--r--apps/openmw/mwmechanics/creaturestats.cpp8
-rw-r--r--apps/openmw/mwmechanics/creaturestats.hpp2
-rw-r--r--apps/openmw/mwmechanics/difficultyscaling.cpp4
-rw-r--r--apps/openmw/mwmechanics/enchanting.cpp6
-rw-r--r--apps/openmw/mwmechanics/magiceffects.cpp24
-rw-r--r--apps/openmw/mwmechanics/magiceffects.hpp2
-rw-r--r--apps/openmw/mwmechanics/mechanicsmanagerimp.cpp112
-rw-r--r--apps/openmw/mwmechanics/mechanicsmanagerimp.hpp2
-rw-r--r--apps/openmw/mwmechanics/npcstats.cpp2
-rw-r--r--apps/openmw/mwmechanics/pathfinding.cpp13
-rw-r--r--apps/openmw/mwmechanics/spellcasting.cpp3
-rw-r--r--apps/openmw/mwmechanics/spelleffects.cpp65
-rw-r--r--apps/openmw/mwmechanics/spellpriority.cpp41
-rw-r--r--apps/openmw/mwmechanics/spellutil.cpp49
-rw-r--r--apps/openmw/mwmechanics/spellutil.hpp3
-rw-r--r--apps/openmw/mwmechanics/stat.cpp15
-rw-r--r--apps/openmw/mwmechanics/stat.hpp7
-rw-r--r--apps/openmw/mwmechanics/trading.cpp4
-rw-r--r--apps/openmw/mwmechanics/typedaipackage.hpp3
-rw-r--r--apps/openmw/mwmechanics/weaponpriority.cpp3
-rw-r--r--apps/openmw/mwphysics/hasspherecollisioncallback.hpp6
-rw-r--r--apps/openmw/mwphysics/mtphysics.cpp39
-rw-r--r--apps/openmw/mwphysics/mtphysics.hpp7
-rw-r--r--apps/openmw/mwphysics/physicssystem.cpp2
-rw-r--r--apps/openmw/mwphysics/ptrholder.hpp12
-rw-r--r--apps/openmw/mwrender/actoranimation.cpp5
-rw-r--r--apps/openmw/mwrender/actorspaths.cpp6
-rw-r--r--apps/openmw/mwrender/actorspaths.hpp8
-rw-r--r--apps/openmw/mwrender/animation.cpp55
-rw-r--r--apps/openmw/mwrender/bulletdebugdraw.cpp4
-rw-r--r--apps/openmw/mwrender/camera.cpp388
-rw-r--r--apps/openmw/mwrender/camera.hpp179
-rw-r--r--apps/openmw/mwrender/characterpreview.cpp51
-rw-r--r--apps/openmw/mwrender/globalmap.cpp7
-rw-r--r--apps/openmw/mwrender/groundcover.cpp92
-rw-r--r--apps/openmw/mwrender/groundcover.hpp16
-rw-r--r--apps/openmw/mwrender/localmap.cpp19
-rw-r--r--apps/openmw/mwrender/navmesh.cpp79
-rw-r--r--apps/openmw/mwrender/navmesh.hpp34
-rw-r--r--apps/openmw/mwrender/npcanimation.cpp19
-rw-r--r--apps/openmw/mwrender/objectpaging.cpp10
-rw-r--r--apps/openmw/mwrender/objects.cpp8
-rw-r--r--apps/openmw/mwrender/postprocessor.cpp107
-rw-r--r--apps/openmw/mwrender/postprocessor.hpp8
-rw-r--r--apps/openmw/mwrender/recastmesh.cpp6
-rw-r--r--apps/openmw/mwrender/recastmesh.hpp7
-rw-r--r--apps/openmw/mwrender/renderingmanager.cpp79
-rw-r--r--apps/openmw/mwrender/renderingmanager.hpp11
-rw-r--r--apps/openmw/mwrender/ripplesimulation.cpp8
-rw-r--r--apps/openmw/mwrender/screenshotmanager.cpp13
-rw-r--r--apps/openmw/mwrender/sky.cpp17
-rw-r--r--apps/openmw/mwrender/sky.hpp1
-rw-r--r--apps/openmw/mwrender/skyutil.cpp22
-rw-r--r--apps/openmw/mwrender/skyutil.hpp2
-rw-r--r--apps/openmw/mwrender/viewovershoulder.cpp110
-rw-r--r--apps/openmw/mwrender/viewovershoulder.hpp30
-rw-r--r--apps/openmw/mwrender/water.cpp21
-rw-r--r--apps/openmw/mwscript/aiextensions.cpp114
-rw-r--r--apps/openmw/mwscript/skyextensions.cpp10
-rw-r--r--apps/openmw/mwscript/statsextensions.cpp67
-rw-r--r--apps/openmw/mwscript/transformationextensions.cpp26
-rw-r--r--apps/openmw/mwsound/loudness.cpp5
-rw-r--r--apps/openmw/mwsound/openal_output.cpp24
-rw-r--r--apps/openmw/mwsound/openal_output.hpp2
-rw-r--r--apps/openmw/mwsound/sound.hpp111
-rw-r--r--apps/openmw/mwsound/soundmanagerimp.cpp107
-rw-r--r--apps/openmw/mwsound/soundmanagerimp.hpp3
-rw-r--r--apps/openmw/mwsound/volumesettings.cpp2
-rw-r--r--apps/openmw/mwstate/character.cpp12
-rw-r--r--apps/openmw/mwstate/charactermanager.cpp12
-rw-r--r--apps/openmw/mwworld/cellstore.cpp21
-rw-r--r--apps/openmw/mwworld/cellstore.hpp2
-rw-r--r--apps/openmw/mwworld/contentloader.hpp25
-rw-r--r--apps/openmw/mwworld/esmloader.cpp230
-rw-r--r--apps/openmw/mwworld/esmloader.hpp15
-rw-r--r--apps/openmw/mwworld/esmstore.cpp2
-rw-r--r--apps/openmw/mwworld/groundcoverstore.cpp54
-rw-r--r--apps/openmw/mwworld/groundcoverstore.hpp29
-rw-r--r--apps/openmw/mwworld/magiceffects.cpp210
-rw-r--r--apps/openmw/mwworld/magiceffects.hpp17
-rw-r--r--apps/openmw/mwworld/player.cpp64
-rw-r--r--apps/openmw/mwworld/player.hpp4
-rw-r--r--apps/openmw/mwworld/projectilemanager.cpp13
-rw-r--r--apps/openmw/mwworld/scene.cpp66
-rw-r--r--apps/openmw/mwworld/store.cpp73
-rw-r--r--apps/openmw/mwworld/store.hpp41
-rw-r--r--apps/openmw/mwworld/worldimp.cpp205
-rw-r--r--apps/openmw/mwworld/worldimp.hpp18
-rw-r--r--apps/openmw/options.cpp27
-rw-r--r--apps/openmw_test_suite/CMakeLists.txt21
-rw-r--r--apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp242
-rw-r--r--apps/openmw_test_suite/detournavigator/generate.hpp51
-rw-r--r--apps/openmw_test_suite/detournavigator/gettilespositions.cpp2
-rw-r--r--apps/openmw_test_suite/detournavigator/navigator.cpp945
-rw-r--r--apps/openmw_test_suite/detournavigator/navmeshdb.cpp112
-rw-r--r--apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp175
-rw-r--r--apps/openmw_test_suite/detournavigator/operators.hpp2
-rw-r--r--apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp160
-rw-r--r--apps/openmw_test_suite/detournavigator/recastmeshobject.cpp8
-rw-r--r--apps/openmw_test_suite/detournavigator/settings.hpp50
-rw-r--r--apps/openmw_test_suite/detournavigator/settingsutils.cpp4
-rw-r--r--apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp167
-rw-r--r--apps/openmw_test_suite/esmloader/load.cpp23
-rw-r--r--apps/openmw_test_suite/files/hash.cpp55
-rw-r--r--apps/openmw_test_suite/lua/test_i18n.cpp110
-rw-r--r--apps/openmw_test_suite/lua/test_lua.cpp4
-rw-r--r--apps/openmw_test_suite/lua/test_scriptscontainer.cpp44
-rw-r--r--apps/openmw_test_suite/lua/test_serialization.cpp63
-rw-r--r--apps/openmw_test_suite/lua/test_storage.cpp103
-rw-r--r--apps/openmw_test_suite/lua/test_ui_content.cpp97
-rw-r--r--apps/openmw_test_suite/lua/test_utilpackage.cpp17
-rw-r--r--apps/openmw_test_suite/lua/testing_util.hpp7
-rw-r--r--apps/openmw_test_suite/misc/test_resourcehelpers.cpp80
-rw-r--r--apps/openmw_test_suite/mwscript/test_scripts.cpp15
-rw-r--r--apps/openmw_test_suite/mwworld/test_store.cpp13
-rw-r--r--apps/openmw_test_suite/nifloader/testbulletnifloader.cpp6
-rw-r--r--apps/openmw_test_suite/openmw/options.cpp137
-rw-r--r--apps/openmw_test_suite/serialization/binaryreader.cpp (renamed from apps/openmw_test_suite/detournavigator/serialization/binaryreader.cpp)6
-rw-r--r--apps/openmw_test_suite/serialization/binarywriter.cpp (renamed from apps/openmw_test_suite/detournavigator/serialization/binarywriter.cpp)6
-rw-r--r--apps/openmw_test_suite/serialization/format.hpp (renamed from apps/openmw_test_suite/detournavigator/serialization/format.hpp)8
-rw-r--r--apps/openmw_test_suite/serialization/integration.cpp (renamed from apps/openmw_test_suite/detournavigator/serialization/integration.cpp)10
-rw-r--r--apps/openmw_test_suite/serialization/sizeaccumulator.cpp (renamed from apps/openmw_test_suite/detournavigator/serialization/sizeaccumulator.cpp)6
-rw-r--r--apps/wizard/inisettings.cpp2
-rw-r--r--apps/wizard/mainwizard.cpp26
-rw-r--r--cmake/FindGMock.cmake24
-rw-r--r--cmake/FindLZ4.cmake2
-rw-r--r--components/CMakeLists.txt34
-rw-r--r--components/bsa/memorystream.cpp48
-rw-r--r--components/bsa/memorystream.hpp28
-rw-r--r--components/compiler/discardparser.cpp19
-rw-r--r--components/compiler/discardparser.hpp2
-rw-r--r--components/compiler/exprparser.cpp21
-rw-r--r--components/compiler/fileparser.cpp5
-rw-r--r--components/compiler/lineparser.cpp51
-rw-r--r--components/compiler/lineparser.hpp2
-rw-r--r--components/compiler/scanner.cpp4
-rw-r--r--components/compiler/scanner.hpp3
-rw-r--r--components/compiler/stringparser.cpp37
-rw-r--r--components/compiler/stringparser.hpp10
-rw-r--r--components/contentselector/model/contentmodel.cpp56
-rw-r--r--components/contentselector/model/contentmodel.hpp3
-rw-r--r--components/contentselector/view/contentselector.cpp5
-rw-r--r--components/contentselector/view/contentselector.hpp1
-rw-r--r--components/crashcatcher/crashcatcher.cpp5
-rw-r--r--components/crashcatcher/windows_crashcatcher.cpp6
-rw-r--r--components/crashcatcher/windows_crashmonitor.cpp130
-rw-r--r--components/crashcatcher/windows_crashmonitor.hpp20
-rw-r--r--components/crashcatcher/windows_crashshm.hpp1
-rw-r--r--components/debug/debuglog.hpp24
-rw-r--r--components/detournavigator/areatype.hpp15
-rw-r--r--components/detournavigator/asyncnavmeshupdater.cpp625
-rw-r--r--components/detournavigator/asyncnavmeshupdater.hpp143
-rw-r--r--components/detournavigator/cachedrecastmeshmanager.cpp29
-rw-r--r--components/detournavigator/cachedrecastmeshmanager.hpp13
-rw-r--r--components/detournavigator/dbrefgeometryobject.hpp61
-rw-r--r--components/detournavigator/debug.cpp3
-rw-r--r--components/detournavigator/debug.hpp45
-rw-r--r--components/detournavigator/findrandompointaroundcircle.cpp2
-rw-r--r--components/detournavigator/findrandompointaroundcircle.hpp4
-rw-r--r--components/detournavigator/findsmoothpath.hpp13
-rw-r--r--components/detournavigator/generatenavmeshtile.cpp95
-rw-r--r--components/detournavigator/generatenavmeshtile.hpp71
-rw-r--r--components/detournavigator/gettilespositions.hpp16
-rw-r--r--components/detournavigator/heightfieldshape.hpp19
-rw-r--r--components/detournavigator/makenavmesh.cpp391
-rw-r--r--components/detournavigator/makenavmesh.hpp29
-rw-r--r--components/detournavigator/navigator.cpp39
-rw-r--r--components/detournavigator/navigator.hpp89
-rw-r--r--components/detournavigator/navigatorimpl.cpp38
-rw-r--r--components/detournavigator/navigatorimpl.hpp10
-rw-r--r--components/detournavigator/navigatorstub.hpp7
-rw-r--r--components/detournavigator/navigatorutils.cpp37
-rw-r--r--components/detournavigator/navigatorutils.hpp68
-rw-r--r--components/detournavigator/navmeshcacheitem.cpp56
-rw-r--r--components/detournavigator/navmeshcacheitem.hpp42
-rw-r--r--components/detournavigator/navmeshdb.cpp296
-rw-r--r--components/detournavigator/navmeshdb.hpp153
-rw-r--r--components/detournavigator/navmeshdbutils.cpp63
-rw-r--r--components/detournavigator/navmeshdbutils.hpp17
-rw-r--r--components/detournavigator/navmeshmanager.cpp57
-rw-r--r--components/detournavigator/navmeshmanager.hpp12
-rw-r--r--components/detournavigator/navmeshtilescache.cpp9
-rw-r--r--components/detournavigator/navmeshtilescache.hpp6
-rw-r--r--components/detournavigator/navmeshtileview.cpp83
-rw-r--r--components/detournavigator/navmeshtileview.hpp2
-rw-r--r--components/detournavigator/objectid.hpp5
-rw-r--r--components/detournavigator/objecttransform.hpp27
-rw-r--r--components/detournavigator/offmeshconnectionsmanager.cpp6
-rw-r--r--components/detournavigator/offmeshconnectionsmanager.hpp6
-rw-r--r--components/detournavigator/preparednavmeshdata.cpp29
-rw-r--r--components/detournavigator/preparednavmeshdata.hpp2
-rw-r--r--components/detournavigator/preparednavmeshdatatuple.hpp22
-rw-r--r--components/detournavigator/raycast.cpp2
-rw-r--r--components/detournavigator/raycast.hpp4
-rw-r--r--components/detournavigator/recast.cpp80
-rw-r--r--components/detournavigator/recast.hpp21
-rw-r--r--components/detournavigator/recastmesh.cpp28
-rw-r--r--components/detournavigator/recastmesh.hpp89
-rw-r--r--components/detournavigator/recastmeshbuilder.cpp64
-rw-r--r--components/detournavigator/recastmeshbuilder.hpp17
-rw-r--r--components/detournavigator/recastmeshmanager.cpp48
-rw-r--r--components/detournavigator/recastmeshmanager.hpp30
-rw-r--r--components/detournavigator/recastmeshobject.cpp3
-rw-r--r--components/detournavigator/recastmeshobject.hpp22
-rw-r--r--components/detournavigator/recastmeshprovider.hpp33
-rw-r--r--components/detournavigator/serialization.cpp272
-rw-r--r--components/detournavigator/serialization.hpp29
-rw-r--r--components/detournavigator/settings.cpp93
-rw-r--r--components/detournavigator/settings.hpp42
-rw-r--r--components/detournavigator/settingsutils.hpp61
-rw-r--r--components/detournavigator/tilebounds.hpp9
-rw-r--r--components/detournavigator/tilecachedrecastmeshmanager.cpp141
-rw-r--r--components/detournavigator/tilecachedrecastmeshmanager.hpp46
-rw-r--r--components/detournavigator/version.hpp26
-rw-r--r--components/esm/aipackage.hpp8
-rw-r--r--components/esm/aisequence.cpp54
-rw-r--r--components/esm/aisequence.hpp4
-rw-r--r--components/esm/cellref.cpp5
-rw-r--r--components/esm/cellref.hpp5
-rw-r--r--components/esm/creaturestats.cpp14
-rw-r--r--components/esm/creaturestats.hpp1
-rw-r--r--components/esm/defs.hpp10
-rw-r--r--components/esm/esmreader.cpp3
-rw-r--r--components/esm/esmreader.hpp6
-rw-r--r--components/esm/loadland.cpp2
-rw-r--r--components/esm/loadmgef.cpp28
-rw-r--r--components/esm/luascripts.cpp4
-rw-r--r--components/esm/luascripts.hpp8
-rw-r--r--components/esm/player.cpp48
-rw-r--r--components/esm/player.hpp5
-rw-r--r--components/esm/savedgame.cpp2
-rw-r--r--components/esmloader/load.cpp21
-rw-r--r--components/fallback/validate.cpp9
-rw-r--r--components/fallback/validate.hpp2
-rw-r--r--components/files/configfileparser.cpp297
-rw-r--r--components/files/configfileparser.hpp17
-rw-r--r--components/files/configurationmanager.cpp57
-rw-r--r--components/files/configurationmanager.hpp10
-rw-r--r--components/files/escape.cpp145
-rw-r--r--components/files/escape.hpp191
-rw-r--r--components/files/hash.cpp41
-rw-r--r--components/files/hash.hpp14
-rw-r--r--components/fontloader/fontloader.cpp9
-rw-r--r--components/interpreter/defines.cpp3
-rw-r--r--components/lua/configuration.cpp15
-rw-r--r--components/lua/i18n.cpp111
-rw-r--r--components/lua/i18n.hpp41
-rw-r--r--components/lua/luastate.cpp91
-rw-r--r--components/lua/luastate.hpp34
-rw-r--r--components/lua/scriptscontainer.cpp49
-rw-r--r--components/lua/scriptscontainer.hpp31
-rw-r--r--components/lua/serialization.cpp90
-rw-r--r--components/lua/serialization.hpp6
-rw-r--r--components/lua/storage.cpp198
-rw-r--r--components/lua/storage.hpp81
-rw-r--r--components/lua/utilpackage.cpp7
-rw-r--r--components/lua_ui/content.cpp106
-rw-r--r--components/lua_ui/content.hpp41
-rw-r--r--components/lua_ui/element.cpp165
-rw-r--r--components/lua_ui/element.hpp31
-rw-r--r--components/lua_ui/layers.hpp55
-rw-r--r--components/lua_ui/properties.hpp64
-rw-r--r--components/lua_ui/text.cpp29
-rw-r--r--components/lua_ui/text.hpp27
-rw-r--r--components/lua_ui/textedit.cpp10
-rw-r--r--components/lua_ui/textedit.hpp18
-rw-r--r--components/lua_ui/widget.cpp264
-rw-r--r--components/lua_ui/widget.hpp99
-rw-r--r--components/lua_ui/widgetlist.cpp31
-rw-r--r--components/lua_ui/widgetlist.hpp14
-rw-r--r--components/lua_ui/window.cpp96
-rw-r--r--components/lua_ui/window.hpp37
-rw-r--r--components/misc/convert.hpp10
-rw-r--r--components/misc/errorMarker.cpp1390
-rw-r--r--components/misc/errorMarker.hpp11
-rw-r--r--components/misc/hash.hpp13
-rw-r--r--components/misc/osguservalues.cpp6
-rw-r--r--components/misc/osguservalues.hpp14
-rw-r--r--components/misc/resourcehelpers.cpp22
-rw-r--r--components/misc/resourcehelpers.hpp2
-rw-r--r--components/misc/stringops.hpp15
-rw-r--r--components/misc/typetraits.hpp19
-rw-r--r--components/misc/utf8stream.hpp6
-rw-r--r--components/myguiplatform/myguicompat.h12
-rw-r--r--components/myguiplatform/myguidatamanager.cpp8
-rw-r--r--components/myguiplatform/myguidatamanager.hpp10
-rw-r--r--components/myguiplatform/myguirendermanager.cpp7
-rw-r--r--components/myguiplatform/myguirendermanager.hpp14
-rw-r--r--components/myguiplatform/myguitexture.cpp2
-rw-r--r--components/myguiplatform/myguitexture.hpp23
-rw-r--r--components/myguiplatform/scalinglayer.cpp4
-rw-r--r--components/nif/controller.cpp9
-rw-r--r--components/nif/controller.hpp6
-rw-r--r--components/nif/data.cpp15
-rw-r--r--components/nif/data.hpp5
-rw-r--r--components/nif/extra.cpp34
-rw-r--r--components/nif/extra.hpp25
-rw-r--r--components/nif/niffile.cpp23
-rw-r--r--components/nif/niffile.hpp5
-rw-r--r--components/nif/nifkey.hpp71
-rw-r--r--components/nif/nifstream.hpp3
-rw-r--r--components/nif/node.hpp41
-rw-r--r--components/nif/physics.cpp313
-rw-r--r--components/nif/physics.hpp332
-rw-r--r--components/nif/property.cpp56
-rw-r--r--components/nif/property.hpp63
-rw-r--r--components/nif/record.hpp19
-rw-r--r--components/nif/recordptr.hpp13
-rw-r--r--components/nifbullet/bulletnifloader.cpp3
-rw-r--r--components/nifosg/controller.cpp61
-rw-r--r--components/nifosg/controller.hpp9
-rw-r--r--components/nifosg/nifloader.cpp140
-rw-r--r--components/process/processinvoker.cpp14
-rw-r--r--components/process/processinvoker.hpp6
-rw-r--r--components/resource/bulletshape.cpp2
-rw-r--r--components/resource/bulletshape.hpp11
-rw-r--r--components/resource/bulletshapemanager.cpp9
-rw-r--r--components/resource/scenemanager.cpp95
-rw-r--r--components/resource/scenemanager.hpp4
-rw-r--r--components/resource/stats.cpp2
-rw-r--r--components/sceneutil/agentpath.cpp4
-rw-r--r--components/sceneutil/agentpath.hpp4
-rw-r--r--components/sceneutil/controller.cpp15
-rw-r--r--components/sceneutil/controller.hpp12
-rw-r--r--components/sceneutil/depth.cpp63
-rw-r--r--components/sceneutil/depth.hpp117
-rw-r--r--components/sceneutil/detourdebugdraw.cpp32
-rw-r--r--components/sceneutil/detourdebugdraw.hpp7
-rw-r--r--components/sceneutil/lightmanager.cpp524
-rw-r--r--components/sceneutil/lightmanager.hpp53
-rw-r--r--components/sceneutil/morphgeometry.cpp22
-rw-r--r--components/sceneutil/mwshadowtechnique.cpp37
-rw-r--r--components/sceneutil/mwshadowtechnique.hpp8
-rw-r--r--components/sceneutil/navmesh.cpp254
-rw-r--r--components/sceneutil/navmesh.hpp8
-rw-r--r--components/sceneutil/optimizer.cpp8
-rw-r--r--components/sceneutil/osgacontroller.cpp2
-rw-r--r--components/sceneutil/recastmesh.cpp17
-rw-r--r--components/sceneutil/recastmesh.hpp4
-rw-r--r--components/sceneutil/rtt.cpp2
-rw-r--r--components/sceneutil/serialize.cpp3
-rw-r--r--components/sceneutil/shadow.cpp8
-rw-r--r--components/sceneutil/shadowsbin.cpp17
-rw-r--r--components/sceneutil/shadowsbin.hpp26
-rw-r--r--components/sceneutil/util.cpp103
-rw-r--r--components/sceneutil/util.hpp32
-rw-r--r--components/sceneutil/waterutil.cpp4
-rw-r--r--components/sdlutil/sdlmappings.cpp251
-rw-r--r--components/sdlutil/sdlmappings.hpp (renamed from apps/openmw/mwinput/sdlmappings.hpp)12
-rw-r--r--components/serialization/binaryreader.hpp (renamed from components/detournavigator/serialization/binaryreader.hpp)30
-rw-r--r--components/serialization/binarywriter.hpp (renamed from components/detournavigator/serialization/binarywriter.hpp)39
-rw-r--r--components/serialization/format.hpp (renamed from components/detournavigator/serialization/format.hpp)25
-rw-r--r--components/serialization/sizeaccumulator.hpp (renamed from components/detournavigator/serialization/sizeaccumulator.hpp)10
-rw-r--r--components/settings/categories.hpp2
-rw-r--r--components/settings/settings.cpp40
-rw-r--r--components/settings/settings.hpp27
-rw-r--r--components/shader/shadermanager.cpp10
-rw-r--r--components/shader/shadermanager.hpp3
-rw-r--r--components/shader/shadervisitor.cpp188
-rw-r--r--components/shader/shadervisitor.hpp7
-rw-r--r--components/sqlite3/request.hpp8
-rw-r--r--components/sqlite3/types.hpp15
-rw-r--r--components/terrain/buffercache.cpp4
-rw-r--r--components/terrain/chunkmanager.cpp3
-rw-r--r--components/terrain/material.cpp6
-rw-r--r--components/terrain/quadtreenode.cpp43
-rw-r--r--components/terrain/quadtreenode.hpp2
-rw-r--r--components/terrain/quadtreeworld.cpp63
-rw-r--r--components/terrain/quadtreeworld.hpp8
-rw-r--r--components/terrain/viewdata.cpp1
-rw-r--r--components/terrain/viewdata.hpp2
-rw-r--r--components/vfs/filesystemarchive.cpp2
-rw-r--r--components/widgets/fontwrapper.hpp6
-rw-r--r--components/widgets/numericeditbox.cpp2
-rw-r--r--docker/Dockerfile.ubuntu23
-rw-r--r--docker/README.md17
-rwxr-xr-xdocker/build.sh13
-rwxr-xr-xdocs/source/generate_luadoc.sh24
-rw-r--r--docs/source/manuals/installation/install-game-files.rst8
-rw-r--r--docs/source/reference/documentationHowTo.rst6
-rw-r--r--docs/source/reference/lua-scripting/api.rst31
-rw-r--r--docs/source/reference/lua-scripting/engine_handlers.rst5
-rw-r--r--docs/source/reference/lua-scripting/interface_camera.rst6
-rw-r--r--docs/source/reference/lua-scripting/openmw_aux_calendar.rst5
-rw-r--r--docs/source/reference/lua-scripting/openmw_aux_time.rst5
-rw-r--r--docs/source/reference/lua-scripting/openmw_camera.rst5
-rw-r--r--docs/source/reference/lua-scripting/openmw_settings.rst5
-rw-r--r--docs/source/reference/lua-scripting/openmw_storage.rst5
-rw-r--r--docs/source/reference/lua-scripting/overview.rst48
-rw-r--r--docs/source/reference/lua-scripting/user_interface.rst172
-rw-r--r--docs/source/reference/lua-scripting/widgets/widget.rst77
-rw-r--r--docs/source/reference/modding/custom-models/index.rst1
-rw-r--r--docs/source/reference/modding/custom-models/pipeline-blender-collada-static-models.rst129
-rw-r--r--docs/source/reference/modding/settings/game.rst14
-rw-r--r--docs/source/reference/modding/settings/general.rst2
-rw-r--r--docs/source/reference/modding/settings/groundcover.rst2
-rw-r--r--docs/source/reference/modding/settings/lua.rst23
-rw-r--r--docs/source/reference/modding/settings/map.rst1
-rw-r--r--docs/source/reference/modding/settings/navigator.rst49
-rw-r--r--docs/source/reference/modding/settings/shaders.rst20
-rw-r--r--docs/source/reference/modding/settings/water.rst15
-rw-r--r--docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst12
-rw-r--r--extern/CMakeLists.txt2
-rw-r--r--extern/i18n.lua/CMakeLists.txt17
-rw-r--r--extern/i18n.lua/LICENSE22
-rw-r--r--extern/i18n.lua/README.md164
-rw-r--r--extern/i18n.lua/i18n/init.lua188
-rw-r--r--extern/i18n.lua/i18n/interpolate.lua60
-rw-r--r--extern/i18n.lua/i18n/plural.lua280
-rw-r--r--extern/i18n.lua/i18n/variants.lua49
-rw-r--r--extern/i18n.lua/i18n/version.lua1
-rw-r--r--extern/osg-ffmpeg-videoplayer/videostate.cpp108
-rw-r--r--extern/osg-ffmpeg-videoplayer/videostate.hpp24
-rw-r--r--extern/smhasher/CMakeLists.txt2
-rw-r--r--extern/smhasher/MurmurHash3.cpp152
-rw-r--r--extern/smhasher/MurmurHash3.h33
-rw-r--r--files/CMakeLists.txt1
-rw-r--r--files/builtin_scripts/CMakeLists.txt30
-rw-r--r--files/builtin_scripts/builtin.omwscripts1
-rw-r--r--files/builtin_scripts/i18n/Calendar/en.lua42
-rw-r--r--files/builtin_scripts/openmw_aux/calendar.lua159
-rw-r--r--files/builtin_scripts/openmw_aux/time.lua104
-rw-r--r--files/builtin_scripts/openmw_aux/util.lua67
-rw-r--r--files/builtin_scripts/scripts/omw/camera.lua247
-rw-r--r--files/builtin_scripts/scripts/omw/head_bobbing.lua51
-rw-r--r--files/builtin_scripts/scripts/omw/third_person.lua139
-rw-r--r--files/lua_api/openmw/async.lua16
-rw-r--r--files/lua_api/openmw/camera.lua171
-rw-r--r--files/lua_api/openmw/core.lua63
-rw-r--r--files/lua_api/openmw/input.lua149
-rw-r--r--files/lua_api/openmw/settings.lua14
-rw-r--r--files/lua_api/openmw/storage.lua96
-rw-r--r--files/lua_api/openmw/ui.lua133
-rw-r--r--files/lua_api/openmw/util.lua16
-rw-r--r--files/lua_api/openmw/world.lua30
-rw-r--r--files/lua_api/os.doclua64
-rw-r--r--files/mygui/CMakeLists.txt4
-rw-r--r--files/mygui/openmw_hud.layout37
-rw-r--r--files/mygui/openmw_layers.xml1
-rw-r--r--files/mygui/openmw_resources.xml8
-rw-r--r--files/mygui/openmw_settings_window.layout10
-rw-r--r--files/openmw.cfg1
-rw-r--r--files/openmw.cfg.local1
-rw-r--r--files/settings-default.cfg34
-rw-r--r--files/shaders/CMakeLists.txt5
-rw-r--r--files/shaders/nv_default_fragment.glsl5
-rw-r--r--files/shaders/objects_fragment.glsl12
-rw-r--r--files/shaders/softparticles.glsl32
-rw-r--r--files/shaders/water_fragment.glsl147
-rw-r--r--files/shaders/water_vertex.glsl9
-rw-r--r--files/ui/advancedpage.ui45
-rw-r--r--files/ui/datafilespage.ui84
-rw-r--r--files/vfs/CMakeLists.txt4
-rwxr-xr-xscripts/find_missing_merge_requests.py103
-rwxr-xr-xscripts/osg_stats.py43
586 files changed, 20102 insertions, 6602 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a07dec1d68..8275df7572 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,17 +3,25 @@
stages:
- build
-.Debian_Image:
+# https://blog.nimbleways.com/let-s-make-faster-gitlab-ci-cd-pipelines/
+variables:
+ FF_USE_NEW_SHELL_ESCAPE: "true"
+ FF_USE_FASTZIP: "true"
+ # These can be specified per job or per pipeline
+ ARTIFACT_COMPRESSION_LEVEL: "fast"
+ CACHE_COMPRESSION_LEVEL: "fast"
+
+.Ubuntu_Image:
tags:
- docker
- linux
- image: debian:bullseye
+ image: ubuntu:focal
rules:
- if: $CI_PIPELINE_SOURCE == "push"
-.Debian:
- extends: .Debian_Image
+.Ubuntu:
+ extends: .Ubuntu_Image
cache:
paths:
- apt-cache/
@@ -35,7 +43,7 @@ stages:
- build/install/
Clang_Tidy:
- extends: .Debian_Image
+ extends: .Ubuntu_Image
stage: build
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
@@ -50,34 +58,41 @@ Clang_Tidy:
CXX: clang++
CI_CLANG_TIDY: 1
timeout: 8h
+ artifacts:
+ paths: []
+ expire_in: 1 minute
Coverity:
- extends: .Debian_Image
+ extends: .Ubuntu_Image
stage: build
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
before_script:
- - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic coverity
+ - CI/install_debian_deps.sh clang openmw-deps openmw-deps-dynamic coverity
- curl -o /tmp/cov-analysis-linux64.tgz https://scan.coverity.com/download/linux64 --form project=$COVERITY_SCAN_PROJECT_NAME --form token=$COVERITY_SCAN_TOKEN
- tar xfz /tmp/cov-analysis-linux64.tgz
script:
- CI/before_script.linux.sh
# Remove the specific targets and build everything once we can do it under 3h
- - cov-analysis-linux64-*/bin/cov-build --dir cov-int cmake --build build -- -j $(nproc) openmw esmtool bsatool niftest openmw-wizard openmw-launcher openmw-iniimporter openmw-essimporter
+ - cov-analysis-linux64-*/bin/cov-build --dir cov-int cmake --build build -- -j $(nproc) openmw esmtool bsatool niftest openmw-wizard openmw-launcher openmw-iniimporter openmw-essimporter openmw-navmeshtool openmw-cs
after_script:
- tar cfz cov-int.tar.gz cov-int
- curl https://scan.coverity.com/builds?project=$COVERITY_SCAN_PROJECT_NAME
--form token=$COVERITY_SCAN_TOKEN --form email=$GITLAB_USER_EMAIL
- --form file=@cov-int.tar.gz --form version="`git describe --tags`"
- --form description="`git describe --tags` / $CI_COMMIT_TITLE / $CI_COMMIT_REF_NAME:$CI_PIPELINE_ID"
+ --form file=@cov-int.tar.gz --form version="$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA"
+ --form description="CI_COMMIT_SHORT_SHA / $CI_COMMIT_TITLE / $CI_COMMIT_REF_NAME:$CI_PIPELINE_ID"
variables:
- CC: gcc
- CXX: g++
+ CC: clang
+ CXX: clang++
+ CXXFLAGS: -O0
+ artifacts:
+ paths:
+ - /builds/OpenMW/openmw/cov-int/build-log.txt
-Debian_GCC:
- extends: .Debian
+Ubuntu_GCC:
+ extends: .Ubuntu
cache:
- key: Debian_GCC.v2
+ key: Ubuntu_GCC.v2
before_script:
- CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic
variables:
@@ -87,27 +102,41 @@ Debian_GCC:
# When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks.
timeout: 2h
-Debian_GCC_tests:
- extends: Debian_GCC
+Ubuntu_GCC_tests:
+ extends: Ubuntu_GCC
cache:
- key: Debian_GCC_tests.v2
+ key: Ubuntu_GCC_tests.v2
variables:
CCACHE_SIZE: 1G
BUILD_TESTS_ONLY: 1
+ artifacts:
+ paths: []
+ expire_in: 1 minute
-Debian_GCC_tests_Debug:
- extends: Debian_GCC
+Ubuntu_GCC_tests_Debug:
+ extends: Ubuntu_GCC
cache:
- key: Debian_GCC_tests_Debug.v1
+ key: Ubuntu_GCC_tests_Debug.v1
variables:
CCACHE_SIZE: 1G
BUILD_TESTS_ONLY: 1
CMAKE_BUILD_TYPE: Debug
+ artifacts:
+ paths: []
+ expire_in: 1 minute
-Debian_GCC_Static_Deps:
- extends: Debian_GCC
+Ubuntu_GCC_Static_Deps:
+ extends: Ubuntu_GCC
+ rules:
+ - if: $CI_PIPELINE_SOURCE == "push"
+ changes:
+ - "**/CMakeLists.txt"
+ - "cmake/**/*"
+ - "CI/**/*"
+ - ".gitlab-ci.yml"
+ allow_failure: true
cache:
- key: Debian_GCC_Static_Deps
+ key: Ubuntu_GCC_Static_Deps
paths:
- apt-cache/
- ccache/
@@ -118,20 +147,23 @@ Debian_GCC_Static_Deps:
CI_OPENMW_USE_STATIC_DEPS: 1
timeout: 3h
-Debian_GCC_Static_Deps_tests:
- extends: Debian_GCC_Static_Deps
+Ubuntu_GCC_Static_Deps_tests:
+ extends: Ubuntu_GCC_Static_Deps
cache:
- key: Debian_GCC_Static_Deps_tests
+ key: Ubuntu_GCC_Static_Deps_tests
variables:
CCACHE_SIZE: 1G
BUILD_TESTS_ONLY: 1
+ artifacts:
+ paths: []
+ expire_in: 1 minute
-Debian_Clang:
- extends: .Debian
+Ubuntu_Clang:
+ extends: .Ubuntu
before_script:
- CI/install_debian_deps.sh clang openmw-deps openmw-deps-dynamic
cache:
- key: Debian_Clang.v2
+ key: Ubuntu_Clang.v2
variables:
CC: clang
CXX: clang++
@@ -139,22 +171,28 @@ Debian_Clang:
# When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks.
timeout: 2h
-Debian_Clang_tests:
- extends: Debian_Clang
+Ubuntu_Clang_tests:
+ extends: Ubuntu_Clang
cache:
- key: Debian_Clang_tests.v2
+ key: Ubuntu_Clang_tests.v2
variables:
CCACHE_SIZE: 1G
BUILD_TESTS_ONLY: 1
+ artifacts:
+ paths: []
+ expire_in: 1 minute
-Debian_Clang_tests_Debug:
- extends: Debian_Clang
+Ubuntu_Clang_tests_Debug:
+ extends: Ubuntu_Clang
cache:
- key: Debian_Clang_tests_Debug.v1
+ key: Ubuntu_Clang_tests_Debug.v1
variables:
CCACHE_SIZE: 1G
BUILD_TESTS_ONLY: 1
CMAKE_BUILD_TYPE: Debug
+ artifacts:
+ paths: []
+ expire_in: 1 minute
.MacOS:
image: macos-11-xcode-12
@@ -163,8 +201,7 @@ Debian_Clang_tests_Debug:
stage: build
only:
variables:
- - $CI_PROJECT_ID == "7107382"
- - $CI_PIPELINE_SOURCE == "push"
+ - $CI_PROJECT_ID == "7107382" && $CI_PIPELINE_SOURCE == "push"
cache:
paths:
- ccache/
@@ -177,7 +214,7 @@ Debian_Clang_tests_Debug:
- ccache -z -M "${CCACHE_SIZE}"
- CI/before_script.osx.sh
- cd build; make -j $(sysctl -n hw.logicalcpu) package
- - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}.dmg"; done
+ - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME##*/}_${CI_JOB_ID}.dmg"; done
- ccache -s
artifacts:
paths:
@@ -192,21 +229,20 @@ macOS11_Xcode12:
variables:
CCACHE_SIZE: 3G
-macOS10.15_Xcode11:
+macOS12_Xcode13:
extends: .MacOS
- image: macos-10.15-xcode-11
- allow_failure: true
+ image: macos-12-xcode-13
cache:
- key: macOS10.15_Xcode11.v1
+ key: macOS12_Xcode13.v1
variables:
CCACHE_SIZE: 3G
variables: &engine-targets
- targets: "openmw,openmw-essimporter,openmw-iniimporter,openmw-launcher,openmw-wizard"
+ targets: "openmw,openmw-iniimporter,openmw-launcher,openmw-wizard,openmw-navmeshtool"
package: "Engine"
variables: &cs-targets
- targets: "openmw-cs,bsatool,esmtool,niftest"
+ targets: "openmw-cs,bsatool,esmtool,niftest,openmw-essimporter"
package: "CS"
variables: &tests-targets
@@ -330,6 +366,9 @@ Windows_Ninja_Tests_RelWithDebInfo:
config: "RelWithDebInfo"
# Gitlab can't successfully execute following binaries due to unknown reason
# executables: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe"
+ artifacts:
+ paths: []
+ expire_in: 1 minute
.Windows_MSBuild_Base:
tags:
@@ -396,21 +435,16 @@ Windows_Ninja_Tests_RelWithDebInfo:
- MSVC2019_64/*/*/*/*/*/*/*.log
- MSVC2019_64/*/*/*/*/*/*/*/*.log
-Daily_Windows_MSBuild_Engine_Release:on-schedule:
- extends:
- - .Windows_MSBuild_Base
- variables:
- <<: *engine-targets
- config: "Release"
- rules:
- - if: $CI_PIPELINE_SOURCE == "schedule"
-
Windows_MSBuild_Engine_Release:
extends:
- .Windows_MSBuild_Base
variables:
<<: *engine-targets
config: "Release"
+ rules:
+ # run this for both pushes and schedules so 'latest successful pipeline for branch' always includes it
+ - if: $CI_PIPELINE_SOURCE == "push"
+ - if: $CI_PIPELINE_SOURCE == "schedule"
Windows_MSBuild_Engine_Debug:
extends:
@@ -432,6 +466,10 @@ Windows_MSBuild_CS_Release:
variables:
<<: *cs-targets
config: "Release"
+ rules:
+ # run this for both pushes and schedules so 'latest successful pipeline for branch' always includes it
+ - if: $CI_PIPELINE_SOURCE == "push"
+ - if: $CI_PIPELINE_SOURCE == "schedule"
Windows_MSBuild_CS_Debug:
extends:
@@ -455,27 +493,28 @@ Windows_MSBuild_Tests_RelWithDebInfo:
config: "RelWithDebInfo"
# Gitlab can't successfully execute following binaries due to unknown reason
# executables: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe"
+ artifacts:
+ paths: []
+ expire_in: 1 minute
-Debian_AndroidNDK_arm64-v8a:
+Ubuntu_AndroidNDK_arm64-v8a:
tags:
- linux
- image: debian:bullseye
+ image: psi29a/android-ndk:focal-ndk22
rules:
- if: $CI_PIPELINE_SOURCE == "push"
variables:
CCACHE_SIZE: 3G
cache:
- key: Debian_AndroidNDK_arm64-v8a.v3
+ key: Ubuntu__Focal_AndroidNDK_r22b_arm64-v8a.v1
paths:
- apt-cache/
- ccache/
- build/extern/fetched/
before_script:
- export APT_CACHE_DIR=`pwd`/apt-cache && mkdir -pv $APT_CACHE_DIR
- - echo "deb http://deb.debian.org/debian unstable main contrib" > /etc/apt/sources.list
- - echo "google-android-ndk-installer google-android-installers/mirror select https://dl.google.com" | debconf-set-selections
- apt-get update -yq
- - apt-get -q -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake ccache curl unzip git build-essential google-android-ndk-installer
+ - apt-get -q -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake ccache curl unzip git build-essential
stage: build
script:
- export CCACHE_BASEDIR="`pwd`"
@@ -493,3 +532,18 @@ Debian_AndroidNDK_arm64-v8a:
# When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks.
timeout: 1h30m
+FindMissingMergeRequests:
+ image: python:latest
+ stage: build
+ rules:
+ - if: '$CI_PIPELINE_SOURCE == "schedule"'
+ variables:
+ PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
+ cache:
+ key: FindMissingMergeRequests.v1
+ paths:
+ - .cache/pip
+ before_script:
+ - pip3 install --user requests click discord_webhook
+ script:
+ - scripts/find_missing_merge_requests.py --project_id=$CI_PROJECT_ID --ignored_mrs_path=$CI_PROJECT_DIR/.resubmitted_merge_requests.txt
diff --git a/.resubmitted_merge_requests.txt b/.resubmitted_merge_requests.txt
new file mode 100644
index 0000000000..1585a60ec1
--- /dev/null
+++ b/.resubmitted_merge_requests.txt
@@ -0,0 +1,8 @@
+1471
+1450
+1420
+1314
+1216
+1172
+1160
+1051
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 1322dfca1b..0000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,80 +0,0 @@
-language: cpp
-branches:
- only:
- - master
- - /openmw-.*$/
-cache: ccache
-addons:
- apt:
- sources:
- - sourceline: 'ppa:openmw/openmw'
- packages: [
- # Dev
- build-essential, cmake, clang-tools-9, ccache,
- # Boost
- libboost-filesystem-dev, libboost-iostreams-dev, libboost-program-options-dev, libboost-system-dev,
- # FFmpeg
- libavcodec-dev, libavformat-dev, libavutil-dev, libswresample-dev, libswscale-dev,
- # Audio, Video and Misc. deps
- libsdl2-dev, libqt5opengl5-dev, libopenal-dev, libunshield-dev, libtinyxml-dev, liblz4-dev,
- # The other ones from OpenMW ppa
- libbullet-dev, libopenscenegraph-dev, libmygui-dev
- ]
-matrix:
- include:
- - name: OpenMW (all) on MacOS 10.15 with Xcode 11.6
- os: osx
- osx_image: xcode11.6
- - name: OpenMW (all) on Ubuntu Focal with GCC
- os: linux
- dist: focal
- - name: OpenMW (tests only) on Ubuntu Focal with GCC
- os: linux
- dist: focal
- env:
- - BUILD_TESTS_ONLY: 1
- - name: OpenMW (openmw) on Ubuntu Focal with Clang's Static Analysis
- os: linux
- dist: focal
- env:
- - MATRIX_EVAL="CC=clang-9 && CXX=clang++-9"
- - ANALYZE="scan-build-9 --force-analyze-debug-code --use-cc clang-9 --use-c++ clang++-9"
- compiler: clang
-
-before_install:
- - if [ "${TRAVIS_OS_NAME}" = "linux" ]; then eval "${MATRIX_EVAL}"; fi
- - ./CI/before_install.${TRAVIS_OS_NAME}.sh
-before_script:
- - ccache -z
- - ./CI/before_script.${TRAVIS_OS_NAME}.sh
-script:
- - cd ./build
- - ${ANALYZE} make -j3;
- - if [ "${TRAVIS_OS_NAME}" = "osx" ]; then make package; fi
- - if [ "${TRAVIS_OS_NAME}" = "osx" ]; then ../CI/check_package.osx.sh; fi
- - if [ "${TRAVIS_OS_NAME}" = "linux" ] && [ "${BUILD_TESTS_ONLY}" ]; then ./openmw_test_suite; fi
- - if [ "${TRAVIS_OS_NAME}" = "linux" ]; then cd .. && ./CI/check_tabs.sh; fi
- - cd "${TRAVIS_BUILD_DIR}"
- - ccache -s
-deploy:
- provider: script
- script: ./CI/deploy.osx.sh
- skip_cleanup: true
- on:
- branch: master
- condition: "$TRAVIS_EVENT_TYPE = cron && $TRAVIS_OS_NAME = osx"
- repo: OpenMW/openmw
-notifications:
- email:
- if: repository_slug = OpenMW/openmw AND branch = master
- recipients:
- - corrmage+travis-ci@gmail.com
- on_success: change
- on_failure: always
- irc:
- if: repository_slug = OpenMW/openmw AND branch = master
- channels:
- - "irc.libera.chat#openmw"
- on_success: change
- on_failure: always
- use_notice: true
diff --git a/AUTHORS.md b/AUTHORS.md
index 8ca9db90b9..2ddaa03cf6 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -31,6 +31,7 @@ Programmers
Allofich
Andreas Stöckel
Andrei Kortunov (akortunov)
+ Andrew Appuhamy (andrew-app)
AnyOldName3
Ardekantur
Armin Preiml
@@ -92,6 +93,7 @@ Programmers
Haoda Wang (h313)
hristoast
Internecine
+ Ivan Beloborodov (myrix)
Jackerty
Jacob Essex (Yacoby)
Jacob Turnbull (Tankinfrank)
@@ -113,6 +115,7 @@ Programmers
John Blomberg (fstp)
Jordan Ayers
Jordan Milne
+ Josquin Frei
Josua Grawitter
Jules Blok (Armada651)
julianko
@@ -215,6 +218,7 @@ Programmers
tlmullis
tri4ng1e
Thoronador
+ Tom Lowe (Vulpen)
Tom Mason (wheybags)
Torben Leif Carrington (TorbenC)
unelsson
@@ -230,7 +234,7 @@ Programmers
Yuri Krupenin
zelurker
Noah Gooder
- Andrew Appuhamy (andrew-app)
+
Documentation
-------------
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c491cb83c0..9c45d15e6d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,21 +2,31 @@
------
Bug #1751: Birthsign abilities increase modified attribute values instead of base ones
+ Bug #1930: Followers are still fighting if a target stops combat with a leader
+ Bug #2036: SetStat and ModStat instructions aren't implemented the same way as in Morrowind
Bug #3246: ESSImporter: Most NPCs are dead on save load
+ Bug #3488: AI combat aiming is too slow
Bug #3514: Editing a reference's position after loading an esp file makes the reference disappear
Bug #3737: Scripts from The Underground 2 .esp do not play (all patched versions)
Bug #3792: 1 frame late magicka recalc breaks early scripted magicka reactions to Intelligence change
Bug #3846: Strings starting with "-" fail to compile if not enclosed in quotes
+ Bug #3855: AI sometimes spams defensive spells
Bug #3905: Great House Dagoth issues
Bug #4203: Resurrecting an actor should close the loot GUI
+ Bug #4376: Moved actors don't respawn in their original cells
+ Bug #4389: NPC's lips do not move if his head model has the NiBSAnimationNode root node
Bug #4602: Robert's Bodies: crash inside createInstance()
Bug #4700: Editor: Incorrect command implementation
Bug #4744: Invisible particles must still be processed
+ Bug #4949: Incorrect particle lighting
Bug #5088: Sky abruptly changes direction during certain weather transitions
Bug #5100: Persuasion doesn't always clamp the resulting disposition
Bug #5120: Scripted object spawning updates physics system
Bug #5207: Loose summons can be present in scene
+ Bug #5377: console does not appear after using menutest in inventory
Bug #5379: Wandering NPCs falling through cantons
+ Bug #5394: Windows snapping no longer works
+ Bug #5434: Pinned windows shouldn't cover breath progress bar
Bug #5453: Magic effect VFX are offset for creatures
Bug #5483: AutoCalc flag is not used to calculate spells cost
Bug #5508: Engine binary links to Qt without using it
@@ -28,6 +38,7 @@
Bug #5842: GetDisposition adds temporary disposition change from different actors
Bug #5863: GetEffect should return true after the player has teleported
Bug #5913: Failed assertion during Ritual of Trees quest
+ Bug #5928: Glow in the Dahrk functionality used without mod installed
Bug #5937: Lights always need to be rotated by 90 degrees
Bug #6037: Morrowind Content Language Cannot be Set to English in OpenMW Launcher
Bug #6051: NaN water height in ESM file is not handled gracefully
@@ -47,7 +58,9 @@
Bug #6168: Weather particles flicker for a frame at start of storms
Bug #6172: Some creatures can't open doors
Bug #6174: Spellmaking and Enchanting sliders differences from vanilla
+ Bug #6177: Followers of player follower stop following after waiting for a day
Bug #6184: Command and Calm and Demoralize and Frenzy and Rally magic effects inconsistencies with vanilla
+ Bug #6191: Encumbrance messagebox timer works incorrectly
Bug #6197: Infinite Casting Loop
Bug #6253: Multiple instances of Reflect stack additively
Bug #6255: Reflect is different from vanilla
@@ -59,15 +72,36 @@
Bug #6289: Keyword search in dialogues expected the text to be all ASCII characters
Bug #6291: Can't pickup the dead mage's journal from the mysterious hunter mod
Bug #6302: Teleporting disabled actor breaks its disabled state
+ Bug #6303: After "go to jail" weapon can stuck in the ready to attack state
Bug #6307: Pathfinding in Infidelities quest from Tribunal addon is broken
Bug #6321: Arrow enchantments should always be applied to the target
Bug #6322: Total sold/cost should reset to 0 when there are no items offered
Bug #6323: Wyrmhaven: Alboin doesn't follower the player character out of his house
+ Bug #6324: Special Slave Companions: Can't buy the slave companions
Bug #6326: Detect Enchantment/Key should detect items in unresolved containers
+ Bug #6327: Blocking roots the character in place
+ Bug #6333: Werewolf stat changes should be implemented as damage/fortifications
+ Bug #6343: Magic projectile speed doesn't take race weight into account
Bug #6347: PlaceItem/PlaceItemCell/PlaceAt should work with levelled creatures
+ Bug #6354: SFX abruptly cut off after crossing max distance; implement soft fading of sound effects
+ Bug #6358: Changeweather command does not report an error when entering non-existent region
Bug #6363: Some scripts in Morrowland fail to work
Bug #6376: Creatures should be able to use torches
+ Bug #6386: Artifacts in water reflection due to imprecise screen-space coordinate computation
+ Bug #6396: Inputting certain Unicode characters triggers an assertion
+ Bug #6416: Morphs are applied to the wrong target
+ Bug #6417: OpenMW doesn't always use the right node to accumulate movement
+ Bug #6429: Wyrmhaven: Can't add AI packages to player
+ Bug #6433: Items bound to Quick Keys sometimes do not appear until the Quick Key menu is opened
+ Bug #6451: Weapon summoned from Cast When Used item will have the name "None"
+ Bug #6473: Strings from NIF should be parsed only to first null terminator
+ Bug #6493: Unlocking owned but not locked or unlocked containers is considered a crime
+ Bug #6517: Rotations for KeyframeData in NIFs should be optional
+ Bug #6519: Effects tooltips for ingredients work incorrectly
+ Bug #6523: Disintegrate Weapon is resisted by Resist Magicka instead of Sanctuary
+ Bug #6544: Far from world origin objects jitter when camera is still
Feature #890: OpenMW-CS: Column filtering
+ Feature #1465: "Reset" argument for AI functions
Feature #2554: Modifying an object triggers the instances table to scroll to the corresponding record
Feature #2780: A way to see current OpenMW version in the console
Feature #3616: Allow Zoom levels on the World Map
@@ -82,12 +116,18 @@
Feature #6017: Separate persistent and temporary cell references when saving
Feature #6032: Reverse-z depth buffer
Feature #6078: First person should not clear depth buffer
+ Feature #6128: Soft Particles
Feature #6161: Refactor Sky to use shaders and GLES/GL3 friendly
Feature #6162: Refactor GUI to use shaders and to be GLES and GL3+ friendly
+ Feature #6189: Navigation mesh disk cache
Feature #6199: Support FBO Rendering
+ Feature #6248: Embedded error marker mesh
Feature #6249: Alpha testing support for Collada
Feature #6251: OpenMW-CS: Set instance movement based on camera zoom
Feature #6288: Preserve the "blocked" record flag for referenceable objects.
+ Feature #6380: Commas are treated as whitespace in vanilla
+ Feature #6419: Topics shouldn't be greyed out if they can produce another topic reference
+ Feature #6534: Shader-based object texture blending
Task #6201: Remove the "Note: No relevant classes found. No output generated" warnings
Task #6264: Remove the old classes in animation.cpp
@@ -224,6 +264,8 @@
Bug #6043: Actor can have torch missing when torch animation is played
Bug #6047: Mouse bindings can be triggered during save loading
Bug #6136: Game freezes when NPCs try to open doors that are about to be closed
+ Bug #6142: Groundcover plugins change cells flags
+ Bug #6276: Deleted groundcover instances are not deleted in game
Bug #6294: Game crashes with empty pathgrid
Feature #390: 3rd person look "over the shoulder"
Feature #832: OpenMW-CS: Handle deleted references
diff --git a/CI/before_install.android.sh b/CI/before_install.android.sh
index 59d98f48c4..712ded2769 100755
--- a/CI/before_install.android.sh
+++ b/CI/before_install.android.sh
@@ -1,4 +1,4 @@
#!/bin/sh -ex
-curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201230.zip -o ~/openmw-android-deps.zip
-unzip -o ~/openmw-android-deps -d /usr/lib/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null
+curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20211114.zip -o ~/openmw-android-deps.zip
+unzip -o ~/openmw-android-deps -d /android-ndk-r22/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null
diff --git a/CI/before_script.android.sh b/CI/before_script.android.sh
index 3219f3a4ba..43422f68c1 100755
--- a/CI/before_script.android.sh
+++ b/CI/before_script.android.sh
@@ -7,9 +7,10 @@ mkdir -p build
cd build
cmake \
--DCMAKE_TOOLCHAIN_FILE=/usr/lib/android-sdk/ndk-bundle/build/cmake/android.toolchain.cmake \
+-DCMAKE_TOOLCHAIN_FILE=/android-ndk-r22/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-21 \
+-DANDROID_LD=deprecated \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_INSTALL_PREFIX=install \
@@ -21,8 +22,7 @@ cmake \
-DBUILD_ESSIMPORTER=0 \
-DBUILD_OPENCS=0 \
-DBUILD_WIZARD=0 \
+-DBUILD_NAVMESHTOOL=OFF \
-DOPENMW_USE_SYSTEM_MYGUI=OFF \
--DOPENMW_USE_SYSTEM_OSG=OFF \
--DOPENMW_USE_SYSTEM_BULLET=OFF \
-DOPENMW_USE_SYSTEM_SQLITE3=OFF \
..
diff --git a/CI/before_script.linux.sh b/CI/before_script.linux.sh
index b9fed204e3..8278ffcd0e 100755
--- a/CI/before_script.linux.sh
+++ b/CI/before_script.linux.sh
@@ -46,7 +46,7 @@ fi
if [[ $CI_CLANG_TIDY ]]; then
CMAKE_CONF_OPTS+=(
- -DCMAKE_CXX_CLANG_TIDY='clang-tidy;-checks=-*,boost-*,clang-analyzer-*,concurrency-*,performance-*,-header-filter=.*,bugprone-*,misc-definitions-in-headers,misc-misplaced-const,misc-redundant-expression'
+ -DCMAKE_CXX_CLANG_TIDY='clang-tidy;-checks=-*,boost-*,clang-analyzer-*,concurrency-*,performance-*,-header-filter=.*,bugprone-*,misc-definitions-in-headers,misc-misplaced-const,misc-redundant-expression,-bugprone-narrowing-conversions'
)
fi
@@ -71,6 +71,7 @@ if [[ "${BUILD_TESTS_ONLY}" ]]; then
-DBUILD_ESSIMPORTER=OFF \
-DBUILD_OPENCS=OFF \
-DBUILD_WIZARD=OFF \
+ -DBUILD_NAVMESHTOOL=OFF \
-DBUILD_UNITTESTS=${BUILD_UNITTESTS} \
-DBUILD_BENCHMARKS=${BUILD_BENCHMARKS} \
-DGTEST_ROOT="${GOOGLETEST_DIR}" \
diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh
index 1341289335..32169fc04b 100644
--- a/CI/before_script.msvc.sh
+++ b/CI/before_script.msvc.sh
@@ -566,9 +566,9 @@ if [ -z $SKIP_DOWNLOAD ]; then
fi
# SDL2
- download "SDL 2.0.12" \
- "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/SDL2-2.0.12.zip" \
- "SDL2-2.0.12.zip"
+ download "SDL 2.0.18" \
+ "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/SDL2-2.0.18.zip" \
+ "SDL2-2.0.18.zip"
# LZ4
download "LZ4 1.9.2" \
@@ -898,17 +898,17 @@ fi
cd $DEPS
echo
# SDL2
-printf "SDL 2.0.12... "
+printf "SDL 2.0.18... "
{
- if [ -d SDL2-2.0.12 ]; then
+ if [ -d SDL2-2.0.18 ]; then
printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then
- rm -rf SDL2-2.0.12
- eval 7z x -y SDL2-2.0.12.zip $STRIP
+ rm -rf SDL2-2.0.18
+ eval 7z x -y SDL2-2.0.18.zip $STRIP
fi
- export SDL2DIR="$(real_pwd)/SDL2-2.0.12"
+ export SDL2DIR="$(real_pwd)/SDL2-2.0.18"
for config in ${CONFIGURATIONS[@]}; do
- add_runtime_dlls $config "$(pwd)/SDL2-2.0.12/lib/x${ARCHSUFFIX}/SDL2.dll"
+ add_runtime_dlls $config "$(pwd)/SDL2-2.0.18/lib/x${ARCHSUFFIX}/SDL2.dll"
done
echo Done.
}
diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh
index 6d0fe8c99e..8b2c9c6f01 100755
--- a/CI/before_script.osx.sh
+++ b/CI/before_script.osx.sh
@@ -16,7 +16,7 @@ cmake \
-D CMAKE_CXX_FLAGS="-stdlib=libc++" \
-D CMAKE_C_FLAGS_RELEASE="-g -O0" \
-D CMAKE_CXX_FLAGS_RELEASE="-g -O0" \
--D CMAKE_OSX_DEPLOYMENT_TARGET="10.14" \
+-D CMAKE_OSX_DEPLOYMENT_TARGET="10.15" \
-D CMAKE_BUILD_TYPE=RELEASE \
-D OPENMW_OSX_DEPLOYMENT=TRUE \
-D OPENMW_USE_SYSTEM_SQLITE3=OFF \
diff --git a/CI/install_debian_deps.sh b/CI/install_debian_deps.sh
index a8843207d9..131ccae305 100755
--- a/CI/install_debian_deps.sh
+++ b/CI/install_debian_deps.sh
@@ -22,9 +22,8 @@ declare -rA GROUPED_DEPS=(
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev
libsdl2-dev libqt5opengl5-dev libopenal-dev libunshield-dev libtinyxml-dev
libbullet-dev liblz4-dev libpng-dev libjpeg-dev libluajit-5.1-dev
- ca-certificates
+ librecast-dev libsqlite3-dev ca-certificates
"
- # TODO: add librecastnavigation-dev when debian is ready
# These dependencies can alternatively be built and linked statically.
[openmw-deps-dynamic]="libmygui-dev libopenscenegraph-dev libsqlite3-dev"
@@ -64,5 +63,7 @@ done
export APT_CACHE_DIR="${PWD}/apt-cache"
set -x
mkdir -pv "$APT_CACHE_DIR"
-apt-get update -yq
-apt-get -q -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends "${deps[@]}"
+apt-get update -yqq
+apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends software-properties-common >/dev/null
+add-apt-repository -y ppa:openmw/openmw
+apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends "${deps[@]}" >/dev/null
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f2ee87b2ce..67a8f7c679 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -37,6 +37,7 @@ option(BUILD_DOCS "Build documentation." OFF )
option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF)
option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest" OFF)
option(BUILD_BENCHMARKS "Build benchmarks with Google Benchmark" OFF)
+option(BUILD_NAVMESHTOOL "Build navmesh tool" ON)
set(OpenGL_GL_PREFERENCE LEGACY) # Use LEGACY as we use GL2; GLNVD is for GL3 and up.
@@ -398,7 +399,7 @@ set(Boost_NO_BOOST_CMAKE ON)
find_package(Boost 1.6.2 REQUIRED COMPONENTS ${BOOST_COMPONENTS} OPTIONAL_COMPONENTS ${BOOST_OPTIONAL_COMPONENTS})
if(OPENMW_USE_SYSTEM_MYGUI)
- find_package(MyGUI 3.2.2 REQUIRED)
+ find_package(MyGUI 3.4.1 REQUIRED)
endif()
find_package(SDL2 2.0.9 REQUIRED)
find_package(OpenAL REQUIRED)
@@ -414,7 +415,8 @@ else(USE_LUAJIT)
endif(USE_LUAJIT)
# C++ library binding to Lua
-set(SOL_INCLUDE_DIRS ${OpenMW_SOURCE_DIR}/extern/sol3.2.2 ${OpenMW_SOURCE_DIR}/extern/sol_config)
+set(SOL_INCLUDE_DIR ${OpenMW_SOURCE_DIR}/extern/sol3.2.2)
+set(SOL_CONFIG_DIR ${OpenMW_SOURCE_DIR}/extern/sol_config)
include_directories(
BEFORE SYSTEM
@@ -426,7 +428,8 @@ include_directories(
${OPENGL_INCLUDE_DIR}
${BULLET_INCLUDE_DIRS}
${LUA_INCLUDE_DIR}
- ${SOL_INCLUDE_DIRS}
+ ${SOL_INCLUDE_DIR}
+ ${SOL_CONFIG_DIR}
)
link_directories(${SDL2_LIBRARY_DIRS} ${Boost_LIBRARY_DIRS})
@@ -443,9 +446,8 @@ if (APPLE)
"${APP_BUNDLE_DIR}/Contents/Resources/OpenMW.icns" COPYONLY)
endif (APPLE)
-if (NOT APPLE)
- set(OPENMW_MYGUI_FILES_ROOT ${OpenMW_BINARY_DIR})
- set(OPENMW_SHADERS_ROOT ${OpenMW_BINARY_DIR})
+if (NOT APPLE) # this is modified for macOS use later in "apps/open[mw|cs]/CMakeLists.txt"
+ set(OPENMW_RESOURCES_ROOT ${OpenMW_BINARY_DIR})
endif ()
add_subdirectory(files/)
@@ -602,6 +604,10 @@ if (BUILD_BENCHMARKS)
add_subdirectory(apps/benchmarks)
endif()
+if (BUILD_NAVMESHTOOL)
+ add_subdirectory(apps/navmeshtool)
+endif()
+
if (WIN32)
if (MSVC)
if (OPENMW_MP_BUILD)
@@ -701,6 +707,10 @@ if (WIN32)
if (BUILD_BENCHMARKS)
set_target_properties(openmw_detournavigator_navmeshtilescache_benchmark PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}")
endif()
+
+ if (BUILD_NAVMESHTOOL)
+ set_target_properties(openmw-navmeshtool PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}")
+ endif()
endif(MSVC)
# TODO: At some point release builds should not use the console but rather write to a log file
@@ -709,8 +719,10 @@ if (WIN32)
endif()
if (BUILD_OPENMW AND APPLE)
- # Without these flags LuaJit crashes on startup on OSX
- set_target_properties(openmw PROPERTIES LINK_FLAGS "-pagezero_size 10000 -image_base 100000000")
+ if (USE_LUAJIT)
+ # Without these flags LuaJit crashes on startup on OSX
+ set_target_properties(openmw PROPERTIES LINK_FLAGS "-pagezero_size 10000 -image_base 100000000")
+ endif(USE_LUAJIT)
target_compile_definitions(components PRIVATE GL_SILENCE_DEPRECATION=1)
target_compile_definitions(openmw PRIVATE GL_SILENCE_DEPRECATION=1)
endif()
@@ -941,6 +953,9 @@ elseif(NOT APPLE)
IF(BUILD_WIZARD)
INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-wizard" DESTINATION "${BINDIR}" )
ENDIF(BUILD_WIZARD)
+ if(BUILD_NAVMESHTOOL)
+ install(PROGRAMS "${INSTALL_SOURCE}/openmw-navmeshtool" DESTINATION "${BINDIR}" )
+ endif()
# Install licenses
INSTALL(FILES "files/mygui/DejaVuFontLicense.txt" DESTINATION "${LICDIR}" )
diff --git a/README.md b/README.md
index 638801b129..557cd614cd 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,6 @@
OpenMW
======
-[![Coverity Scan Build Status](https://scan.coverity.com/projects/3740/badge.svg)](https://scan.coverity.com/projects/3740) [![pipeline status](https://gitlab.com/OpenMW/openmw/badges/master/pipeline.svg)](https://gitlab.com/OpenMW/openmw/commits/master)
-
OpenMW is an open-source game engine that supports playing Morrowind by Bethesda Softworks. You need to own the game for OpenMW to play Morrowind.
OpenMW also comes with OpenMW-CS, a replacement for Bethesda's Construction Set.
diff --git a/apps/benchmarks/detournavigator/navmeshtilescache.cpp b/apps/benchmarks/detournavigator/navmeshtilescache.cpp
index 6c530db41c..c27fdc4289 100644
--- a/apps/benchmarks/detournavigator/navmeshtilescache.cpp
+++ b/apps/benchmarks/detournavigator/navmeshtilescache.cpp
@@ -25,18 +25,10 @@ namespace
};
template <typename Random>
- TilePosition generateTilePosition(int max, Random& random)
+ osg::Vec2i generateVec2i(int max, Random& random)
{
std::uniform_int_distribution<int> distribution(0, max);
- return TilePosition(distribution(random), distribution(random));
- }
-
- template <typename Random>
- TileBounds generateTileBounds(Random& random)
- {
- std::uniform_real_distribution<float> distribution(0.0, 1.0);
- const osg::Vec2f min(distribution(random), distribution(random));
- return TileBounds {min, min + osg::Vec2f(1.0, 1.0)};
+ return osg::Vec2i(distribution(random), distribution(random));
}
template <typename Random>
@@ -91,8 +83,7 @@ namespace
{
std::uniform_real_distribution<float> distribution(0.0, 1.0);
std::generate_n(out, count, [&] {
- const osg::Vec3f shift(distribution(random), distribution(random), distribution(random));
- return Cell {1, shift};
+ return CellWater {generateVec2i(1000, random), Water {ESM::Land::REAL_SIZE, distribution(random)}};
});
}
@@ -117,16 +108,18 @@ namespace
{
std::uniform_real_distribution<float> distribution(0.0, 1.0);
Heightfield result;
- result.mBounds = generateTileBounds(random);
+ result.mCellPosition = generateVec2i(1000, random);
+ result.mCellSize = ESM::Land::REAL_SIZE;
result.mMinHeight = distribution(random);
result.mMaxHeight = result.mMinHeight + 1.0;
- result.mShift = osg::Vec3f(distribution(random), distribution(random), distribution(random));
- result.mScale = distribution(random);
result.mLength = static_cast<std::uint8_t>(ESM::Land::LAND_SIZE);
std::generate_n(std::back_inserter(result.mHeights), ESM::Land::LAND_NUM_VERTS, [&]
{
return distribution(random);
});
+ result.mOriginalSize = ESM::Land::LAND_SIZE;
+ result.mMinX = 0;
+ result.mMinY = 0;
return result;
}
@@ -135,7 +128,8 @@ namespace
{
std::uniform_real_distribution<float> distribution(0.0, 1.0);
FlatHeightfield result;
- result.mBounds = generateTileBounds(random);
+ result.mCellPosition = generateVec2i(1000, random);
+ result.mCellSize = ESM::Land::REAL_SIZE;
result.mHeight = distribution(random);
return result;
}
@@ -144,14 +138,14 @@ namespace
Key generateKey(std::size_t triangles, Random& random)
{
const osg::Vec3f agentHalfExtents = generateAgentHalfExtents(0.5, 1.5, random);
- const TilePosition tilePosition = generateTilePosition(10000, random);
+ const TilePosition tilePosition = generateVec2i(10000, random);
const std::size_t generation = std::uniform_int_distribution<std::size_t>(0, 100)(random);
const std::size_t revision = std::uniform_int_distribution<std::size_t>(0, 10000)(random);
Mesh mesh = generateMesh(triangles, random);
- std::vector<Cell> water;
+ std::vector<CellWater> water;
generateWater(std::back_inserter(water), 1, random);
RecastMesh recastMesh(generation, revision, std::move(mesh), std::move(water),
- {generateHeightfield(random)}, {generateFlatHeightfield(random)});
+ {generateHeightfield(random)}, {generateFlatHeightfield(random)}, {});
return Key {agentHalfExtents, tilePosition, std::move(recastMesh)};
}
diff --git a/apps/esmtool/record.cpp b/apps/esmtool/record.cpp
index 1de3288ad4..171f64eabe 100644
--- a/apps/esmtool/record.cpp
+++ b/apps/esmtool/record.cpp
@@ -30,7 +30,7 @@ void printAIPackage(const ESM::AIPackage& p)
{
std::cout << " Travel Coordinates: (" << p.mTravel.mX << ","
<< p.mTravel.mY << "," << p.mTravel.mZ << ")" << std::endl;
- std::cout << " Travel Unknown: " << p.mTravel.mUnk << std::endl;
+ std::cout << " Should repeat: " << p.mTravel.mShouldRepeat << std::endl;
}
else if (p.mType == ESM::AI_Follow || p.mType == ESM::AI_Escort)
{
@@ -38,12 +38,12 @@ void printAIPackage(const ESM::AIPackage& p)
<< p.mTarget.mY << "," << p.mTarget.mZ << ")" << std::endl;
std::cout << " Duration: " << p.mTarget.mDuration << std::endl;
std::cout << " Target ID: " << p.mTarget.mId.toString() << std::endl;
- std::cout << " Unknown: " << p.mTarget.mUnk << std::endl;
+ std::cout << " Should repeat: " << p.mTarget.mShouldRepeat << std::endl;
}
else if (p.mType == ESM::AI_Activate)
{
std::cout << " Name: " << p.mActivate.mName.toString() << std::endl;
- std::cout << " Activate Unknown: " << p.mActivate.mUnk << std::endl;
+ std::cout << " Should repeat: " << p.mActivate.mShouldRepeat << std::endl;
}
else {
std::cout << " BadPackage: " << Misc::StringUtils::format("0x%08X", p.mType) << std::endl;
diff --git a/apps/essimporter/converter.cpp b/apps/essimporter/converter.cpp
index 874b936baf..6e79e27f18 100644
--- a/apps/essimporter/converter.cpp
+++ b/apps/essimporter/converter.cpp
@@ -2,7 +2,6 @@
#include <stdexcept>
#include <algorithm>
-#include <climits> // INT_MIN
#include <osgDB/WriteFile>
@@ -371,7 +370,7 @@ namespace ESSImport
if (cellref.mHasACDT)
convertACDT(cellref.mACDT, objstate.mCreatureStats);
else
- objstate.mCreatureStats.mGoldPool = INT_MIN; // HACK: indicates no ACDT
+ objstate.mCreatureStats.mMissingACDT = true;
if (cellref.mHasACSC)
convertACSC(cellref.mACSC, objstate.mCreatureStats);
convertNpcData(cellref, objstate.mNpcStats);
@@ -414,7 +413,7 @@ namespace ESSImport
if (cellref.mHasACDT)
convertACDT(cellref.mACDT, objstate.mCreatureStats);
else
- objstate.mCreatureStats.mGoldPool = INT_MIN; // HACK: indicates no ACDT
+ objstate.mCreatureStats.mMissingACDT = true;
if (cellref.mHasACSC)
convertACSC(cellref.mACSC, objstate.mCreatureStats);
convertCREC(crecIt->second, objstate);
diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp
index 2c33992ac3..fc1d84a61f 100644
--- a/apps/launcher/advancedpage.cpp
+++ b/apps/launcher/advancedpage.cpp
@@ -1,6 +1,7 @@
#include "advancedpage.hpp"
#include <array>
+#include <string>
#include <components/config/gamesettings.hpp>
#include <QFileDialog>
@@ -20,13 +21,13 @@ Launcher::AdvancedPage::AdvancedPage(Config::GameSettings &gameSettings, QWidget
setObjectName ("AdvancedPage");
setupUi(this);
- for(const char * name : Launcher::enumerateOpenALDevices())
+ for(const std::string& name : Launcher::enumerateOpenALDevices())
{
- audioDeviceSelectorComboBox->addItem(QString::fromUtf8(name), QString::fromUtf8(name));
+ audioDeviceSelectorComboBox->addItem(QString::fromStdString(name), QString::fromStdString(name));
}
- for(const char * name : Launcher::enumerateOpenALDevicesHrtf())
+ for(const std::string& name : Launcher::enumerateOpenALDevicesHrtf())
{
- hrtfProfileSelectorComboBox->addItem(QString::fromUtf8(name), QString::fromUtf8(name));
+ hrtfProfileSelectorComboBox->addItem(QString::fromStdString(name), QString::fromStdString(name));
}
loadSettings();
@@ -117,6 +118,11 @@ bool Launcher::AdvancedPage::loadSettings()
loadSettingBool(autoUseTerrainSpecularMapsCheckBox, "auto use terrain specular maps", "Shaders");
loadSettingBool(bumpMapLocalLightingCheckBox, "apply lighting to environment maps", "Shaders");
loadSettingBool(radialFogCheckBox, "radial fog", "Shaders");
+ loadSettingBool(softParticlesCheckBox, "soft particles", "Shaders");
+ loadSettingBool(antialiasAlphaTestCheckBox, "antialias alpha test", "Shaders");
+ if (Settings::Manager::getInt("antialiasing", "Video") == 0) {
+ antialiasAlphaTestCheckBox->setCheckState(Qt::Unchecked);
+ }
loadSettingBool(magicItemAnimationsCheckBox, "use magic item animations", "Game");
connect(animSourcesCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotAnimSourcesToggled(bool)));
loadSettingBool(animSourcesCheckBox, "use additional anim sources", "Game");
@@ -137,6 +143,8 @@ bool Launcher::AdvancedPage::loadSettings()
loadSettingBool(activeGridObjectPagingCheckBox, "object paging active grid", "Terrain");
viewingDistanceComboBox->setValue(convertToCells(Settings::Manager::getInt("viewing distance", "Camera")));
objectPagingMinSizeComboBox->setValue(Settings::Manager::getDouble("object paging min size", "Terrain"));
+
+ loadSettingBool(nightDaySwitchesCheckBox, "day night switches", "Game");
}
// Audio
@@ -207,7 +215,7 @@ bool Launcher::AdvancedPage::loadSettings()
{
// Saves
loadSettingBool(timePlayedCheckbox, "timeplayed", "Saves");
- maximumQuicksavesComboBox->setValue(Settings::Manager::getInt("max quicksaves", "Saves"));
+ loadSettingInt(maximumQuicksavesComboBox,"max quicksaves", "Saves");
// Other Settings
QString screenshotFormatString = QString::fromStdString(Settings::Manager::getString("screenshot format", "General")).toUpper();
@@ -252,14 +260,10 @@ void Launcher::AdvancedPage::saveSettings()
saveSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game");
saveSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game");
saveSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game");
- int unarmedFactorsStrengthIndex = unarmedFactorsStrengthComboBox->currentIndex();
- if (unarmedFactorsStrengthIndex != Settings::Manager::getInt("strength influences hand to hand", "Game"))
- Settings::Manager::setInt("strength influences hand to hand", "Game", unarmedFactorsStrengthIndex);
+ saveSettingInt(unarmedFactorsStrengthComboBox, "strength influences hand to hand", "Game");
saveSettingBool(stealingFromKnockedOutCheckBox, "always allow stealing from knocked out actors", "Game");
saveSettingBool(enableNavigatorCheckBox, "enable", "Navigator");
- int numPhysicsThreads = physicsThreadsSpinBox->value();
- if (numPhysicsThreads != Settings::Manager::getInt("async num threads", "Physics"))
- Settings::Manager::setInt("async num threads", "Physics", numPhysicsThreads);
+ saveSettingInt(physicsThreadsSpinBox, "async num threads", "Physics");
}
// Visuals
@@ -270,6 +274,8 @@ void Launcher::AdvancedPage::saveSettings()
saveSettingBool(autoUseTerrainSpecularMapsCheckBox, "auto use terrain specular maps", "Shaders");
saveSettingBool(bumpMapLocalLightingCheckBox, "apply lighting to environment maps", "Shaders");
saveSettingBool(radialFogCheckBox, "radial fog", "Shaders");
+ saveSettingBool(softParticlesCheckBox, "soft particles", "Shaders");
+ saveSettingBool(antialiasAlphaTestCheckBox, "antialias alpha test", "Shaders");
saveSettingBool(magicItemAnimationsCheckBox, "use magic item animations", "Game");
saveSettingBool(animSourcesCheckBox, "use additional anim sources", "Game");
saveSettingBool(weaponSheathingCheckBox, "weapon sheathing", "Game");
@@ -294,6 +300,8 @@ void Launcher::AdvancedPage::saveSettings()
double objectPagingMinSize = objectPagingMinSizeComboBox->value();
if (objectPagingMinSize != Settings::Manager::getDouble("object paging min size", "Terrain"))
Settings::Manager::setDouble("object paging min size", "Terrain", objectPagingMinSize);
+
+ saveSettingBool(nightDaySwitchesCheckBox, "day night switches", "Game");
}
// Audio
@@ -349,9 +357,7 @@ void Launcher::AdvancedPage::saveSettings()
saveSettingBool(showMeleeInfoCheckBox, "show melee info", "Game");
saveSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game");
saveSettingBool(changeDialogTopicsCheckBox, "color topic enable", "GUI");
- int showOwnedCurrentIndex = showOwnedComboBox->currentIndex();
- if (showOwnedCurrentIndex != Settings::Manager::getInt("show owned", "Game"))
- Settings::Manager::setInt("show owned", "Game", showOwnedCurrentIndex);
+ saveSettingInt(showOwnedComboBox,"show owned", "Game");
saveSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI");
saveSettingBool(useZoomOnMapCheckBox, "allow zooming", "Map");
saveSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game");
@@ -370,11 +376,7 @@ void Launcher::AdvancedPage::saveSettings()
{
// Saves Settings
saveSettingBool(timePlayedCheckbox, "timeplayed", "Saves");
- int maximumQuicksaves = maximumQuicksavesComboBox->value();
- if (maximumQuicksaves != Settings::Manager::getInt("max quicksaves", "Saves"))
- {
- Settings::Manager::setInt("max quicksaves", "Saves", maximumQuicksaves);
- }
+ saveSettingInt(maximumQuicksavesComboBox, "max quicksaves", "Saves");
// Other Settings
std::string screenshotFormatString = screenshotFormatComboBox->currentText().toLower().toStdString();
@@ -416,6 +418,32 @@ void Launcher::AdvancedPage::saveSettingBool(QCheckBox *checkbox, const std::str
Settings::Manager::setBool(setting, group, cValue);
}
+void Launcher::AdvancedPage::loadSettingInt(QComboBox *comboBox, const std::string &setting, const std::string &group)
+{
+ int currentIndex = Settings::Manager::getInt(setting, group);
+ comboBox->setCurrentIndex(currentIndex);
+}
+
+void Launcher::AdvancedPage::saveSettingInt(QComboBox *comboBox, const std::string &setting, const std::string &group)
+{
+ int currentIndex = comboBox->currentIndex();
+ if (currentIndex != Settings::Manager::getInt(setting, group))
+ Settings::Manager::setInt(setting, group, currentIndex);
+}
+
+void Launcher::AdvancedPage::loadSettingInt(QSpinBox *spinBox, const std::string &setting, const std::string &group)
+{
+ int value = Settings::Manager::getInt(setting, group);
+ spinBox->setValue(value);
+}
+
+void Launcher::AdvancedPage::saveSettingInt(QSpinBox *spinBox, const std::string &setting, const std::string &group)
+{
+ int value = spinBox->value();
+ if (value != Settings::Manager::getInt(setting, group))
+ Settings::Manager::setInt(setting, group, value);
+}
+
void Launcher::AdvancedPage::slotLoadedCellsChanged(QStringList cellNames)
{
loadCellsForAutocomplete(cellNames);
diff --git a/apps/launcher/advancedpage.hpp b/apps/launcher/advancedpage.hpp
index 9685dcefeb..1d16fae706 100644
--- a/apps/launcher/advancedpage.hpp
+++ b/apps/launcher/advancedpage.hpp
@@ -41,8 +41,12 @@ namespace Launcher
* @param filePaths the file paths of the content files to be examined
*/
void loadCellsForAutocomplete(QStringList filePaths);
- void loadSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group);
- void saveSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group);
+ static void loadSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group);
+ static void saveSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group);
+ static void loadSettingInt(QComboBox *comboBox, const std::string& setting, const std::string& group);
+ static void saveSettingInt(QComboBox *comboBox, const std::string& setting, const std::string& group);
+ static void loadSettingInt(QSpinBox *spinBox, const std::string& setting, const std::string& group);
+ static void saveSettingInt(QSpinBox *spinBox, const std::string& setting, const std::string& group);
};
}
#endif
diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp
index 4c66668e4a..f5efa8bbcc 100644
--- a/apps/launcher/datafilespage.cpp
+++ b/apps/launcher/datafilespage.cpp
@@ -1,4 +1,5 @@
#include "datafilespage.hpp"
+#include "maindialog.hpp"
#include <QDebug>
@@ -8,6 +9,7 @@
#include <QSortFilterProxyModel>
#include <thread>
#include <mutex>
+#include <algorithm>
#include <apps/launcher/utils/cellnameloader.hpp>
#include <components/files/configurationmanager.hpp>
@@ -24,11 +26,14 @@
const char *Launcher::DataFilesPage::mDefaultContentListName = "Default";
-Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, Config::LauncherSettings &launcherSettings, QWidget *parent)
+Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings,
+ Config::LauncherSettings &launcherSettings, MainDialog *parent)
: QWidget(parent)
+ , mMainDialog(parent)
, mCfgMgr(cfg)
, mGameSettings(gameSettings)
, mLauncherSettings(launcherSettings)
+ , mNavMeshToolInvoker(new Process::ProcessInvoker(this))
{
ui.setupUi (this);
setObjectName ("DataFilesPage");
@@ -57,8 +62,6 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config:
void Launcher::DataFilesPage::buildView()
{
- ui.verticalLayout->insertWidget (0, mSelector->uiWidget());
-
QToolButton * refreshButton = mSelector->refreshButton();
//tool buttons
@@ -89,6 +92,13 @@ void Launcher::DataFilesPage::buildView()
this, SLOT (slotProfileChangedByUser(QString, QString)));
connect(ui.refreshDataFilesAction, SIGNAL(triggered()),this, SLOT(slotRefreshButtonClicked()));
+
+ connect(ui.updateNavMeshButton, SIGNAL(clicked()), this, SLOT(startNavMeshTool()));
+ connect(ui.cancelNavMeshButton, SIGNAL(clicked()), this, SLOT(killNavMeshTool()));
+
+ connect(mNavMeshToolInvoker->getProcess(), SIGNAL(readyReadStandardOutput()), this, SLOT(updateNavMeshProgress()));
+ connect(mNavMeshToolInvoker->getProcess(), SIGNAL(readyReadStandardError()), this, SLOT(updateNavMeshProgress()));
+ connect(mNavMeshToolInvoker->getProcess(), SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(navMeshToolFinished(int, QProcess::ExitStatus)));
}
bool Launcher::DataFilesPage::loadSettings()
@@ -121,6 +131,7 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName)
for (const QString &path : paths)
mSelector->addFiles(path);
+ mSelector->sortFiles();
PathIterator pathIterator(paths);
@@ -410,3 +421,62 @@ void Launcher::DataFilesPage::reloadCells(QStringList selectedFiles)
std::sort(cellNamesList.begin(), cellNamesList.end());
emit signalLoadedCellsChanged(cellNamesList);
}
+
+void Launcher::DataFilesPage::startNavMeshTool()
+{
+ mMainDialog->writeSettings();
+
+ ui.navMeshLogPlainTextEdit->clear();
+ ui.navMeshProgressBar->setValue(0);
+ ui.navMeshProgressBar->setMaximum(1);
+
+ if (!mNavMeshToolInvoker->startProcess(QLatin1String("openmw-navmeshtool")))
+ return;
+
+ ui.cancelNavMeshButton->setEnabled(true);
+ ui.navMeshProgressBar->setEnabled(true);
+}
+
+void Launcher::DataFilesPage::killNavMeshTool()
+{
+ mNavMeshToolInvoker->killProcess();
+}
+
+void Launcher::DataFilesPage::updateNavMeshProgress()
+{
+ QProcess& process = *mNavMeshToolInvoker->getProcess();
+ QString text;
+ while (process.canReadLine())
+ {
+ const QByteArray line = process.readLine();
+ const auto end = std::find_if(line.rbegin(), line.rend(), [] (auto v) { return v != '\n' && v != '\r'; });
+ text = QString::fromUtf8(line.mid(0, line.size() - (end - line.rbegin())));
+ ui.navMeshLogPlainTextEdit->appendPlainText(text);
+ }
+ const QRegularExpression pattern(R"([\( ](\d+)/(\d+)[\) ])");
+ QRegularExpressionMatch match = pattern.match(text);
+ if (!match.hasMatch())
+ return;
+ int value = match.captured(1).toInt();
+ const int maximum = match.captured(2).toInt();
+ if (text.contains("cell"))
+ ui.navMeshProgressBar->setMaximum(maximum * 100);
+ else if (maximum > ui.navMeshProgressBar->maximum())
+ ui.navMeshProgressBar->setMaximum(maximum);
+ else
+ value += static_cast<int>(std::round(
+ (ui.navMeshProgressBar->maximum() - maximum)
+ * (static_cast<float>(value) / static_cast<float>(maximum))
+ ));
+ ui.navMeshProgressBar->setValue(value);
+}
+
+void Launcher::DataFilesPage::navMeshToolFinished(int exitCode, QProcess::ExitStatus exitStatus)
+{
+ updateNavMeshProgress();
+ ui.navMeshLogPlainTextEdit->appendPlainText(QString::fromUtf8(mNavMeshToolInvoker->getProcess()->readAll()));
+ if (exitCode == 0 && exitStatus == QProcess::ExitStatus::NormalExit)
+ ui.navMeshProgressBar->setValue(ui.navMeshProgressBar->maximum());
+ ui.cancelNavMeshButton->setEnabled(false);
+ ui.navMeshProgressBar->setEnabled(false);
+}
diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp
index 5a7a6dc6e6..a039237590 100644
--- a/apps/launcher/datafilespage.hpp
+++ b/apps/launcher/datafilespage.hpp
@@ -2,6 +2,9 @@
#define DATAFILESPAGE_H
#include "ui_datafilespage.h"
+
+#include <components/process/processinvoker.hpp>
+
#include <QWidget>
@@ -19,6 +22,7 @@ namespace Config { class GameSettings;
namespace Launcher
{
+ class MainDialog;
class TextInputDialog;
class ProfilesComboBox;
@@ -31,7 +35,7 @@ namespace Launcher
public:
explicit DataFilesPage (Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings,
- Config::LauncherSettings &launcherSettings, QWidget *parent = nullptr);
+ Config::LauncherSettings &launcherSettings, MainDialog *parent = nullptr);
QAbstractItemModel* profilesModel() const;
@@ -69,12 +73,18 @@ namespace Launcher
void on_cloneProfileAction_triggered();
void on_deleteProfileAction_triggered();
+ void startNavMeshTool();
+ void killNavMeshTool();
+ void updateNavMeshProgress();
+ void navMeshToolFinished(int exitCode, QProcess::ExitStatus exitStatus);
+
public:
/// Content List that is always present
const static char *mDefaultContentListName;
private:
+ MainDialog *mMainDialog;
TextInputDialog *mNewProfileDialog;
TextInputDialog *mCloneProfileDialog;
@@ -87,6 +97,8 @@ namespace Launcher
QStringList previousSelectedFiles;
QString mDataLocal;
+ Process::ProcessInvoker* mNavMeshToolInvoker;
+
void buildView();
void setProfile (int index, bool savePrevious);
void setProfile (const QString &previous, const QString &current, bool savePrevious);
diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp
index 8359834ddb..e6d857a943 100644
--- a/apps/launcher/graphicspage.cpp
+++ b/apps/launcher/graphicspage.cpp
@@ -47,7 +47,6 @@ Launcher::GraphicsPage::GraphicsPage(QWidget *parent)
connect(screenComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(screenChanged(int)));
connect(framerateLimitCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotFramerateLimitToggled(bool)));
connect(shadowDistanceCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotShadowDistLimitToggled(bool)));
-
}
bool Launcher::GraphicsPage::setupSDL()
diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp
index 75d4464b68..d335d7cded 100644
--- a/apps/launcher/maindialog.cpp
+++ b/apps/launcher/maindialog.cpp
@@ -146,7 +146,6 @@ void Launcher::MainDialog::createPages()
connect(mDataFilesPage, SIGNAL(signalProfileChanged(int)), mPlayPage, SLOT(setProfilesIndex(int)));
// Using Qt::QueuedConnection because signal is emitted in a subthread and slot is in the main thread
connect(mDataFilesPage, SIGNAL(signalLoadedCellsChanged(QStringList)), mAdvancedPage, SLOT(slotLoadedCellsChanged(QStringList)), Qt::QueuedConnection);
-
}
Launcher::FirstRunDialogResult Launcher::MainDialog::showFirstRunDialog()
diff --git a/apps/launcher/utils/openalutil.cpp b/apps/launcher/utils/openalutil.cpp
index 53fd704203..469872d158 100644
--- a/apps/launcher/utils/openalutil.cpp
+++ b/apps/launcher/utils/openalutil.cpp
@@ -9,9 +9,9 @@
#define ALC_ALL_DEVICES_SPECIFIER 0x1013
#endif
-std::vector<const char *> Launcher::enumerateOpenALDevices()
+std::vector<std::string> Launcher::enumerateOpenALDevices()
{
- std::vector<const char *> devlist;
+ std::vector<std::string> devlist;
const ALCchar *devnames;
if(alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT"))
@@ -22,7 +22,7 @@ std::vector<const char *> Launcher::enumerateOpenALDevices()
{
devnames = alcGetString(nullptr, ALC_DEVICE_SPECIFIER);
}
-
+
while(devnames && *devnames)
{
devlist.emplace_back(devnames);
@@ -31,9 +31,9 @@ std::vector<const char *> Launcher::enumerateOpenALDevices()
return devlist;
}
-std::vector<const char *> Launcher::enumerateOpenALDevicesHrtf()
+std::vector<std::string> Launcher::enumerateOpenALDevicesHrtf()
{
- std::vector<const char *> ret;
+ std::vector<std::string> ret;
ALCdevice *device = alcOpenDevice(nullptr);
if(device)
diff --git a/apps/launcher/utils/openalutil.hpp b/apps/launcher/utils/openalutil.hpp
index 4a84fbae7d..b084dce7ce 100644
--- a/apps/launcher/utils/openalutil.hpp
+++ b/apps/launcher/utils/openalutil.hpp
@@ -1,7 +1,8 @@
#include <vector>
+#include <string>
namespace Launcher
{
- std::vector<const char *> enumerateOpenALDevices();
- std::vector<const char *> enumerateOpenALDevicesHrtf();
-} \ No newline at end of file
+ std::vector<std::string> enumerateOpenALDevices();
+ std::vector<std::string> enumerateOpenALDevicesHrtf();
+}
diff --git a/apps/navmeshtool/CMakeLists.txt b/apps/navmeshtool/CMakeLists.txt
new file mode 100644
index 0000000000..7df049af97
--- /dev/null
+++ b/apps/navmeshtool/CMakeLists.txt
@@ -0,0 +1,22 @@
+set(NAVMESHTOOL
+ worldspacedata.cpp
+ navmesh.cpp
+ main.cpp
+)
+source_group(apps\\navmeshtool FILES ${NAVMESHTOOL})
+
+openmw_add_executable(openmw-navmeshtool ${NAVMESHTOOL})
+
+target_link_libraries(openmw-navmeshtool
+ ${Boost_PROGRAM_OPTIONS_LIBRARY}
+ components
+)
+
+if (BUILD_WITH_CODE_COVERAGE)
+ add_definitions(--coverage)
+ target_link_libraries(openmw-navmeshtool gcov)
+endif()
+
+if (WIN32)
+ install(TARGETS openmw-navmeshtool RUNTIME DESTINATION ".")
+endif()
diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp
new file mode 100644
index 0000000000..3e867fcbc9
--- /dev/null
+++ b/apps/navmeshtool/main.cpp
@@ -0,0 +1,209 @@
+#include "worldspacedata.hpp"
+#include "navmesh.hpp"
+
+#include <components/debug/debugging.hpp>
+#include <components/detournavigator/navmeshdb.hpp>
+#include <components/detournavigator/recastglobalallocator.hpp>
+#include <components/esm/esmreader.hpp>
+#include <components/esm/variant.hpp>
+#include <components/esmloader/esmdata.hpp>
+#include <components/esmloader/load.hpp>
+#include <components/fallback/fallback.hpp>
+#include <components/fallback/validate.hpp>
+#include <components/files/configurationmanager.hpp>
+#include <components/resource/bulletshapemanager.hpp>
+#include <components/resource/imagemanager.hpp>
+#include <components/resource/niffilemanager.hpp>
+#include <components/resource/scenemanager.hpp>
+#include <components/settings/settings.hpp>
+#include <components/to_utf8/to_utf8.hpp>
+#include <components/version/version.hpp>
+#include <components/vfs/manager.hpp>
+#include <components/vfs/registerarchives.hpp>
+
+#include <osg/Vec3f>
+
+#include <boost/filesystem.hpp>
+#include <boost/program_options.hpp>
+
+#include <cstddef>
+#include <stdexcept>
+#include <string>
+#include <thread>
+#include <vector>
+
+namespace NavMeshTool
+{
+ namespace
+ {
+ namespace bpo = boost::program_options;
+
+ using StringsVector = std::vector<std::string>;
+
+ bpo::options_description makeOptionsDescription()
+ {
+ using Fallback::FallbackMap;
+
+ bpo::options_description result;
+
+ result.add_options()
+ ("help", "print help message")
+
+ ("version", "print version information and quit")
+
+ ("data", bpo::value<Files::MaybeQuotedPathContainer>()->default_value(Files::MaybeQuotedPathContainer(), "data")
+ ->multitoken()->composing(), "set data directories (later directories have higher priority)")
+
+ ("data-local", bpo::value<Files::MaybeQuotedPathContainer::value_type>()->default_value(Files::MaybeQuotedPathContainer::value_type(), ""),
+ "set local data directory (highest priority)")
+
+ ("fallback-archive", bpo::value<StringsVector>()->default_value(StringsVector(), "fallback-archive")
+ ->multitoken()->composing(), "set fallback BSA archives (later archives have higher priority)")
+
+ ("resources", bpo::value<Files::MaybeQuotedPath>()->default_value(Files::MaybeQuotedPath(), "resources"),
+ "set resources directory")
+
+ ("content", bpo::value<StringsVector>()->default_value(StringsVector(), "")
+ ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon/omwscripts")
+
+ ("fs-strict", bpo::value<bool>()->implicit_value(true)
+ ->default_value(false), "strict file system handling (no case folding)")
+
+ ("encoding", bpo::value<std::string>()->
+ default_value("win1252"),
+ "Character encoding used in OpenMW game messages:\n"
+ "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n"
+ "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n"
+ "\n\twin1252 - Western European (Latin) alphabet, used by default")
+
+ ("fallback", bpo::value<Fallback::FallbackMap>()->default_value(Fallback::FallbackMap(), "")
+ ->multitoken()->composing(), "fallback values")
+
+ ("threads", bpo::value<std::size_t>()->default_value(std::max<std::size_t>(std::thread::hardware_concurrency() - 1, 1)),
+ "number of threads for parallel processing")
+
+ ("process-interior-cells", bpo::value<bool>()->implicit_value(true)
+ ->default_value(false), "build navmesh for interior cells")
+ ;
+
+ return result;
+ }
+
+ void loadSettings(const Files::ConfigurationManager& config, Settings::Manager& settings)
+ {
+ const std::string localDefault = (config.getLocalPath() / "defaults.bin").string();
+ const std::string globalDefault = (config.getGlobalPath() / "defaults.bin").string();
+
+ if (boost::filesystem::exists(localDefault))
+ settings.loadDefault(localDefault);
+ else if (boost::filesystem::exists(globalDefault))
+ settings.loadDefault(globalDefault);
+ else
+ throw std::runtime_error("No default settings file found! Make sure the file \"defaults.bin\" was properly installed.");
+
+ const std::string settingsPath = (config.getUserConfigPath() / "settings.cfg").string();
+ if (boost::filesystem::exists(settingsPath))
+ settings.loadUser(settingsPath);
+ }
+
+ int runNavMeshTool(int argc, char *argv[])
+ {
+ bpo::options_description desc = makeOptionsDescription();
+
+ bpo::parsed_options options = bpo::command_line_parser(argc, argv)
+ .options(desc).allow_unregistered().run();
+ bpo::variables_map variables;
+
+ bpo::store(options, variables);
+ bpo::notify(variables);
+
+ if (variables.find("help") != variables.end())
+ {
+ getRawStdout() << desc << std::endl;
+ return 0;
+ }
+
+ Files::ConfigurationManager config;
+
+ bpo::variables_map composingVariables = Files::separateComposingVariables(variables, desc);
+ config.readConfiguration(variables, desc);
+ Files::mergeComposingVariables(variables, composingVariables, desc);
+
+ const std::string encoding(variables["encoding"].as<std::string>());
+ Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding);
+ ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(encoding));
+
+ Files::PathContainer dataDirs(asPathContainer(variables["data"].as<Files::MaybeQuotedPathContainer>()));
+
+ auto local = variables["data-local"].as<Files::MaybeQuotedPathContainer::value_type>();
+ if (!local.empty())
+ dataDirs.push_back(std::move(local));
+
+ config.processPaths(dataDirs);
+
+ const auto fsStrict = variables["fs-strict"].as<bool>();
+ const auto resDir = variables["resources"].as<Files::MaybeQuotedPath>();
+ Version::Version v = Version::getOpenmwVersion(resDir.string());
+ Log(Debug::Info) << v.describe();
+ dataDirs.insert(dataDirs.begin(), resDir / "vfs");
+ const auto fileCollections = Files::Collections(dataDirs, !fsStrict);
+ const auto archives = variables["fallback-archive"].as<StringsVector>();
+ const auto contentFiles = variables["content"].as<StringsVector>();
+ const std::size_t threadsNumber = variables["threads"].as<std::size_t>();
+
+ if (threadsNumber < 1)
+ {
+ std::cerr << "Invalid threads number: " << threadsNumber << ", expected >= 1";
+ return -1;
+ }
+
+ const bool processInteriorCells = variables["process-interior-cells"].as<bool>();
+
+ Fallback::Map::init(variables["fallback"].as<Fallback::FallbackMap>().mMap);
+
+ VFS::Manager vfs(fsStrict);
+
+ VFS::registerArchives(&vfs, fileCollections, archives, true);
+
+ Settings::Manager settings;
+ loadSettings(config, settings);
+
+ const osg::Vec3f agentHalfExtents = Settings::Manager::getVector3("default actor pathfind half extents", "Game");
+
+ DetourNavigator::NavMeshDb db((config.getUserDataPath() / "navmesh.db").string());
+
+ std::vector<ESM::ESMReader> readers(contentFiles.size());
+ EsmLoader::Query query;
+ query.mLoadActivators = true;
+ query.mLoadCells = true;
+ query.mLoadContainers = true;
+ query.mLoadDoors = true;
+ query.mLoadGameSettings = true;
+ query.mLoadLands = true;
+ query.mLoadStatics = true;
+ const EsmLoader::EsmData esmData = EsmLoader::loadEsmData(query, contentFiles, fileCollections, readers, &encoder);
+
+ Resource::ImageManager imageManager(&vfs);
+ Resource::NifFileManager nifFileManager(&vfs);
+ Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager);
+ Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager);
+ DetourNavigator::RecastGlobalAllocator::init();
+ DetourNavigator::Settings navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager();
+ navigatorSettings.mRecast.mSwimHeightScale = EsmLoader::getGameSetting(esmData.mGameSettings, "fSwimHeightScale").getFloat();
+
+ WorldspaceData cellsData = gatherWorldspaceData(navigatorSettings, readers, vfs, bulletShapeManager,
+ esmData, processInteriorCells);
+
+ generateAllNavMeshTiles(agentHalfExtents, navigatorSettings, threadsNumber, cellsData, std::move(db));
+
+ Log(Debug::Info) << "Done";
+
+ return 0;
+ }
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ return wrapApplication(NavMeshTool::runNavMeshTool, argc, argv, "NavMeshTool");
+}
diff --git a/apps/navmeshtool/navmesh.cpp b/apps/navmeshtool/navmesh.cpp
new file mode 100644
index 0000000000..4187197947
--- /dev/null
+++ b/apps/navmeshtool/navmesh.cpp
@@ -0,0 +1,212 @@
+#include "navmesh.hpp"
+
+#include "worldspacedata.hpp"
+
+#include <components/bullethelpers/aabb.hpp>
+#include <components/debug/debuglog.hpp>
+#include <components/detournavigator/generatenavmeshtile.hpp>
+#include <components/detournavigator/gettilespositions.hpp>
+#include <components/detournavigator/navmeshdb.hpp>
+#include <components/detournavigator/navmeshdbutils.hpp>
+#include <components/detournavigator/offmeshconnection.hpp>
+#include <components/detournavigator/offmeshconnectionsmanager.hpp>
+#include <components/detournavigator/preparednavmeshdata.hpp>
+#include <components/detournavigator/recastmesh.hpp>
+#include <components/detournavigator/recastmeshprovider.hpp>
+#include <components/detournavigator/serialization.hpp>
+#include <components/detournavigator/tilecachedrecastmeshmanager.hpp>
+#include <components/detournavigator/tileposition.hpp>
+#include <components/esm/loadcell.hpp>
+#include <components/misc/guarded.hpp>
+#include <components/misc/progressreporter.hpp>
+#include <components/sceneutil/workqueue.hpp>
+#include <components/sqlite3/transaction.hpp>
+
+#include <DetourNavMesh.h>
+
+#include <osg/Vec3f>
+
+#include <algorithm>
+#include <atomic>
+#include <chrono>
+#include <cstddef>
+#include <stdexcept>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace NavMeshTool
+{
+ namespace
+ {
+ using DetourNavigator::GenerateNavMeshTile;
+ using DetourNavigator::NavMeshDb;
+ using DetourNavigator::NavMeshTileInfo;
+ using DetourNavigator::PreparedNavMeshData;
+ using DetourNavigator::RecastMeshProvider;
+ using DetourNavigator::MeshSource;
+ using DetourNavigator::Settings;
+ using DetourNavigator::ShapeId;
+ using DetourNavigator::TileId;
+ using DetourNavigator::TilePosition;
+ using DetourNavigator::TileVersion;
+ using Sqlite3::Transaction;
+
+ void logGeneratedTiles(std::size_t provided, std::size_t expected)
+ {
+ Log(Debug::Info) << provided << "/" << expected << " ("
+ << (static_cast<double>(provided) / static_cast<double>(expected) * 100)
+ << "%) navmesh tiles are generated";
+ }
+
+ struct LogGeneratedTiles
+ {
+ void operator()(std::size_t provided, std::size_t expected) const
+ {
+ logGeneratedTiles(provided, expected);
+ }
+ };
+
+ class NavMeshTileConsumer final : public DetourNavigator::NavMeshTileConsumer
+ {
+ public:
+ std::atomic_size_t mExpected {0};
+
+ explicit NavMeshTileConsumer(NavMeshDb&& db)
+ : mDb(std::move(db))
+ , mTransaction(mDb.startTransaction())
+ , mNextTileId(mDb.getMaxTileId() + 1)
+ , mNextShapeId(mDb.getMaxShapeId() + 1)
+ {}
+
+ std::size_t getProvided() const { return mProvided.load(); }
+
+ std::size_t getInserted() const { return mInserted.load(); }
+
+ std::size_t getUpdated() const { return mUpdated.load(); }
+
+ std::int64_t resolveMeshSource(const MeshSource& source) override
+ {
+ const std::lock_guard lock(mMutex);
+ return DetourNavigator::resolveMeshSource(mDb, source, mNextShapeId);
+ }
+
+ std::optional<NavMeshTileInfo> find(const std::string& worldspace, const TilePosition &tilePosition,
+ const std::vector<std::byte> &input) override
+ {
+ std::optional<NavMeshTileInfo> result;
+ std::lock_guard lock(mMutex);
+ if (const auto tile = mDb.findTile(worldspace, tilePosition, input))
+ {
+ NavMeshTileInfo info;
+ info.mTileId = tile->mTileId;
+ info.mVersion = tile->mVersion;
+ result.emplace(info);
+ }
+ return result;
+ }
+
+ void ignore() override { report(); }
+
+ void insert(const std::string& worldspace, const TilePosition& tilePosition, std::int64_t version,
+ const std::vector<std::byte>& input, PreparedNavMeshData& data) override
+ {
+ data.mUserId = static_cast<unsigned>(mNextTileId);
+ {
+ std::lock_guard lock(mMutex);
+ mDb.insertTile(mNextTileId, worldspace, tilePosition, TileVersion {version}, input, serialize(data));
+ ++mNextTileId.t;
+ }
+ ++mInserted;
+ report();
+ }
+
+ void update(std::int64_t tileId, std::int64_t version, PreparedNavMeshData& data) override
+ {
+ data.mUserId = static_cast<unsigned>(tileId);
+ {
+ std::lock_guard lock(mMutex);
+ mDb.updateTile(TileId {tileId}, TileVersion {version}, serialize(data));
+ }
+ ++mUpdated;
+ report();
+ }
+
+ void wait()
+ {
+ constexpr std::size_t tilesPerTransaction = 3000;
+ std::unique_lock lock(mMutex);
+ while (mProvided < mExpected)
+ {
+ mHasTile.wait(lock);
+ if (mProvided % tilesPerTransaction == 0)
+ {
+ mTransaction.commit();
+ mTransaction = mDb.startTransaction();
+ }
+ }
+ logGeneratedTiles(mProvided, mExpected);
+ }
+
+ void commit() { mTransaction.commit(); }
+
+ private:
+ std::atomic_size_t mProvided {0};
+ std::atomic_size_t mInserted {0};
+ std::atomic_size_t mUpdated {0};
+ std::mutex mMutex;
+ NavMeshDb mDb;
+ Transaction mTransaction;
+ TileId mNextTileId;
+ std::condition_variable mHasTile;
+ Misc::ProgressReporter<LogGeneratedTiles> mReporter;
+ ShapeId mNextShapeId;
+
+ void report()
+ {
+ mReporter(mProvided + 1, mExpected);
+ ++mProvided;
+ mHasTile.notify_one();
+ }
+ };
+ }
+
+ void generateAllNavMeshTiles(const osg::Vec3f& agentHalfExtents, const Settings& settings,
+ const std::size_t threadsNumber, WorldspaceData& data, NavMeshDb&& db)
+ {
+ Log(Debug::Info) << "Generating navmesh tiles by " << threadsNumber << " parallel workers...";
+
+ SceneUtil::WorkQueue workQueue(threadsNumber);
+ auto navMeshTileConsumer = std::make_shared<NavMeshTileConsumer>(std::move(db));
+ std::size_t tiles = 0;
+
+ for (const std::unique_ptr<WorldspaceNavMeshInput>& input : data.mNavMeshInputs)
+ {
+ DetourNavigator::getTilesPositions(
+ Misc::Convert::toOsg(input->mAabb.m_min), Misc::Convert::toOsg(input->mAabb.m_max), settings.mRecast,
+ [&] (const TilePosition& tilePosition)
+ {
+ workQueue.addWorkItem(new GenerateNavMeshTile(
+ input->mWorldspace,
+ tilePosition,
+ RecastMeshProvider(input->mTileCachedRecastMeshManager),
+ agentHalfExtents,
+ settings,
+ navMeshTileConsumer
+ ));
+
+ ++tiles;
+ });
+
+ navMeshTileConsumer->mExpected = tiles;
+ }
+
+ navMeshTileConsumer->wait();
+ navMeshTileConsumer->commit();
+
+ Log(Debug::Info) << "Generated navmesh for " << navMeshTileConsumer->getProvided() << " tiles, "
+ << navMeshTileConsumer->getInserted() << " are inserted and "
+ << navMeshTileConsumer->getUpdated() << " updated";
+ }
+}
diff --git a/apps/navmeshtool/navmesh.hpp b/apps/navmeshtool/navmesh.hpp
new file mode 100644
index 0000000000..725f0cd6a4
--- /dev/null
+++ b/apps/navmeshtool/navmesh.hpp
@@ -0,0 +1,23 @@
+#ifndef OPENMW_NAVMESHTOOL_NAVMESH_H
+#define OPENMW_NAVMESHTOOL_NAVMESH_H
+
+#include <osg/Vec3f>
+
+#include <cstddef>
+#include <string_view>
+
+namespace DetourNavigator
+{
+ class NavMeshDb;
+ struct Settings;
+}
+
+namespace NavMeshTool
+{
+ struct WorldspaceData;
+
+ void generateAllNavMeshTiles(const osg::Vec3f& agentHalfExtents, const DetourNavigator::Settings& settings,
+ const std::size_t threadsNumber, WorldspaceData& cellsData, DetourNavigator::NavMeshDb&& db);
+}
+
+#endif
diff --git a/apps/navmeshtool/worldspacedata.cpp b/apps/navmeshtool/worldspacedata.cpp
new file mode 100644
index 0000000000..d05a8fe3f0
--- /dev/null
+++ b/apps/navmeshtool/worldspacedata.cpp
@@ -0,0 +1,330 @@
+#include "worldspacedata.hpp"
+
+#include <components/bullethelpers/aabb.hpp>
+#include <components/bullethelpers/heightfield.hpp>
+#include <components/debug/debuglog.hpp>
+#include <components/detournavigator/gettilespositions.hpp>
+#include <components/detournavigator/objectid.hpp>
+#include <components/detournavigator/recastmesh.hpp>
+#include <components/detournavigator/tilecachedrecastmeshmanager.hpp>
+#include <components/esm/cellref.hpp>
+#include <components/esm/esmreader.hpp>
+#include <components/esm/loadcell.hpp>
+#include <components/esm/loadland.hpp>
+#include <components/esmloader/esmdata.hpp>
+#include <components/esmloader/lessbyid.hpp>
+#include <components/esmloader/record.hpp>
+#include <components/misc/coordinateconverter.hpp>
+#include <components/misc/resourcehelpers.hpp>
+#include <components/misc/stringops.hpp>
+#include <components/resource/bulletshapemanager.hpp>
+#include <components/settings/settings.hpp>
+#include <components/vfs/manager.hpp>
+
+#include <LinearMath/btVector3.h>
+
+#include <osg/Vec2i>
+#include <osg/Vec3f>
+#include <osg/ref_ptr>
+
+#include <algorithm>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <string_view>
+#include <thread>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+namespace NavMeshTool
+{
+ namespace
+ {
+ using DetourNavigator::CollisionShape;
+ using DetourNavigator::HeightfieldPlane;
+ using DetourNavigator::HeightfieldShape;
+ using DetourNavigator::HeightfieldSurface;
+ using DetourNavigator::ObjectId;
+ using DetourNavigator::ObjectTransform;
+
+ struct CellRef
+ {
+ ESM::RecNameInts mType;
+ ESM::RefNum mRefNum;
+ std::string mRefId;
+ float mScale;
+ ESM::Position mPos;
+
+ CellRef(ESM::RecNameInts type, ESM::RefNum refNum, std::string&& refId, float scale, const ESM::Position& pos)
+ : mType(type), mRefNum(refNum), mRefId(std::move(refId)), mScale(scale), mPos(pos) {}
+ };
+
+ ESM::RecNameInts getType(const EsmLoader::EsmData& esmData, std::string_view refId)
+ {
+ const auto it = std::lower_bound(esmData.mRefIdTypes.begin(), esmData.mRefIdTypes.end(),
+ refId, EsmLoader::LessById {});
+ if (it == esmData.mRefIdTypes.end() || it->mId != refId)
+ return {};
+ return it->mType;
+ }
+
+ std::vector<CellRef> loadCellRefs(const ESM::Cell& cell, const EsmLoader::EsmData& esmData,
+ std::vector<ESM::ESMReader>& readers)
+ {
+ std::vector<EsmLoader::Record<CellRef>> cellRefs;
+
+ for (std::size_t i = 0; i < cell.mContextList.size(); i++)
+ {
+ ESM::ESMReader& reader = readers[static_cast<std::size_t>(cell.mContextList[i].index)];
+ cell.restore(reader, static_cast<int>(i));
+ ESM::CellRef cellRef;
+ bool deleted = false;
+ while (ESM::Cell::getNextRef(reader, cellRef, deleted))
+ {
+ Misc::StringUtils::lowerCaseInPlace(cellRef.mRefID);
+ const ESM::RecNameInts type = getType(esmData, cellRef.mRefID);
+ if (type == ESM::RecNameInts {})
+ continue;
+ cellRefs.emplace_back(deleted, type, cellRef.mRefNum, std::move(cellRef.mRefID),
+ cellRef.mScale, cellRef.mPos);
+ }
+ }
+
+ Log(Debug::Debug) << "Loaded " << cellRefs.size() << " cell refs";
+
+ const auto getKey = [] (const EsmLoader::Record<CellRef>& v) -> const ESM::RefNum& { return v.mValue.mRefNum; };
+ std::vector<CellRef> result = prepareRecords(cellRefs, getKey);
+
+ Log(Debug::Debug) << "Prepared " << result.size() << " unique cell refs";
+
+ return result;
+ }
+
+ template <class F>
+ void forEachObject(const ESM::Cell& cell, const EsmLoader::EsmData& esmData, const VFS::Manager& vfs,
+ Resource::BulletShapeManager& bulletShapeManager, std::vector<ESM::ESMReader>& readers,
+ F&& f)
+ {
+ std::vector<CellRef> cellRefs = loadCellRefs(cell, esmData, readers);
+
+ Log(Debug::Debug) << "Prepared " << cellRefs.size() << " unique cell refs";
+
+ for (CellRef& cellRef : cellRefs)
+ {
+ std::string model(getModel(esmData, cellRef.mRefId, cellRef.mType));
+ if (model.empty())
+ continue;
+
+ if (cellRef.mType != ESM::REC_STAT)
+ model = Misc::ResourceHelpers::correctActorModelPath(model, &vfs);
+
+ osg::ref_ptr<const Resource::BulletShape> shape = [&]
+ {
+ try
+ {
+ return bulletShapeManager.getShape("meshes/" + model);
+ }
+ catch (const std::exception& e)
+ {
+ Log(Debug::Warning) << "Failed to load cell ref \"" << cellRef.mRefId << "\" model \"" << model << "\": " << e.what();
+ return osg::ref_ptr<const Resource::BulletShape>();
+ }
+ } ();
+
+ if (shape == nullptr || shape->mCollisionShape == nullptr)
+ continue;
+
+ osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance(new Resource::BulletShapeInstance(std::move(shape)));
+
+ switch (cellRef.mType)
+ {
+ case ESM::REC_ACTI:
+ case ESM::REC_CONT:
+ case ESM::REC_DOOR:
+ case ESM::REC_STAT:
+ f(BulletObject(std::move(shapeInstance), cellRef.mPos, cellRef.mScale));
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ struct GetXY
+ {
+ osg::Vec2i operator()(const ESM::Land& value) const { return osg::Vec2i(value.mX, value.mY); }
+ };
+
+ struct LessByXY
+ {
+ bool operator ()(const ESM::Land& lhs, const ESM::Land& rhs) const
+ {
+ return GetXY {}(lhs) < GetXY {}(rhs);
+ }
+
+ bool operator ()(const ESM::Land& lhs, const osg::Vec2i& rhs) const
+ {
+ return GetXY {}(lhs) < rhs;
+ }
+
+ bool operator ()(const osg::Vec2i& lhs, const ESM::Land& rhs) const
+ {
+ return lhs < GetXY {}(rhs);
+ }
+ };
+
+ btAABB getAabb(const osg::Vec2i& cellPosition, btScalar minHeight, btScalar maxHeight)
+ {
+ btAABB aabb;
+ aabb.m_min = btVector3(
+ static_cast<btScalar>(cellPosition.x() * ESM::Land::REAL_SIZE),
+ static_cast<btScalar>(cellPosition.y() * ESM::Land::REAL_SIZE),
+ minHeight
+ );
+ aabb.m_min = btVector3(
+ static_cast<btScalar>((cellPosition.x() + 1) * ESM::Land::REAL_SIZE),
+ static_cast<btScalar>((cellPosition.y() + 1) * ESM::Land::REAL_SIZE),
+ maxHeight
+ );
+ return aabb;
+ }
+
+ void mergeOrAssign(const btAABB& aabb, btAABB& target, bool& initialized)
+ {
+ if (initialized)
+ return target.merge(aabb);
+
+ target.m_min = aabb.m_min;
+ target.m_max = aabb.m_max;
+ initialized = true;
+ }
+
+ std::tuple<HeightfieldShape, float, float> makeHeightfieldShape(const std::optional<ESM::Land>& land,
+ const osg::Vec2i& cellPosition, std::vector<std::vector<float>>& heightfields,
+ std::vector<std::unique_ptr<ESM::Land::LandData>>& landDatas)
+ {
+ if (!land.has_value() || osg::Vec2i(land->mX, land->mY) != cellPosition
+ || (land->mDataTypes & ESM::Land::DATA_VHGT) == 0)
+ return {HeightfieldPlane {ESM::Land::DEFAULT_HEIGHT}, ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT};
+
+ ESM::Land::LandData& landData = *landDatas.emplace_back(std::make_unique<ESM::Land::LandData>());
+ land->loadData(ESM::Land::DATA_VHGT, &landData);
+ heightfields.push_back(std::vector<float>(std::begin(landData.mHeights), std::end(landData.mHeights)));
+ HeightfieldSurface surface;
+ surface.mHeights = heightfields.back().data();
+ surface.mMinHeight = landData.mMinHeight;
+ surface.mMaxHeight = landData.mMaxHeight;
+ surface.mSize = static_cast<std::size_t>(ESM::Land::LAND_SIZE);
+ return {surface, landData.mMinHeight, landData.mMaxHeight};
+ }
+ }
+
+ WorldspaceNavMeshInput::WorldspaceNavMeshInput(std::string worldspace, const DetourNavigator::RecastSettings& settings)
+ : mWorldspace(std::move(worldspace))
+ , mTileCachedRecastMeshManager(settings)
+ {
+ mAabb.m_min = btVector3(0, 0, 0);
+ mAabb.m_max = btVector3(0, 0, 0);
+ }
+
+ WorldspaceData gatherWorldspaceData(const DetourNavigator::Settings& settings, std::vector<ESM::ESMReader>& readers,
+ const VFS::Manager& vfs, Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData,
+ bool processInteriorCells)
+ {
+ Log(Debug::Info) << "Processing " << esmData.mCells.size() << " cells...";
+
+ std::map<std::string_view, std::unique_ptr<WorldspaceNavMeshInput>> navMeshInputs;
+ WorldspaceData data;
+
+ std::size_t objectsCounter = 0;
+
+ for (std::size_t i = 0; i < esmData.mCells.size(); ++i)
+ {
+ const ESM::Cell& cell = esmData.mCells[i];
+ const bool exterior = cell.isExterior();
+
+ if (!exterior && !processInteriorCells)
+ {
+ Log(Debug::Info) << "Skipped interior"
+ << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") \"" << cell.getDescription() << "\"";
+ continue;
+ }
+
+ Log(Debug::Debug) << "Processing " << (exterior ? "exterior" : "interior")
+ << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") \"" << cell.getDescription() << "\"";
+
+ const osg::Vec2i cellPosition(cell.mData.mX, cell.mData.mY);
+ const std::size_t cellObjectsBegin = data.mObjects.size();
+
+ WorldspaceNavMeshInput& navMeshInput = [&] () -> WorldspaceNavMeshInput&
+ {
+ auto it = navMeshInputs.find(cell.mCellId.mWorldspace);
+ if (it == navMeshInputs.end())
+ {
+ it = navMeshInputs.emplace(cell.mCellId.mWorldspace,
+ std::make_unique<WorldspaceNavMeshInput>(cell.mCellId.mWorldspace, settings.mRecast)).first;
+ it->second->mTileCachedRecastMeshManager.setWorldspace(cell.mCellId.mWorldspace);
+ }
+ return *it->second;
+ } ();
+
+ if (exterior)
+ {
+ const auto it = std::lower_bound(esmData.mLands.begin(), esmData.mLands.end(), cellPosition, LessByXY {});
+ const auto [heightfieldShape, minHeight, maxHeight] = makeHeightfieldShape(
+ it == esmData.mLands.end() ? std::optional<ESM::Land>() : *it,
+ cellPosition, data.mHeightfields, data.mLandData
+ );
+
+ mergeOrAssign(getAabb(cellPosition, minHeight, maxHeight),
+ navMeshInput.mAabb, navMeshInput.mAabbInitialized);
+
+ navMeshInput.mTileCachedRecastMeshManager.addHeightfield(cellPosition, ESM::Land::REAL_SIZE, heightfieldShape);
+
+ navMeshInput.mTileCachedRecastMeshManager.addWater(cellPosition, ESM::Land::REAL_SIZE, -1);
+ }
+ else
+ {
+ if ((cell.mData.mFlags & ESM::Cell::HasWater) != 0)
+ navMeshInput.mTileCachedRecastMeshManager.addWater(cellPosition, std::numeric_limits<int>::max(), cell.mWater);
+ }
+
+ forEachObject(cell, esmData, vfs, bulletShapeManager, readers,
+ [&] (BulletObject object)
+ {
+ const btTransform& transform = object.getCollisionObject().getWorldTransform();
+ const btAABB aabb = BulletHelpers::getAabb(*object.getCollisionObject().getCollisionShape(), transform);
+ mergeOrAssign(aabb, navMeshInput.mAabb, navMeshInput.mAabbInitialized);
+ if (const btCollisionShape* avoid = object.getShapeInstance()->mAvoidCollisionShape.get())
+ navMeshInput.mAabb.merge(BulletHelpers::getAabb(*avoid, transform));
+
+ const ObjectId objectId(++objectsCounter);
+ const CollisionShape shape(object.getShapeInstance(), *object.getCollisionObject().getCollisionShape(), object.getObjectTransform());
+
+ navMeshInput.mTileCachedRecastMeshManager.addObject(objectId, shape, transform, DetourNavigator::AreaType_ground);
+
+ if (const btCollisionShape* avoid = object.getShapeInstance()->mAvoidCollisionShape.get())
+ {
+ const CollisionShape avoidShape(object.getShapeInstance(), *avoid, object.getObjectTransform());
+ navMeshInput.mTileCachedRecastMeshManager.addObject(objectId, avoidShape, transform, DetourNavigator::AreaType_null);
+ }
+
+ data.mObjects.emplace_back(std::move(object));
+ });
+
+ Log(Debug::Info) << "Processed " << (exterior ? "exterior" : "interior")
+ << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") " << cell.getDescription()
+ << " with " << (data.mObjects.size() - cellObjectsBegin) << " objects";
+ }
+
+ data.mNavMeshInputs.reserve(navMeshInputs.size());
+ std::transform(navMeshInputs.begin(), navMeshInputs.end(), std::back_inserter(data.mNavMeshInputs),
+ [] (auto& v) { return std::move(v.second); });
+
+ Log(Debug::Info) << "Processed " << esmData.mCells.size() << " cells, added "
+ << data.mObjects.size() << " objects and " << data.mHeightfields.size() << " height fields";
+
+ return data;
+ }
+}
diff --git a/apps/navmeshtool/worldspacedata.hpp b/apps/navmeshtool/worldspacedata.hpp
new file mode 100644
index 0000000000..3dccd5a8bc
--- /dev/null
+++ b/apps/navmeshtool/worldspacedata.hpp
@@ -0,0 +1,97 @@
+#ifndef OPENMW_NAVMESHTOOL_WORLDSPACEDATA_H
+#define OPENMW_NAVMESHTOOL_WORLDSPACEDATA_H
+
+#include <components/bullethelpers/collisionobject.hpp>
+#include <components/detournavigator/tilecachedrecastmeshmanager.hpp>
+#include <components/esm/loadland.hpp>
+#include <components/misc/convert.hpp>
+#include <components/resource/bulletshape.hpp>
+
+#include <BulletCollision/CollisionDispatch/btCollisionObject.h>
+#include <BulletCollision/Gimpact/btBoxCollision.h>
+#include <LinearMath/btVector3.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace ESM
+{
+ class ESMReader;
+}
+
+namespace VFS
+{
+ class Manager;
+}
+
+namespace Resource
+{
+ class BulletShapeManager;
+}
+
+namespace EsmLoader
+{
+ struct EsmData;
+}
+
+namespace DetourNavigator
+{
+ struct Settings;
+}
+
+namespace NavMeshTool
+{
+ using DetourNavigator::TileCachedRecastMeshManager;
+ using DetourNavigator::ObjectTransform;
+
+ struct WorldspaceNavMeshInput
+ {
+ std::string mWorldspace;
+ TileCachedRecastMeshManager mTileCachedRecastMeshManager;
+ btAABB mAabb;
+ bool mAabbInitialized = false;
+
+ explicit WorldspaceNavMeshInput(std::string worldspace, const DetourNavigator::RecastSettings& settings);
+ };
+
+ class BulletObject
+ {
+ public:
+ BulletObject(osg::ref_ptr<Resource::BulletShapeInstance>&& shapeInstance, const ESM::Position& position,
+ float localScaling)
+ : mShapeInstance(std::move(shapeInstance))
+ , mObjectTransform {position, localScaling}
+ , mCollisionObject(BulletHelpers::makeCollisionObject(
+ mShapeInstance->mCollisionShape.get(),
+ Misc::Convert::toBullet(position.asVec3()),
+ Misc::Convert::toBullet(Misc::Convert::makeOsgQuat(position))
+ ))
+ {
+ mShapeInstance->setLocalScaling(btVector3(localScaling, localScaling, localScaling));
+ }
+
+ const osg::ref_ptr<Resource::BulletShapeInstance>& getShapeInstance() const noexcept { return mShapeInstance; }
+ const DetourNavigator::ObjectTransform& getObjectTransform() const noexcept { return mObjectTransform; }
+ btCollisionObject& getCollisionObject() const noexcept { return *mCollisionObject; }
+
+ private:
+ osg::ref_ptr<Resource::BulletShapeInstance> mShapeInstance;
+ DetourNavigator::ObjectTransform mObjectTransform;
+ std::unique_ptr<btCollisionObject> mCollisionObject;
+ };
+
+ struct WorldspaceData
+ {
+ std::vector<std::unique_ptr<WorldspaceNavMeshInput>> mNavMeshInputs;
+ std::vector<BulletObject> mObjects;
+ std::vector<std::unique_ptr<ESM::Land::LandData>> mLandData;
+ std::vector<std::vector<float>> mHeightfields;
+ };
+
+ WorldspaceData gatherWorldspaceData(const DetourNavigator::Settings& settings, std::vector<ESM::ESMReader>& readers,
+ const VFS::Manager& vfs, Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData,
+ bool processInteriorCells);
+}
+
+#endif
diff --git a/apps/niftest/niftest.cpp b/apps/niftest/niftest.cpp
index cb6205ef5c..c42df4a423 100644
--- a/apps/niftest/niftest.cpp
+++ b/apps/niftest/niftest.cpp
@@ -2,8 +2,8 @@
#include <iostream>
#include <fstream>
-#include <cstdlib>
+#include <components/misc/stringops.hpp>
#include <components/nif/niffile.hpp>
#include <components/files/constrainedfilestream.hpp>
#include <components/vfs/manager.hpp>
@@ -18,18 +18,10 @@ namespace bpo = boost::program_options;
namespace bfs = boost::filesystem;
///See if the file has the named extension
-bool hasExtension(std::string filename, std::string extensionToFind)
+bool hasExtension(std::string filename, std::string extensionToFind)
{
std::string extension = filename.substr(filename.find_last_of('.')+1);
-
- //Convert strings to lower case for comparison
- std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
- std::transform(extensionToFind.begin(), extensionToFind.end(), extensionToFind.begin(), ::tolower);
-
- if(extension == extensionToFind)
- return true;
- else
- return false;
+ return Misc::StringUtils::ciEqual(extension, extensionToFind);
}
///See if the file has the "nif" extension.
diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt
index 952bbbdbda..b3d405d1a3 100644
--- a/apps/opencs/CMakeLists.txt
+++ b/apps/opencs/CMakeLists.txt
@@ -183,8 +183,7 @@ if(APPLE)
set(OPENCS_BUNDLE_NAME "OpenMW-CS")
set(OPENCS_BUNDLE_RESOURCES_DIR "${OpenMW_BINARY_DIR}/${OPENCS_BUNDLE_NAME}.app/Contents/Resources")
- set(OPENMW_MYGUI_FILES_ROOT ${OPENCS_BUNDLE_RESOURCES_DIR})
- set(OPENMW_SHADERS_ROOT ${OPENCS_BUNDLE_RESOURCES_DIR})
+ set(OPENMW_RESOURCES_ROOT ${OPENCS_BUNDLE_RESOURCES_DIR})
add_subdirectory(../../files/ ${CMAKE_CURRENT_BINARY_DIR}/files)
diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp
index 3f53a523f4..862d1c545f 100644
--- a/apps/opencs/editor.cpp
+++ b/apps/opencs/editor.cpp
@@ -88,16 +88,16 @@ std::pair<Files::PathContainer, std::vector<std::string> > CS::Editor::readConfi
boost::program_options::options_description desc("Syntax: openmw-cs <options>\nAllowed options");
desc.add_options()
- ("data", boost::program_options::value<Files::EscapePathContainer>()->default_value(Files::EscapePathContainer(), "data")->multitoken()->composing())
- ("data-local", boost::program_options::value<Files::EscapePath>()->default_value(Files::EscapePath(), ""))
+ ("data", boost::program_options::value<Files::MaybeQuotedPathContainer>()->default_value(Files::MaybeQuotedPathContainer(), "data")->multitoken()->composing())
+ ("data-local", boost::program_options::value<Files::MaybeQuotedPathContainer::value_type>()->default_value(Files::MaybeQuotedPathContainer::value_type(), ""))
("fs-strict", boost::program_options::value<bool>()->implicit_value(true)->default_value(false))
- ("encoding", boost::program_options::value<Files::EscapeHashString>()->default_value("win1252"))
- ("resources", boost::program_options::value<Files::EscapePath>()->default_value(Files::EscapePath(), "resources"))
- ("fallback-archive", boost::program_options::value<Files::EscapeStringVector>()->
- default_value(Files::EscapeStringVector(), "fallback-archive")->multitoken())
+ ("encoding", boost::program_options::value<std::string>()->default_value("win1252"))
+ ("resources", boost::program_options::value<Files::MaybeQuotedPath>()->default_value(Files::MaybeQuotedPath(), "resources"))
+ ("fallback-archive", boost::program_options::value<std::vector<std::string>>()->
+ default_value(std::vector<std::string>(), "fallback-archive")->multitoken())
("fallback", boost::program_options::value<FallbackMap>()->default_value(FallbackMap(), "")
->multitoken()->composing(), "fallback values")
- ("script-blacklist", boost::program_options::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
+ ("script-blacklist", boost::program_options::value<std::vector<std::string>>()->default_value(std::vector<std::string>(), "")
->multitoken(), "exclude specified script from the verifier (if the use of the blacklist is enabled)")
("script-blacklist-use", boost::program_options::value<bool>()->implicit_value(true)
->default_value(true), "enable script blacklisting");
@@ -108,24 +108,24 @@ std::pair<Files::PathContainer, std::vector<std::string> > CS::Editor::readConfi
Fallback::Map::init(variables["fallback"].as<FallbackMap>().mMap);
- mEncodingName = variables["encoding"].as<Files::EscapeHashString>().toStdString();
+ mEncodingName = variables["encoding"].as<std::string>();
mDocumentManager.setEncoding(ToUTF8::calculateEncoding(mEncodingName));
mFileDialog.setEncoding (QString::fromUtf8(mEncodingName.c_str()));
- mDocumentManager.setResourceDir (mResources = variables["resources"].as<Files::EscapePath>().mPath);
+ mDocumentManager.setResourceDir (mResources = variables["resources"].as<Files::MaybeQuotedPath>());
if (variables["script-blacklist-use"].as<bool>())
mDocumentManager.setBlacklistedScripts (
- variables["script-blacklist"].as<Files::EscapeStringVector>().toStdStringVector());
+ variables["script-blacklist"].as<std::vector<std::string>>());
mFsStrict = variables["fs-strict"].as<bool>();
Files::PathContainer dataDirs, dataLocal;
if (!variables["data"].empty()) {
- dataDirs = Files::PathContainer(Files::EscapePath::toPathContainer(variables["data"].as<Files::EscapePathContainer>()));
+ dataDirs = asPathContainer(variables["data"].as<Files::MaybeQuotedPathContainer>());
}
- Files::PathContainer::value_type local(variables["data-local"].as<Files::EscapePath>().mPath);
+ Files::PathContainer::value_type local(variables["data-local"].as<Files::MaybeQuotedPathContainer::value_type>());
if (!local.empty())
dataLocal.push_back(local);
@@ -149,13 +149,9 @@ std::pair<Files::PathContainer, std::vector<std::string> > CS::Editor::readConfi
dataDirs.insert (dataDirs.end(), dataLocal.begin(), dataLocal.end());
//iterate the data directories and add them to the file dialog for loading
- for (Files::PathContainer::const_reverse_iterator iter = dataDirs.rbegin(); iter != dataDirs.rend(); ++iter)
- {
- QString path = QString::fromUtf8 (iter->string().c_str());
- mFileDialog.addFiles(path);
- }
+ mFileDialog.addFiles(dataDirs);
- return std::make_pair (dataDirs, variables["fallback-archive"].as<Files::EscapeStringVector>().toStdStringVector());
+ return std::make_pair (dataDirs, variables["fallback-archive"].as<std::vector<std::string>>());
}
void CS::Editor::createGame()
diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp
index d363b4849d..6f185d60fa 100644
--- a/apps/opencs/model/filter/parser.cpp
+++ b/apps/opencs/model/filter/parser.cpp
@@ -17,6 +17,19 @@
#include "textnode.hpp"
#include "valuenode.hpp"
+namespace
+{
+ bool isAlpha(char c)
+ {
+ return std::isalpha(static_cast<unsigned char>(c));
+ }
+
+ bool isDigit(char c)
+ {
+ return std::isdigit(static_cast<unsigned char>(c));
+ }
+}
+
namespace CSMFilter
{
struct Token
@@ -103,7 +116,7 @@ CSMFilter::Token CSMFilter::Parser::getStringToken()
{
char c = mInput[mIndex];
- if (std::isalpha (c) || c==':' || c=='_' || (!string.empty() && std::isdigit (c)) || c=='"' ||
+ if (isAlpha(c) || c==':' || c=='_' || (!string.empty() && isDigit(c)) || c=='"' ||
(!string.empty() && string[0]=='"'))
string += c;
else
@@ -150,7 +163,7 @@ CSMFilter::Token CSMFilter::Parser::getNumberToken()
{
char c = mInput[mIndex];
- if (std::isdigit (c))
+ if (isDigit(c))
{
string += c;
hasDigit = true;
@@ -225,10 +238,10 @@ CSMFilter::Token CSMFilter::Parser::getNextToken()
case '!': ++mIndex; return Token (Token::Type_OneShot);
}
- if (c=='"' || c=='_' || std::isalpha (c) || c==':')
+ if (c=='"' || c=='_' || isAlpha(c) || c==':')
return getStringToken();
- if (c=='-' || c=='.' || std::isdigit (c))
+ if (c=='-' || c=='.' || isDigit(c))
return getNumberToken();
error();
diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp
index 58a0f296e2..06dc1f4a2e 100644
--- a/apps/opencs/model/prefs/state.cpp
+++ b/apps/opencs/model/prefs/state.cpp
@@ -223,6 +223,7 @@ void CSMPrefs::State::declare()
declareColour ("scene-night-gradient-colour", "Scene Night Gradient Colour", QColor (47, 51, 51, 255)).
setTooltip("Sets the gradient color to use in conjunction with the night background color. Ignored if "
"the gradient option is disabled.");
+ declareBool("scene-day-night-switch-nodes", "Use Day/Night Switch Nodes", true);
declareCategory ("Tooltips");
declareBool ("scene", "Show Tooltips in 3D scenes", true);
diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp
index 6eefdb6e21..8e53b3b270 100644
--- a/apps/opencs/model/world/columns.cpp
+++ b/apps/opencs/model/world/columns.cpp
@@ -255,7 +255,7 @@ namespace CSMWorld
{ ColumnId_AiWanderDist, "Wander Dist" },
{ ColumnId_AiDuration, "Ai Duration" },
{ ColumnId_AiWanderToD, "Wander ToD" },
- { ColumnId_AiWanderRepeat, "Wander Repeat" },
+ { ColumnId_AiWanderRepeat, "Ai Repeat" },
{ ColumnId_AiActivateName, "Activate" },
{ ColumnId_AiTargetId, "Target ID" },
{ ColumnId_AiTargetCell, "Target Cell" },
diff --git a/apps/opencs/model/world/idtable.cpp b/apps/opencs/model/world/idtable.cpp
index 5b4a9b31bc..215f42133d 100644
--- a/apps/opencs/model/world/idtable.cpp
+++ b/apps/opencs/model/world/idtable.cpp
@@ -8,6 +8,7 @@
#include <stdexcept>
#include <components/esm/cellid.hpp>
+#include <components/misc/stringops.hpp>
#include "collectionbase.hpp"
#include "columnbase.hpp"
@@ -354,8 +355,7 @@ CSMWorld::LandTextureIdTable::ImportResults CSMWorld::LandTextureIdTable::import
for (int i = 0; i < idCollection()->getSize(); ++i)
{
auto& record = static_cast<const Record<LandTexture>&>(idCollection()->getRecord(i));
- std::string texture = record.get().mTexture;
- std::transform(texture.begin(), texture.end(), texture.begin(), tolower);
+ std::string texture = Misc::StringUtils::lowerCase(record.get().mTexture);
if (record.isModified())
reverseLookupMap.emplace(texture, idCollection()->getId(i));
}
@@ -376,8 +376,7 @@ CSMWorld::LandTextureIdTable::ImportResults CSMWorld::LandTextureIdTable::import
// Look for a pre-existing record
auto& record = static_cast<const Record<LandTexture>&>(idCollection()->getRecord(oldRow));
- std::string texture = record.get().mTexture;
- std::transform(texture.begin(), texture.end(), texture.begin(), tolower);
+ std::string texture = Misc::StringUtils::lowerCase(record.get().mTexture);
auto searchIt = reverseLookupMap.find(texture);
if (searchIt != reverseLookupMap.end())
{
diff --git a/apps/opencs/model/world/refidadapterimp.hpp b/apps/opencs/model/world/refidadapterimp.hpp
index c35d3c5a7c..153d1bde91 100644
--- a/apps/opencs/model/world/refidadapterimp.hpp
+++ b/apps/opencs/model/world/refidadapterimp.hpp
@@ -1678,7 +1678,7 @@ namespace CSMWorld
newRow.mWander.mTimeOfDay = 0;
for (int i = 0; i < 8; ++i)
newRow.mWander.mIdle[i] = 0;
- newRow.mWander.mShouldRepeat = 0;
+ newRow.mWander.mShouldRepeat = 1;
newRow.mCellName = "";
if (position >= (int)list.size())
@@ -1784,9 +1784,15 @@ namespace CSMWorld
return static_cast<int>(content.mWander.mIdle[subColIndex-4]);
else
return QVariant();
- case 12: // wander repeat
+ case 12: // repeat
if (content.mType == ESM::AI_Wander)
return content.mWander.mShouldRepeat != 0;
+ else if (content.mType == ESM::AI_Travel)
+ return content.mTravel.mShouldRepeat != 0;
+ else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort)
+ return content.mTarget.mShouldRepeat != 0;
+ else if (content.mType == ESM::AI_Activate)
+ return content.mActivate.mShouldRepeat != 0;
else
return QVariant();
case 13: // activate name
@@ -1895,6 +1901,12 @@ namespace CSMWorld
case 12:
if (content.mType == ESM::AI_Wander)
content.mWander.mShouldRepeat = static_cast<unsigned char>(value.toInt());
+ else if (content.mType == ESM::AI_Travel)
+ content.mTravel.mShouldRepeat = static_cast<unsigned char>(value.toInt());
+ else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort)
+ content.mTarget.mShouldRepeat = static_cast<unsigned char>(value.toInt());
+ else if (content.mType == ESM::AI_Activate)
+ content.mActivate.mShouldRepeat = static_cast<unsigned char>(value.toInt());
else
return; // return without saving
diff --git a/apps/opencs/view/doc/filedialog.cpp b/apps/opencs/view/doc/filedialog.cpp
index c3d0d8cc50..946dac047e 100644
--- a/apps/opencs/view/doc/filedialog.cpp
+++ b/apps/opencs/view/doc/filedialog.cpp
@@ -28,9 +28,14 @@ CSVDoc::FileDialog::FileDialog(QWidget *parent) :
mAdjusterWidget = new AdjusterWidget (this);
}
-void CSVDoc::FileDialog::addFiles(const QString &path)
+void CSVDoc::FileDialog::addFiles(const std::vector<boost::filesystem::path>& dataDirs)
{
- mSelector->addFiles(path);
+ for (auto iter = dataDirs.rbegin(); iter != dataDirs.rend(); ++iter)
+ {
+ QString path = QString::fromUtf8(iter->string().c_str());
+ mSelector->addFiles(path);
+ }
+ mSelector->sortFiles();
}
void CSVDoc::FileDialog::setEncoding(const QString &encoding)
diff --git a/apps/opencs/view/doc/filedialog.hpp b/apps/opencs/view/doc/filedialog.hpp
index 6c48fa78b9..6a15b46b7c 100644
--- a/apps/opencs/view/doc/filedialog.hpp
+++ b/apps/opencs/view/doc/filedialog.hpp
@@ -45,7 +45,7 @@ namespace CSVDoc
explicit FileDialog(QWidget *parent = nullptr);
void showDialog (ContentAction action);
- void addFiles (const QString &path);
+ void addFiles(const std::vector<boost::filesystem::path>& dataDirs);
void setEncoding (const QString &encoding);
void clearFiles ();
diff --git a/apps/opencs/view/render/lighting.cpp b/apps/opencs/view/render/lighting.cpp
index 82ad43e6ad..c7bee79289 100644
--- a/apps/opencs/view/render/lighting.cpp
+++ b/apps/opencs/view/render/lighting.cpp
@@ -3,9 +3,12 @@
#include <osg/LightSource>
#include <osg/NodeVisitor>
#include <osg/Switch>
+#include <osg/ValueObject>
#include <components/misc/constants.hpp>
+#include "../../model/prefs/state.hpp"
+
class DayNightSwitchVisitor : public osg::NodeVisitor
{
public:
@@ -16,8 +19,33 @@ public:
void apply(osg::Switch &switchNode) override
{
- if (switchNode.getName() == Constants::NightDayLabel)
- switchNode.setSingleChildOn(mIndex);
+ constexpr int NoIndex = -1;
+
+ int initialIndex = NoIndex;
+ if (!switchNode.getUserValue("initialIndex", initialIndex))
+ {
+ for (size_t i = 0; i < switchNode.getValueList().size(); ++i)
+ {
+ if (switchNode.getValueList()[i])
+ {
+ initialIndex = i;
+ break;
+ }
+ }
+
+ if (initialIndex != NoIndex)
+ switchNode.setUserValue("initialIndex", initialIndex);
+ }
+
+ if (CSMPrefs::get()["Rendering"]["scene-day-night-switch-nodes"].isTrue())
+ {
+ if (switchNode.getName() == Constants::NightDayLabel)
+ switchNode.setSingleChildOn(mIndex);
+ }
+ else if (initialIndex != NoIndex)
+ {
+ switchNode.setSingleChildOn(initialIndex);
+ }
traverse(switchNode);
}
diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp
index 789fad0587..9f4fd29966 100644
--- a/apps/opencs/view/render/object.cpp
+++ b/apps/opencs/view/render/object.cpp
@@ -308,7 +308,7 @@ osg::ref_ptr<osg::Node> CSVRender::Object::makeRotateMarker (int axis)
const float OuterRadius = InnerRadius + MarkerShaftWidth;
const float SegmentDistance = 100.f;
- const size_t SegmentCount = std::min(64, std::max(24, (int)(OuterRadius * 2 * osg::PI / SegmentDistance)));
+ const size_t SegmentCount = std::clamp<int>(OuterRadius * 2 * osg::PI / SegmentDistance, 24, 64);
const size_t VerticesPerSegment = 4;
const size_t IndicesPerSegment = 24;
diff --git a/apps/opencs/view/render/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp
index dbed1ba97c..b0c2140da9 100644
--- a/apps/opencs/view/render/scenewidget.cpp
+++ b/apps/opencs/view/render/scenewidget.cpp
@@ -550,6 +550,11 @@ void SceneWidget::settingChanged (const CSMPrefs::Setting *setting)
{
updateCameraParameters();
}
+ else if (*setting == "Rendering/scene-day-night-switch-nodes")
+ {
+ if (mLighting)
+ setLighting(mLighting);
+ }
}
void RenderWidget::updateCameraParameters(double overrideAspect)
diff --git a/apps/opencs/view/world/previewsubview.cpp b/apps/opencs/view/world/previewsubview.cpp
index f3312bb208..cb231b4782 100644
--- a/apps/opencs/view/world/previewsubview.cpp
+++ b/apps/opencs/view/world/previewsubview.cpp
@@ -25,6 +25,8 @@ CSVWorld::PreviewSubView::PreviewSubView (const CSMWorld::UniversalId& id, CSMDo
else
mScene = new CSVRender::PreviewWidget (document.getData(), id.getId(), true, this);
+ mScene->setExterior(true);
+
CSVWidget::SceneToolbar *toolbar = new CSVWidget::SceneToolbar (48+6, this);
CSVWidget::SceneToolMode *lightingTool = mScene->makeLightingSelector (toolbar);
diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt
index f22204eb8c..b54ecb416c 100644
--- a/apps/openmw/CMakeLists.txt
+++ b/apps/openmw/CMakeLists.txt
@@ -27,7 +27,7 @@ add_openmw_dir (mwrender
add_openmw_dir (mwinput
actions actionmanager bindingsmanager controllermanager controlswitch
- inputmanagerimp mousemanager keyboardmanager sdlmappings sensormanager
+ inputmanagerimp mousemanager keyboardmanager sensormanager
)
add_openmw_dir (mwgui
@@ -74,7 +74,7 @@ add_openmw_dir (mwworld
actionequip timestamp actionalchemy cellstore actionapply actioneat
store esmstore fallback actionrepair actionsoulgem livecellref actiondoor
contentloader esmloader actiontrap cellreflist cellref weather projectilemanager
- cellpreloader datetimemanager
+ cellpreloader datetimemanager groundcoverstore magiceffects
)
add_openmw_dir (mwphysics
@@ -153,9 +153,12 @@ target_link_libraries(openmw
"osg-ffmpeg-videoplayer"
"oics"
components
- ${LUA_LIBRARIES}
)
+if (MSVC AND CMAKE_VERSION VERSION_GREATER_EQUAL 3.16)
+ target_precompile_headers(openmw PRIVATE ${SOL_INCLUDE_DIR}/sol/sol.hpp)
+endif ()
+
if (ANDROID)
target_link_libraries(openmw EGL android log z)
endif (ANDROID)
@@ -176,8 +179,7 @@ endif()
if(APPLE)
set(BUNDLE_RESOURCES_DIR "${APP_BUNDLE_DIR}/Contents/Resources")
- set(OPENMW_MYGUI_FILES_ROOT ${BUNDLE_RESOURCES_DIR})
- set(OPENMW_SHADERS_ROOT ${BUNDLE_RESOURCES_DIR})
+ set(OPENMW_RESOURCES_ROOT ${BUNDLE_RESOURCES_DIR})
add_subdirectory(../../files/ ${CMAKE_CURRENT_BINARY_DIR}/files)
diff --git a/apps/openmw/android_main.cpp b/apps/openmw/android_main.cpp
index cc36388b0b..95365915df 100644
--- a/apps/openmw/android_main.cpp
+++ b/apps/openmw/android_main.cpp
@@ -1,4 +1,6 @@
+#ifndef stderr
int stderr = 0; // Hack: fix linker error
+#endif
#include "SDL_main.h"
#include <SDL_gamecontroller.h>
diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp
index 58dabd2cee..4ad140465b 100644
--- a/apps/openmw/engine.cpp
+++ b/apps/openmw/engine.cpp
@@ -8,6 +8,8 @@
#include <boost/filesystem/fstream.hpp>
+#include <osg/Version>
+
#include <osgViewer/ViewerEventHandlers>
#include <osgDB/ReadFile>
#include <osgDB/WriteFile>
@@ -37,11 +39,11 @@
#include <components/version/version.hpp>
-#include <components/detournavigator/navigator.hpp>
-
#include <components/misc/frameratelimiter.hpp>
#include <components/sceneutil/screencapture.hpp>
+#include <components/sceneutil/depth.hpp>
+#include <components/sceneutil/util.hpp>
#include "mwinput/inputmanagerimp.hpp"
@@ -238,6 +240,20 @@ namespace
{
void operator()(std::string) const {}
};
+
+ class IdentifyOpenGLOperation : public osg::GraphicsOperation
+ {
+ public:
+ IdentifyOpenGLOperation() : GraphicsOperation("IdentifyOpenGLOperation", false)
+ {}
+
+ void operator()(osg::GraphicsContext* graphicsContext) override
+ {
+ Log(Debug::Info) << "OpenGL Vendor: " << glGetString(GL_VENDOR);
+ Log(Debug::Info) << "OpenGL Renderer: " << glGetString(GL_RENDERER);
+ Log(Debug::Info) << "OpenGL Version: " << glGetString(GL_VERSION);
+ }
+ };
}
void OMW::Engine::executeLocalScripts()
@@ -293,6 +309,10 @@ bool OMW::Engine::frame(float frametime)
// Main menu opened? Then scripts are also paused.
bool paused = mEnvironment.getWindowManager()->containsMode(MWGui::GM_MainMenu);
+ // Should be called after input manager update and before any change to the game world.
+ // It applies to the game world queued changes from the previous frame.
+ mLuaManager->synchronizedUpdate();
+
// update game state
{
ScopedProfile<UserStatsType::State> profile(frameStart, frameNumber, *timer, *stats);
@@ -546,6 +566,10 @@ void OMW::Engine::createWindow(Settings::Manager& settings)
if(fullscreen)
flags |= SDL_WINDOW_FULLSCREEN;
+ // Allows for Windows snapping features to properly work in borderless window
+ SDL_SetHint("SDL_BORDERLESS_WINDOWED_STYLE", "1");
+ SDL_SetHint("SDL_BORDERLESS_RESIZABLE_STYLE", "1");
+
if (!windowBorder)
flags |= SDL_WINDOW_BORDERLESS;
@@ -634,8 +658,12 @@ void OMW::Engine::createWindow(Settings::Manager& settings)
camera->setGraphicsContext(graphicsWindow);
camera->setViewport(0, 0, graphicsWindow->getTraits()->width, graphicsWindow->getTraits()->height);
+ osg::ref_ptr<SceneUtil::OperationSequence> realizeOperations = new SceneUtil::OperationSequence(false);
+ mViewer->setRealizeOperation(realizeOperations);
+ realizeOperations->add(new IdentifyOpenGLOperation());
+
if (Debug::shouldDebugOpenGL())
- mViewer->setRealizeOperation(new Debug::EnableGLDebugOperation());
+ realizeOperations->add(new Debug::EnableGLDebugOperation());
mViewer->realize();
@@ -709,7 +737,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings)
mViewer->addEventHandler(mScreenCaptureHandler);
- mLuaManager = new MWLua::LuaManager(mVFS.get());
+ mLuaManager = new MWLua::LuaManager(mVFS.get(), (mResDir / "lua_libs").string());
mEnvironment.setLuaManager(mLuaManager);
// Create input and UI first to set up a bootstrapping environment for
@@ -753,6 +781,28 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings)
osg::ref_ptr<osg::GLExtensions> exts = osg::GLExtensions::Get(0, false);
bool shadersSupported = exts && (exts->glslLanguageVersion >= 1.2f);
+ bool enableReverseZ = false;
+
+ if (Settings::Manager::getBool("reverse z", "Camera"))
+ {
+ if (exts && exts->isClipControlSupported)
+ {
+ enableReverseZ = true;
+ Log(Debug::Info) << "Using reverse-z depth buffer";
+ }
+ else
+ Log(Debug::Warning) << "GL_ARB_clip_control not supported: disabling reverse-z depth buffer";
+ }
+ else
+ Log(Debug::Info) << "Using standard depth buffer";
+
+ SceneUtil::AutoDepth::setReversed(enableReverseZ);
+
+#if OSG_VERSION_LESS_THAN(3, 6, 6)
+ // hack fix for https://github.com/openscenegraph/OpenSceneGraph/issues/1028
+ if (exts)
+ exts->glRenderbufferStorageMultisampleCoverageNV = nullptr;
+#endif
std::string myguiResources = (mResDir / "mygui").string();
osg::ref_ptr<osg::Group> guiRoot = new osg::Group;
@@ -831,6 +881,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings)
}
mLuaManager->init();
+ mLuaManager->loadPermanentStorage(mCfgMgr.getUserConfigPath().string());
}
class OMW::Engine::LuaWorker
@@ -842,10 +893,8 @@ public:
mThread = std::thread([this]{ threadBody(); });
};
- void allowUpdate(double dt)
+ void allowUpdate()
{
- mDt = dt;
- mIsGuiMode = mEngine->mEnvironment.getWindowManager()->isGuiMode();
if (!mThread)
return;
{
@@ -864,7 +913,6 @@ public:
}
else
update();
- mEngine->mLuaManager->applyQueuedChanges();
};
void join()
@@ -888,7 +936,7 @@ private:
const unsigned int frameNumber = viewer->getFrameStamp()->getFrameNumber();
ScopedProfile<UserStatsType::Lua> profile(frameStart, frameNumber, *osg::Timer::instance(), *viewer->getViewerStats());
- mEngine->mLuaManager->update(mIsGuiMode, mDt);
+ mEngine->mLuaManager->update();
}
void threadBody()
@@ -913,8 +961,6 @@ private:
std::condition_variable mCV;
bool mUpdateRequest = false;
bool mJoinRequest = false;
- double mDt = 0;
- bool mIsGuiMode = false;
std::optional<std::thread> mThread;
};
@@ -1027,7 +1073,7 @@ void OMW::Engine::go()
mEnvironment.getWorld()->updateWindowManager();
- luaWorker.allowUpdate(dt); // if there is a separate Lua thread, it starts the update now
+ luaWorker.allowUpdate(); // if there is a separate Lua thread, it starts the update now
mViewer->renderingTraversals();
@@ -1058,8 +1104,7 @@ void OMW::Engine::go()
// Save user settings
settings.saveUser(settingspath);
-
- mViewer->stopThreading();
+ mLuaManager->savePermanentStorage(mCfgMgr.getUserConfigPath().string());
Log(Debug::Info) << "Quitting peacefully.";
}
diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp
index 1cf7abe2f2..d5bed4ba25 100644
--- a/apps/openmw/main.cpp
+++ b/apps/openmw/main.cpp
@@ -1,6 +1,5 @@
#include <components/version/version.hpp>
#include <components/files/configurationmanager.hpp>
-#include <components/files/escape.hpp>
#include <components/fallback/fallback.hpp>
#include <components/fallback/validate.hpp>
#include <components/debug/debugging.hpp>
@@ -57,7 +56,7 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
{
cfgMgr.readConfiguration(variables, desc, true);
- Version::Version v = Version::getOpenmwVersion(variables["resources"].as<Files::EscapePath>().mPath.string());
+ Version::Version v = Version::getOpenmwVersion(variables["resources"].as<Files::MaybeQuotedPath>().string());
getRawStdout() << v.describe() << std::endl;
return false;
}
@@ -66,38 +65,38 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
cfgMgr.readConfiguration(variables, desc);
Files::mergeComposingVariables(variables, composingVariables, desc);
- Version::Version v = Version::getOpenmwVersion(variables["resources"].as<Files::EscapePath>().mPath.string());
+ Version::Version v = Version::getOpenmwVersion(variables["resources"].as<Files::MaybeQuotedPath>().string());
Log(Debug::Info) << v.describe();
engine.setGrabMouse(!variables["no-grab"].as<bool>());
// Font encoding settings
- std::string encoding(variables["encoding"].as<Files::EscapeHashString>().toStdString());
+ std::string encoding(variables["encoding"].as<std::string>());
Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding);
engine.setEncoding(ToUTF8::calculateEncoding(encoding));
// directory settings
engine.enableFSStrict(variables["fs-strict"].as<bool>());
- Files::PathContainer dataDirs(Files::EscapePath::toPathContainer(variables["data"].as<Files::EscapePathContainer>()));
+ Files::PathContainer dataDirs(asPathContainer(variables["data"].as<Files::MaybeQuotedPathContainer>()));
- Files::PathContainer::value_type local(variables["data-local"].as<Files::EscapePath>().mPath);
+ Files::PathContainer::value_type local(variables["data-local"].as<Files::MaybeQuotedPathContainer::value_type>());
if (!local.empty())
dataDirs.push_back(local);
cfgMgr.processPaths(dataDirs);
- engine.setResourceDir(variables["resources"].as<Files::EscapePath>().mPath);
+ engine.setResourceDir(variables["resources"].as<Files::MaybeQuotedPath>());
engine.setDataDirs(dataDirs);
// fallback archives
- StringsVector archives = variables["fallback-archive"].as<Files::EscapeStringVector>().toStdStringVector();
+ StringsVector archives = variables["fallback-archive"].as<StringsVector>();
for (StringsVector::const_iterator it = archives.begin(); it != archives.end(); ++it)
{
engine.addArchive(*it);
}
- StringsVector content = variables["content"].as<Files::EscapeStringVector>().toStdStringVector();
+ StringsVector content = variables["content"].as<StringsVector>();
if (content.empty())
{
Log(Debug::Error) << "No content file given (esm/esp, nor omwgame/omwaddon). Aborting...";
@@ -118,7 +117,7 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
engine.addContentFile(file);
}
- StringsVector groundcover = variables["groundcover"].as<Files::EscapeStringVector>().toStdStringVector();
+ StringsVector groundcover = variables["groundcover"].as<StringsVector>();
for (auto& file : groundcover)
{
engine.addGroundcoverFile(file);
@@ -131,7 +130,7 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
}
// startup-settings
- engine.setCell(variables["start"].as<Files::EscapeHashString>().toStdString());
+ engine.setCell(variables["start"].as<std::string>());
engine.setSkipMenu (variables["skip-menu"].as<bool>(), variables["new-game"].as<bool>());
if (!variables["skip-menu"].as<bool>() && variables["new-game"].as<bool>())
Log(Debug::Warning) << "Warning: new-game used without skip-menu -> ignoring it";
@@ -140,11 +139,11 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
engine.setCompileAll(variables["script-all"].as<bool>());
engine.setCompileAllDialogue(variables["script-all-dialogue"].as<bool>());
engine.setScriptConsoleMode (variables["script-console"].as<bool>());
- engine.setStartupScript (variables["script-run"].as<Files::EscapeHashString>().toStdString());
+ engine.setStartupScript (variables["script-run"].as<std::string>());
engine.setWarningsMode (variables["script-warn"].as<int>());
- engine.setScriptBlacklist (variables["script-blacklist"].as<Files::EscapeStringVector>().toStdStringVector());
+ engine.setScriptBlacklist (variables["script-blacklist"].as<StringsVector>());
engine.setScriptBlacklistUse (variables["script-blacklist-use"].as<bool>());
- engine.setSaveGameFile (variables["load-savegame"].as<Files::EscapePath>().mPath.string());
+ engine.setSaveGameFile (variables["load-savegame"].as<Files::MaybeQuotedPath>().string());
// other settings
Fallback::Map::init(variables["fallback"].as<FallbackMap>().mMap);
diff --git a/apps/openmw/mwbase/inputmanager.hpp b/apps/openmw/mwbase/inputmanager.hpp
index 5ac7c218b5..d475d93cd1 100644
--- a/apps/openmw/mwbase/inputmanager.hpp
+++ b/apps/openmw/mwbase/inputmanager.hpp
@@ -49,8 +49,8 @@ namespace MWBase
virtual void setGamepadGuiCursorEnabled(bool enabled) = 0;
virtual void setAttemptJump(bool jumping) = 0;
- virtual void toggleControlSwitch (const std::string& sw, bool value) = 0;
- virtual bool getControlSwitch (const std::string& sw) = 0;
+ virtual void toggleControlSwitch(std::string_view sw, bool value) = 0;
+ virtual bool getControlSwitch(std::string_view sw) = 0;
virtual std::string getActionDescription (int action) const = 0;
virtual std::string getActionKeyBindingName (int action) const = 0;
@@ -58,8 +58,8 @@ namespace MWBase
virtual bool actionIsActive(int action) const = 0;
virtual float getActionValue(int action) const = 0; // returns value in range [0, 1]
+ virtual bool isControllerButtonPressed(SDL_GameControllerButton button) const = 0;
virtual float getControllerAxisValue(SDL_GameControllerAxis axis) const = 0; // returns value in range [-1, 1]
- virtual uint32_t getMouseButtonsState() const = 0;
virtual int getMouseMoveX() const = 0;
virtual int getMouseMoveY() const = 0;
diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp
index 6bedbb5b4d..484940e3e6 100644
--- a/apps/openmw/mwbase/mechanicsmanager.hpp
+++ b/apps/openmw/mwbase/mechanicsmanager.hpp
@@ -112,6 +112,9 @@ namespace MWBase
/// Makes \a ptr fight \a target. Also shouts a combat taunt.
virtual void startCombat (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) = 0;
+ /// Removes an actor and its allies from combat with the actor's targets.
+ virtual void stopCombat(const MWWorld::Ptr& ptr) = 0;
+
enum OffenseType
{
OT_Theft, // Taking items owned by an NPC or a faction you are not a member of
diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp
index 961a63ac79..0d1ace3f0b 100644
--- a/apps/openmw/mwbase/windowmanager.hpp
+++ b/apps/openmw/mwbase/windowmanager.hpp
@@ -69,6 +69,7 @@ namespace MWGui
class DialogueWindow;
class WindowModal;
class JailScreen;
+ class MessageBox;
enum ShowInDialogueMode {
ShowInDialogueMode_IfPossible,
@@ -145,6 +146,7 @@ namespace MWBase
virtual MWGui::CountDialog* getCountDialog() = 0;
virtual MWGui::ConfirmationDialog* getConfirmationDialog() = 0;
virtual MWGui::TradeWindow* getTradeWindow() = 0;
+ virtual const std::vector<MWGui::MessageBox*> getActiveMessageBoxes() = 0;
/// Make the player use an item, while updating GUI state accordingly
virtual void useItem(const MWWorld::Ptr& item, bool force=false) = 0;
@@ -355,6 +357,7 @@ namespace MWBase
virtual const std::string& getVersionDescription() const = 0;
virtual void onDeleteCustomData(const MWWorld::Ptr& ptr) = 0;
+ virtual void forceLootMode(const MWWorld::Ptr& ptr) = 0;
};
}
diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp
index c1a8d09120..d1747a2e39 100644
--- a/apps/openmw/mwbase/world.hpp
+++ b/apps/openmw/mwbase/world.hpp
@@ -62,6 +62,7 @@ namespace MWPhysics
namespace MWRender
{
class Animation;
+ class Camera;
}
namespace MWMechanics
@@ -433,14 +434,12 @@ namespace MWBase
virtual osg::Matrixf getActorHeadTransform(const MWWorld::ConstPtr& actor) const = 0;
+ virtual MWRender::Camera* getCamera() = 0;
virtual void togglePOV(bool force = false) = 0;
virtual bool isFirstPerson() const = 0;
virtual bool isPreviewModeEnabled() const = 0;
- virtual void togglePreviewMode(bool enable) = 0;
virtual bool toggleVanityMode(bool enable) = 0;
- virtual void allowVanityMode(bool allow) = 0;
virtual bool vanityRotateCamera(float * rot) = 0;
- virtual void adjustCameraDistance(float dist) = 0;
virtual void applyDeferredPreviewRotationToPlayer(float dt) = 0;
virtual void disableDeferredPreviewRotation() = 0;
diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp
index 0b3ffa924f..03b7cfb069 100644
--- a/apps/openmw/mwclass/creature.cpp
+++ b/apps/openmw/mwclass/creature.cpp
@@ -1,7 +1,5 @@
#include "creature.hpp"
-#include <climits> // INT_MIN
-
#include <components/misc/rng.hpp>
#include <components/debug/debuglog.hpp>
#include <components/esm/loadcrea.hpp>
@@ -758,9 +756,7 @@ namespace MWClass
{
if (!ptr.getRefData().getCustomData())
{
- // FIXME: the use of mGoldPool can be replaced with another flag the next time
- // the save file format is changed
- if (creatureState.mCreatureStats.mGoldPool == INT_MIN)
+ if (creatureState.mCreatureStats.mMissingACDT)
ensureCustomData(ptr);
else
{
diff --git a/apps/openmw/mwclass/creaturelevlist.cpp b/apps/openmw/mwclass/creaturelevlist.cpp
index ddb5fd2b38..ee33242126 100644
--- a/apps/openmw/mwclass/creaturelevlist.cpp
+++ b/apps/openmw/mwclass/creaturelevlist.cpp
@@ -64,7 +64,13 @@ namespace MWClass
if (customData.mSpawn)
return;
- MWWorld::Ptr creature = (customData.mSpawnActorId == -1) ? MWWorld::Ptr() : MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId);
+ MWWorld::Ptr creature;
+ if(customData.mSpawnActorId != -1)
+ {
+ creature = MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId);
+ if(creature.isEmpty())
+ creature = ptr.getCell()->getMovedActor(customData.mSpawnActorId);
+ }
if (!creature.isEmpty())
{
const MWMechanics::CreatureStats& creatureStats = creature.getClass().getCreatureStats(creature);
diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp
index 5d50ba558f..c46e3c0534 100644
--- a/apps/openmw/mwclass/npc.cpp
+++ b/apps/openmw/mwclass/npc.cpp
@@ -1,7 +1,6 @@
#include "npc.hpp"
#include <memory>
-#include <climits> // INT_MIN
#include <components/misc/constants.hpp>
#include <components/misc/rng.hpp>
@@ -1302,9 +1301,7 @@ namespace MWClass
{
if (!ptr.getRefData().getCustomData())
{
- // FIXME: the use of mGoldPool can be replaced with another flag the next time
- // the save file format is changed
- if (npcState.mCreatureStats.mGoldPool == INT_MIN)
+ if (npcState.mCreatureStats.mMissingACDT)
ensureCustomData(ptr);
else
// Create a CustomData, but don't fill it from ESM records (not needed)
diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp
index 8aa6acd8ed..9800f1b39c 100644
--- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp
+++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp
@@ -76,9 +76,10 @@ namespace MWDialogue
mKnownTopics.insert( Misc::StringUtils::lowerCase(topic) );
}
- void DialogueManager::parseText (const std::string& text)
+ std::vector<std::string> DialogueManager::parseTopicIdsFromText (const std::string& text)
{
- updateActorKnownTopics();
+ std::vector<std::string> topicIdList;
+
std::vector<HyperTextParser::Token> hypertext = HyperTextParser::parseHyperText(text);
for (std::vector<HyperTextParser::Token>::iterator tok = hypertext.begin(); tok != hypertext.end(); ++tok)
@@ -95,6 +96,18 @@ namespace MWDialogue
topicId = mTranslationDataStorage.topicStandardForm(topicId);
}
+ topicIdList.push_back(topicId);
+ }
+
+ return topicIdList;
+ }
+
+ void DialogueManager::addTopicsFromText (const std::string& text)
+ {
+ updateActorKnownTopics();
+
+ for (const auto& topicId : parseTopicIdsFromText(text))
+ {
if (mActorKnownTopics.count( topicId ))
mKnownTopics.insert( topicId );
}
@@ -136,7 +149,6 @@ namespace MWDialogue
mTalkedTo = creatureStats.hasTalkedToPlayer();
mActorKnownTopics.clear();
- mActorKnownTopicsFlag.clear();
//greeting
const MWWorld::Store<ESM::Dialogue> &dialogs =
@@ -163,7 +175,7 @@ namespace MWDialogue
executeScript (info->mResultScript, mActor);
mLastTopic = it->mId;
- parseText (info->mResponse);
+ addTopicsFromText (info->mResponse);
return true;
}
@@ -277,7 +289,10 @@ namespace MWDialogue
const ESM::Dialogue& dialogue = *dialogues.find (topic);
- const ESM::DialInfo* info = filter.search(dialogue, true);
+ const ESM::DialInfo* info =
+ mChoice == -1 && mActorKnownTopics.count(topic) ?
+ mActorKnownTopics[topic].mInfo : filter.search(dialogue, true);
+
if (info)
{
std::string title;
@@ -320,7 +335,7 @@ namespace MWDialogue
executeScript (info->mResultScript, mActor);
- parseText (info->mResponse);
+ addTopicsFromText (info->mResponse);
}
}
@@ -339,7 +354,6 @@ namespace MWDialogue
updateGlobals();
mActorKnownTopics.clear();
- mActorKnownTopicsFlag.clear();
const auto& dialogs = MWBase::Environment::get().getWorld()->getStore().get<ESM::Dialogue>();
@@ -354,21 +368,41 @@ namespace MWDialogue
if (answer != nullptr)
{
- int flag = 0;
+ int topicFlags = 0;
if(!inJournal(topicId, answer->mId))
{
// Does this dialogue contains some actor-specific answer?
if (Misc::StringUtils::ciEqual(answer->mActor, mActor.getCellRef().getRefId()))
- flag |= MWBase::DialogueManager::TopicType::Specific;
+ topicFlags |= MWBase::DialogueManager::TopicType::Specific;
}
else
- flag |= MWBase::DialogueManager::TopicType::Exhausted;
- mActorKnownTopics.insert (dialog.mId);
- mActorKnownTopicsFlag[dialog.mId] = flag;
+ topicFlags |= MWBase::DialogueManager::TopicType::Exhausted;
+ mActorKnownTopics.insert (std::make_pair(dialog.mId, ActorKnownTopicInfo {topicFlags, answer}));
}
}
}
+
+ // If response to a topic leads to a new topic, the original topic is not exhausted.
+
+ for (auto& [dialogId, topicInfo] : mActorKnownTopics)
+ {
+ // If the topic is not marked as exhausted, we don't need to do anything about it.
+ // If the topic will not be shown to the player, the flag actually does not matter.
+
+ if (!(topicInfo.mFlags & MWBase::DialogueManager::TopicType::Exhausted) ||
+ !mKnownTopics.count(dialogId))
+ continue;
+
+ for (const auto& topicId : parseTopicIdsFromText(topicInfo.mInfo->mResponse))
+ {
+ if (mActorKnownTopics.count( topicId ) && !mKnownTopics.count( topicId ))
+ {
+ topicInfo.mFlags &= ~MWBase::DialogueManager::TopicType::Exhausted;
+ break;
+ }
+ }
+ }
}
std::list<std::string> DialogueManager::getAvailableTopics()
@@ -377,7 +411,7 @@ namespace MWDialogue
std::list<std::string> keywordList;
- for (const std::string& topic : mActorKnownTopics)
+ for (const auto& [topic, topicInfo] : mActorKnownTopics)
{
//does the player know the topic?
if (mKnownTopics.count(topic))
@@ -391,7 +425,7 @@ namespace MWDialogue
int DialogueManager::getTopicFlag(const std::string& topicId)
{
- return mActorKnownTopicsFlag[topicId];
+ return mActorKnownTopics[topicId].mFlags;
}
void DialogueManager::keywordSelected (const std::string& keyword, ResponseCallback* callback)
@@ -421,7 +455,7 @@ namespace MWDialogue
// Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with intimidate)
npcStats.setBaseDisposition(0);
int zero = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false);
- int disposition = std::min(100 - zero, std::max(mOriginalDisposition + mPermanentDispositionChange, -zero));
+ int disposition = std::clamp(mOriginalDisposition + mPermanentDispositionChange, -zero, 100 - zero);
npcStats.setBaseDisposition(disposition);
}
@@ -444,7 +478,7 @@ namespace MWDialogue
if (const ESM::DialInfo *info = filter.search (*dialogue, true))
{
std::string text = info->mResponse;
- parseText (text);
+ addTopicsFromText (text);
mChoice = -1;
mIsInChoice = false;
@@ -579,7 +613,7 @@ namespace MWDialogue
{
const ESM::DialInfo* info = infos[0];
- parseText (info->mResponse);
+ addTopicsFromText (info->mResponse);
const MWWorld::Store<ESM::GameSetting>& gmsts =
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp
index ab2625ff5a..57eb74d0a6 100644
--- a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp
+++ b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp
@@ -10,6 +10,7 @@
#include <components/compiler/streamerrorhandler.hpp>
#include <components/translation/translation.hpp>
#include <components/misc/stringops.hpp>
+#include <components/esm/loadinfo.hpp>
#include "../mwworld/ptr.hpp"
@@ -24,14 +25,19 @@ namespace MWDialogue
{
class DialogueManager : public MWBase::DialogueManager
{
+ struct ActorKnownTopicInfo
+ {
+ int mFlags;
+ const ESM::DialInfo* mInfo;
+ };
+
std::set<std::string, Misc::StringUtils::CiComp> mKnownTopics;// Those are the topics the player knows.
// Modified faction reactions. <Faction1, <Faction2, Difference> >
typedef std::map<std::string, std::map<std::string, int> > ModFactionReactionMap;
ModFactionReactionMap mChangedFactionReaction;
- std::set<std::string, Misc::StringUtils::CiComp> mActorKnownTopics;
- std::unordered_map<std::string, int> mActorKnownTopicsFlag;
+ std::map<std::string, ActorKnownTopicInfo, Misc::StringUtils::CiComp> mActorKnownTopics;
Translation::Storage& mTranslationDataStorage;
MWScript::CompilerContext mCompilerContext;
@@ -51,7 +57,8 @@ namespace MWDialogue
int mCurrentDisposition;
int mPermanentDispositionChange;
- void parseText (const std::string& text);
+ std::vector<std::string> parseTopicIdsFromText (const std::string& text);
+ void addTopicsFromText (const std::string& text);
void updateActorKnownTopics();
void updateGlobals();
diff --git a/apps/openmw/mwdialogue/hypertextparser.cpp b/apps/openmw/mwdialogue/hypertextparser.cpp
index caafa5f324..89a42bf2ea 100644
--- a/apps/openmw/mwdialogue/hypertextparser.cpp
+++ b/apps/openmw/mwdialogue/hypertextparser.cpp
@@ -47,19 +47,8 @@ namespace MWDialogue
void tokenizeKeywords(const std::string & text, std::vector<Token> & tokens)
{
- const MWWorld::Store<ESM::Dialogue> & dialogs =
- MWBase::Environment::get().getWorld()->getStore().get<ESM::Dialogue>();
-
- std::vector<std::string> keywordList;
- keywordList.reserve(dialogs.getSize());
- for (const auto& it : dialogs)
- keywordList.push_back(Misc::StringUtils::lowerCase(it.mId));
- sort(keywordList.begin(), keywordList.end());
-
- KeywordSearch<std::string, int /*unused*/> keywordSearch;
-
- for (const auto& it : keywordList)
- keywordSearch.seed(it, 0 /*unused*/);
+ const auto& keywordSearch =
+ MWBase::Environment::get().getWorld()->getStore().get<ESM::Dialogue>().getDialogIdKeywordSearch();
std::vector<KeywordSearch<std::string, int /*unused*/>::Match> matches;
keywordSearch.highlightKeywords(text.begin(), text.end(), matches);
diff --git a/apps/openmw/mwdialogue/keywordsearch.hpp b/apps/openmw/mwdialogue/keywordsearch.hpp
index 39599457ef..3f932084fe 100644
--- a/apps/openmw/mwdialogue/keywordsearch.hpp
+++ b/apps/openmw/mwdialogue/keywordsearch.hpp
@@ -74,13 +74,13 @@ public:
return left.mBeg < right.mBeg;
}
- void highlightKeywords (Point beg, Point end, std::vector<Match>& out)
+ void highlightKeywords (Point beg, Point end, std::vector<Match>& out) const
{
std::vector<Match> matches;
for (Point i = beg; i != end; ++i)
{
// check first character
- typename Entry::childen_t::iterator candidate = mRoot.mChildren.find (Misc::StringUtils::toLower (*i));
+ typename Entry::childen_t::const_iterator candidate = mRoot.mChildren.find (Misc::StringUtils::toLower (*i));
// no match, on to next character
if (candidate == mRoot.mChildren.end ())
@@ -91,11 +91,11 @@ public:
// some keywords might be longer variations of other keywords, so we definitely need a list of candidates
// the first element in the pair is length of the match, i.e. depth from the first character on
- std::vector< typename std::pair<int, typename Entry::childen_t::iterator> > candidates;
+ std::vector< typename std::pair<int, typename Entry::childen_t::const_iterator> > candidates;
while ((j + 1) != end)
{
- typename Entry::childen_t::iterator next = candidate->second.mChildren.find (Misc::StringUtils::toLower (*++j));
+ typename Entry::childen_t::const_iterator next = candidate->second.mChildren.find (Misc::StringUtils::toLower (*++j));
if (next == candidate->second.mChildren.end ())
{
@@ -116,7 +116,7 @@ public:
// shorter candidates will be added to the vector first. however, we want to check against longer candidates first
std::reverse(candidates.begin(), candidates.end());
- for (typename std::vector< std::pair<int, typename Entry::childen_t::iterator> >::iterator it = candidates.begin();
+ for (typename std::vector< std::pair<int, typename Entry::childen_t::const_iterator> >::iterator it = candidates.begin();
it != candidates.end(); ++it)
{
candidate = it->second;
diff --git a/apps/openmw/mwgui/bookpage.cpp b/apps/openmw/mwgui/bookpage.cpp
index 874d1d866b..acaee7de28 100644
--- a/apps/openmw/mwgui/bookpage.cpp
+++ b/apps/openmw/mwgui/bookpage.cpp
@@ -8,7 +8,7 @@
#include "MyGUI_FactoryManager.h"
#include <components/misc/utf8stream.hpp>
-#include <components/sceneutil/util.hpp>
+#include <components/sceneutil/depth.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp"
@@ -907,12 +907,6 @@ protected:
return {};
MyGUI::IntPoint pos (left, top);
-#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3)
- // work around inconsistency in MyGUI where the mouse press coordinates aren't
- // transformed by the current Layer (even though mouse *move* events are).
- if(!move)
- pos = mNode->getLayer()->getPosition(left, top);
-#endif
pos.left -= mCroppedParent->getAbsoluteLeft ();
pos.top -= mCroppedParent->getAbsoluteTop ();
pos.top += mViewTop;
@@ -1221,7 +1215,7 @@ public:
RenderXform renderXform (mCroppedParent, textFormat.mRenderItem->getRenderTarget()->getInfo());
- float z = SceneUtil::getReverseZ() ? 1.f : -1.f;
+ float z = SceneUtil::AutoDepth::isReversed() ? 1.f : -1.f;
GlyphStream glyphStream(textFormat.mFont, static_cast<float>(mCoord.left), static_cast<float>(mCoord.top - mViewTop),
z /*mNode->getNodeDepth()*/, vertices, renderXform);
diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp
index de771051ef..fdfd192cc1 100644
--- a/apps/openmw/mwgui/container.cpp
+++ b/apps/openmw/mwgui/container.cpp
@@ -38,6 +38,7 @@ namespace MWGui
, mSortModel(nullptr)
, mModel(nullptr)
, mSelectedItem(-1)
+ , mTreatNextOpenAsLoot(false)
{
getWidget(mDisposeCorpseButton, "DisposeCorpseButton");
getWidget(mTakeButton, "TakeButton");
@@ -121,13 +122,15 @@ namespace MWGui
void ContainerWindow::setPtr(const MWWorld::Ptr& container)
{
+ bool lootAnyway = mTreatNextOpenAsLoot;
+ mTreatNextOpenAsLoot = false;
mPtr = container;
bool loot = mPtr.getClass().isActor() && mPtr.getClass().getCreatureStats(mPtr).isDead();
if (mPtr.getClass().hasInventoryStore(mPtr))
{
- if (mPtr.getClass().isNpc() && !loot)
+ if (mPtr.getClass().isNpc() && !loot && !lootAnyway)
{
// we are stealing stuff
mModel = new PickpocketItemModel(mPtr, new InventoryItemModel(container),
diff --git a/apps/openmw/mwgui/container.hpp b/apps/openmw/mwgui/container.hpp
index 2a0dee44e2..66a20e7ef5 100644
--- a/apps/openmw/mwgui/container.hpp
+++ b/apps/openmw/mwgui/container.hpp
@@ -37,6 +37,7 @@ namespace MWGui
void onDeleteCustomData(const MWWorld::Ptr& ptr) override;
+ void treatNextOpenAsLoot() { mTreatNextOpenAsLoot = true; };
private:
DragAndDrop* mDragAndDrop;
@@ -44,7 +45,7 @@ namespace MWGui
SortFilterItemModel* mSortModel;
ItemModel* mModel;
int mSelectedItem;
-
+ bool mTreatNextOpenAsLoot;
MyGUI::Button* mDisposeCorpseButton;
MyGUI::Button* mTakeButton;
MyGUI::Button* mCloseButton;
diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp
index 1da77f5d06..86cad0fa7a 100644
--- a/apps/openmw/mwgui/dialogue.cpp
+++ b/apps/openmw/mwgui/dialogue.cpp
@@ -347,8 +347,7 @@ namespace MWGui
{
if (!mScrollBar->getVisible())
return;
- mScrollBar->setScrollPosition(std::min(static_cast<int>(mScrollBar->getScrollRange()-1),
- std::max(0, static_cast<int>(mScrollBar->getScrollPosition() - _rel*0.3))));
+ mScrollBar->setScrollPosition(std::clamp<int>(mScrollBar->getScrollPosition() - _rel*0.3, 0, mScrollBar->getScrollRange() - 1));
onScrollbarMoved(mScrollBar, mScrollBar->getScrollPosition());
}
diff --git a/apps/openmw/mwgui/enchantingdialog.cpp b/apps/openmw/mwgui/enchantingdialog.cpp
index d0d2118c6e..ee0067f082 100644
--- a/apps/openmw/mwgui/enchantingdialog.cpp
+++ b/apps/openmw/mwgui/enchantingdialog.cpp
@@ -109,7 +109,7 @@ namespace MWGui
{
mEnchantmentPoints->setCaption(std::to_string(static_cast<int>(mEnchanting.getEnchantPoints(false))) + " / " + std::to_string(mEnchanting.getMaxEnchantValue()));
mCharge->setCaption(std::to_string(mEnchanting.getGemCharge()));
- mSuccessChance->setCaption(std::to_string(std::max(0, std::min(100, mEnchanting.getEnchantChance()))));
+ mSuccessChance->setCaption(std::to_string(std::clamp(mEnchanting.getEnchantChance(), 0, 100)));
mCastCost->setCaption(std::to_string(mEnchanting.getEffectiveCastCost()));
mPrice->setCaption(std::to_string(mEnchanting.getEnchantPrice()));
diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp
index 5a7b5a9590..f6136791e9 100644
--- a/apps/openmw/mwgui/hud.cpp
+++ b/apps/openmw/mwgui/hud.cpp
@@ -81,7 +81,7 @@ namespace MWGui
, mMinimap(nullptr)
, mCrosshair(nullptr)
, mCellNameBox(nullptr)
- , mDrowningFrame(nullptr)
+ , mDrowningBar(nullptr)
, mDrowningFlash(nullptr)
, mHealthManaStaminaBaseLeft(0)
, mWeapBoxBaseLeft(0)
@@ -119,6 +119,7 @@ namespace MWGui
fatigueFrame->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onHMSClicked);
//Drowning bar
+ getWidget(mDrowningBar, "DrowningBar");
getWidget(mDrowningFrame, "DrowningFrame");
getWidget(mDrowning, "Drowning");
getWidget(mDrowningFlash, "Flash");
@@ -224,7 +225,7 @@ namespace MWGui
void HUD::setDrowningBarVisible(bool visible)
{
- mDrowningFrame->setVisible(visible);
+ mDrowningBar->setVisible(visible);
}
void HUD::onWorldClicked(MyGUI::Widget* _sender)
@@ -368,9 +369,6 @@ namespace MWGui
mWeaponSpellBox->setPosition(mWeaponSpellBox->getPosition() + MyGUI::IntPoint(0,20));
}
- if (mIsDrowning)
- mDrowningFlashTheta += dt * osg::PI*2;
-
mSpellIcons->updateWidgets(mEffectBox, true);
if (mEnemyActorId != -1 && mEnemyHealth->getVisible())
@@ -378,8 +376,13 @@ namespace MWGui
updateEnemyHealthBar();
}
+ if (mDrowningBar->getVisible())
+ mDrowningBar->setPosition(mMainWidget->getWidth()/2 - mDrowningFrame->getWidth()/2, mMainWidget->getTop());
+
if (mIsDrowning)
{
+ mDrowningFlashTheta += dt * osg::PI*2;
+
float intensity = (cos(mDrowningFlashTheta) + 2.0f) / 3.0f;
mDrowningFlash->setAlpha(intensity);
@@ -610,7 +613,7 @@ namespace MWGui
static const float fNPCHealthBarFade = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fNPCHealthBarFade")->mValue.getFloat();
if (fNPCHealthBarFade > 0.f)
- mEnemyHealth->setAlpha(std::max(0.f, std::min(1.f, mEnemyHealthTimer/fNPCHealthBarFade)));
+ mEnemyHealth->setAlpha(std::clamp(mEnemyHealthTimer / fNPCHealthBarFade, 0.f, 1.f));
}
diff --git a/apps/openmw/mwgui/hud.hpp b/apps/openmw/mwgui/hud.hpp
index 8a89320d8c..ef591bec97 100644
--- a/apps/openmw/mwgui/hud.hpp
+++ b/apps/openmw/mwgui/hud.hpp
@@ -73,7 +73,7 @@ namespace MWGui
MyGUI::ImageBox* mCrosshair;
MyGUI::TextBox* mCellNameBox;
MyGUI::TextBox* mWeaponSpellBox;
- MyGUI::Widget *mDrowningFrame, *mDrowningFlash;
+ MyGUI::Widget *mDrowningBar, *mDrowningFrame, *mDrowningFlash;
// bottom left elements
int mHealthManaStaminaBaseLeft, mWeapBoxBaseLeft, mSpellBoxBaseLeft, mSneakBoxBaseLeft;
diff --git a/apps/openmw/mwgui/keyboardnavigation.cpp b/apps/openmw/mwgui/keyboardnavigation.cpp
index b718b712c0..249e5d0240 100644
--- a/apps/openmw/mwgui/keyboardnavigation.cpp
+++ b/apps/openmw/mwgui/keyboardnavigation.cpp
@@ -79,7 +79,7 @@ void KeyboardNavigation::restoreFocus(int mode)
if (found != mKeyFocus.end())
{
MyGUI::Widget* w = found->second;
- if (w && w->getVisible() && w->getEnabled())
+ if (w && w->getVisible() && w->getEnabled() && w->getInheritedVisible() && w->getInheritedEnabled())
MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(found->second);
}
}
@@ -93,19 +93,6 @@ void KeyboardNavigation::_unlinkWidget(MyGUI::Widget *widget)
mCurrentFocus = nullptr;
}
-#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3)
-void styleFocusedButton(MyGUI::Widget* w)
-{
- if (w)
- {
- if (MyGUI::Button* b = w->castType<MyGUI::Button>(false))
- {
- b->_setWidgetState("highlighted");
- }
- }
-}
-#endif
-
bool isRootParent(MyGUI::Widget* widget, MyGUI::Widget* root)
{
while (widget && widget->getParent())
@@ -128,9 +115,6 @@ void KeyboardNavigation::onFrame()
if (focus == mCurrentFocus)
{
-#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3)
- styleFocusedButton(mCurrentFocus);
-#endif
return;
}
@@ -143,19 +127,8 @@ void KeyboardNavigation::onFrame()
if (focus != mCurrentFocus)
{
-#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3)
- if (mCurrentFocus)
- {
- if (MyGUI::Button* b = mCurrentFocus->castType<MyGUI::Button>(false))
- b->_setWidgetState("normal");
- }
-#endif
mCurrentFocus = focus;
}
-
-#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3)
- styleFocusedButton(mCurrentFocus);
-#endif
}
void KeyboardNavigation::setDefaultFocus(MyGUI::Widget *window, MyGUI::Widget *defaultFocus)
@@ -273,7 +246,7 @@ bool KeyboardNavigation::switchFocus(int direction, bool wrap)
if (wrap)
index = (index + keyFocusList.size())%keyFocusList.size();
else
- index = std::min(std::max(0, index), static_cast<int>(keyFocusList.size())-1);
+ index = std::clamp<int>(index, 0, keyFocusList.size() - 1);
MyGUI::Widget* next = keyFocusList[index];
int vertdiff = next->getTop() - focus->getTop();
diff --git a/apps/openmw/mwgui/mapwindow.hpp b/apps/openmw/mwgui/mapwindow.hpp
index d3cd626475..61f48d5279 100644
--- a/apps/openmw/mwgui/mapwindow.hpp
+++ b/apps/openmw/mwgui/mapwindow.hpp
@@ -4,6 +4,8 @@
#include <stdint.h>
#include <memory>
+#include <osg/Vec2f>
+
#include "windowpinnablebase.hpp"
#include <components/esm/cellid.hpp>
diff --git a/apps/openmw/mwgui/messagebox.cpp b/apps/openmw/mwgui/messagebox.cpp
index ed6633c983..890406cd12 100644
--- a/apps/openmw/mwgui/messagebox.cpp
+++ b/apps/openmw/mwgui/messagebox.cpp
@@ -145,7 +145,6 @@ namespace MWGui
return mInterMessageBoxe != nullptr;
}
-
bool MessageBoxManager::removeMessageBox (MessageBox *msgbox)
{
std::vector<MessageBox*>::iterator it;
@@ -161,6 +160,11 @@ namespace MWGui
return false;
}
+ const std::vector<MessageBox*> MessageBoxManager::getActiveMessageBoxes()
+ {
+ return mMessageBoxes;
+ }
+
int MessageBoxManager::readPressedButton (bool reset)
{
int pressed = mLastButtonPressed;
diff --git a/apps/openmw/mwgui/messagebox.hpp b/apps/openmw/mwgui/messagebox.hpp
index 26d26bac56..d46d31d938 100644
--- a/apps/openmw/mwgui/messagebox.hpp
+++ b/apps/openmw/mwgui/messagebox.hpp
@@ -49,6 +49,8 @@ namespace MWGui
void setVisible(bool value);
+ const std::vector<MessageBox*> getActiveMessageBoxes();
+
private:
std::vector<MessageBox*> mMessageBoxes;
InteractiveMessageBox* mInterMessageBoxe;
@@ -63,6 +65,7 @@ namespace MWGui
public:
MessageBox (MessageBoxManager& parMessageBoxManager, const std::string& message);
void setMessage (const std::string& message);
+ const std::string& getMessage() { return mMessage; };
int getHeight ();
void update (int height);
void setVisible(bool value);
@@ -72,7 +75,7 @@ namespace MWGui
protected:
MessageBoxManager& mMessageBoxManager;
- const std::string& mMessage;
+ std::string mMessage;
MyGUI::EditBox* mMessageWidget;
int mBottomPadding;
int mNextBoxPadding;
diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp
index ad61f63b90..e55b9b4878 100644
--- a/apps/openmw/mwgui/quickkeysmenu.cpp
+++ b/apps/openmw/mwgui/quickkeysmenu.cpp
@@ -79,44 +79,48 @@ namespace MWGui
delete mMagicSelectionDialog;
}
- void QuickKeysMenu::onOpen()
+ inline void QuickKeysMenu::validate(int index)
{
- WindowBase::onOpen();
-
MWWorld::Ptr player = MWMechanics::getPlayer();
MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player);
-
- // Check if quick keys are still valid
- for (int i=0; i<10; ++i)
+ switch (mKey[index].type)
{
- switch (mKey[i].type)
+ case Type_Unassigned:
+ case Type_HandToHand:
+ case Type_Magic:
+ break;
+ case Type_Item:
+ case Type_MagicItem:
{
- case Type_Unassigned:
- case Type_HandToHand:
- case Type_Magic:
- break;
- case Type_Item:
- case Type_MagicItem:
- {
- MWWorld::Ptr item = *mKey[i].button->getUserData<MWWorld::Ptr>();
- // Make sure the item is available and is not broken
- if (!item || item.getRefData().getCount() < 1 ||
- (item.getClass().hasItemHealth(item) &&
+ MWWorld::Ptr item = *mKey[index].button->getUserData<MWWorld::Ptr>();
+ // Make sure the item is available and is not broken
+ if (!item || item.getRefData().getCount() < 1 ||
+ (item.getClass().hasItemHealth(item) &&
item.getClass().getItemHealth(item) <= 0))
- {
- // Try searching for a compatible replacement
- item = store.findReplacement(mKey[i].id);
+ {
+ // Try searching for a compatible replacement
+ item = store.findReplacement(mKey[index].id);
- if (item)
- mKey[i].button->setUserData(MWWorld::Ptr(item));
+ if (item)
+ mKey[index].button->setUserData(MWWorld::Ptr(item));
- break;
- }
+ break;
}
}
}
}
+ void QuickKeysMenu::onOpen()
+ {
+ WindowBase::onOpen();
+
+ // Quick key index
+ for (int index = 0; index < 10; ++index)
+ {
+ validate(index);
+ }
+ }
+
void QuickKeysMenu::unassign(keyData* key)
{
key->button->clearUserStrings();
@@ -329,11 +333,13 @@ namespace MWGui
assert(index >= 1 && index <= 10);
keyData *key = &mKey[index-1];
-
+
MWWorld::Ptr player = MWMechanics::getPlayer();
MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player);
const MWMechanics::CreatureStats &playerStats = player.getClass().getCreatureStats(player);
+ validate(index-1);
+
// Delay action executing,
// if player is busy for now (casting a spell, attacking someone, etc.)
bool isDelayNeeded = MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player)
diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp
index b2742df796..4761c98ceb 100644
--- a/apps/openmw/mwgui/quickkeysmenu.hpp
+++ b/apps/openmw/mwgui/quickkeysmenu.hpp
@@ -76,7 +76,8 @@ namespace MWGui
void onQuickKeyButtonClicked(MyGUI::Widget* sender);
void onOkButtonClicked(MyGUI::Widget* sender);
-
+ // Check if quick key is still valid
+ inline void validate(int index);
void unassign(keyData* key);
};
diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp
index 3b4afc852f..69d09f7260 100644
--- a/apps/openmw/mwgui/settingswindow.cpp
+++ b/apps/openmw/mwgui/settingswindow.cpp
@@ -171,7 +171,7 @@ namespace MWGui
else
valueStr = MyGUI::utility::toString(int(value));
- value = std::max(min, std::min(value, max));
+ value = std::clamp(value, min, max);
value = (value-min)/(max-min);
scroll->setScrollPosition(static_cast<size_t>(value * (scroll->getScrollRange() - 1)));
@@ -232,6 +232,7 @@ namespace MWGui
getWidget(mControllerSwitch, "ControllerButton");
getWidget(mWaterTextureSize, "WaterTextureSize");
getWidget(mWaterReflectionDetail, "WaterReflectionDetail");
+ getWidget(mWaterRainRippleDetail, "WaterRainRippleDetail");
getWidget(mLightingMethodButton, "LightingMethodButton");
getWidget(mLightsResetButton, "LightsResetButton");
getWidget(mMaxLights, "MaxLights");
@@ -259,6 +260,7 @@ namespace MWGui
mWaterTextureSize->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterTextureSizeChanged);
mWaterReflectionDetail->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterReflectionDetailChanged);
+ mWaterRainRippleDetail->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterRainRippleDetailChanged);
mLightingMethodButton->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onLightingMethodButtonChanged);
mLightsResetButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onLightsResetButtonClicked);
@@ -267,6 +269,8 @@ namespace MWGui
mKeyboardSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onKeyboardSwitchClicked);
mControllerSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onControllerSwitchClicked);
+ computeMinimumWindowSize();
+
center();
mResetControlsButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onResetDefaultBindings);
@@ -305,10 +309,12 @@ namespace MWGui
if (waterTextureSize >= 2048)
mWaterTextureSize->setIndexSelected(2);
- int waterReflectionDetail = Settings::Manager::getInt("reflection detail", "Water");
- waterReflectionDetail = std::min(5, std::max(0, waterReflectionDetail));
+ int waterReflectionDetail = std::clamp(Settings::Manager::getInt("reflection detail", "Water"), 0, 5);
mWaterReflectionDetail->setIndexSelected(waterReflectionDetail);
+ int waterRainRippleDetail = std::clamp(Settings::Manager::getInt("rain ripple detail", "Water"), 0, 2);
+ mWaterRainRippleDetail->setIndexSelected(waterRainRippleDetail);
+
updateMaxLightsComboBox(mMaxLights);
mWindowBorderButton->setEnabled(!Settings::Manager::getBool("fullscreen", "Video"));
@@ -392,11 +398,18 @@ namespace MWGui
void SettingsWindow::onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos)
{
- unsigned int level = std::min((unsigned int)5, (unsigned int)pos);
+ unsigned int level = static_cast<unsigned int>(std::min<size_t>(pos, 5));
Settings::Manager::setInt("reflection detail", "Water", level);
apply();
}
+ void SettingsWindow::onWaterRainRippleDetailChanged(MyGUI::ComboBox* _sender, size_t pos)
+ {
+ unsigned int level = static_cast<unsigned int>(std::min<size_t>(pos, 2));
+ Settings::Manager::setInt("rain ripple detail", "Water", level);
+ apply();
+ }
+
void SettingsWindow::onLightingMethodButtonChanged(MyGUI::ComboBox* _sender, size_t pos)
{
if (pos == MyGUI::ITEM_NONE)
@@ -739,6 +752,32 @@ namespace MWGui
layoutControlsBox();
}
+ void SettingsWindow::computeMinimumWindowSize()
+ {
+ auto* window = mMainWidget->castType<MyGUI::Window>();
+ auto minSize = window->getMinSize();
+
+ // Window should be at minimum wide enough to show all tabs.
+ int tabBarWidth = 0;
+ for (uint32_t i = 0; i < mSettingsTab->getItemCount(); i++)
+ {
+ tabBarWidth += mSettingsTab->getButtonWidthAt(i);
+ }
+
+ // Need to include window margins
+ int margins = mMainWidget->getWidth() - mSettingsTab->getWidth();
+ int minimumWindowWidth = tabBarWidth + margins;
+
+ if (minimumWindowWidth > minSize.width)
+ {
+ minSize.width = minimumWindowWidth;
+ window->setMinSize(minSize);
+
+ // Make a dummy call to setSize so MyGUI can apply any resize resulting from the change in MinSize
+ mMainWidget->setSize(mMainWidget->getSize());
+ }
+ }
+
void SettingsWindow::resetScrollbars()
{
mResolutionList->setScrollPosition(0);
diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp
index 9c28733f9a..d58e94e84a 100644
--- a/apps/openmw/mwgui/settingswindow.hpp
+++ b/apps/openmw/mwgui/settingswindow.hpp
@@ -31,6 +31,7 @@ namespace MWGui
MyGUI::ComboBox* mWaterTextureSize;
MyGUI::ComboBox* mWaterReflectionDetail;
+ MyGUI::ComboBox* mWaterRainRippleDetail;
MyGUI::ComboBox* mMaxLights;
MyGUI::ComboBox* mLightingMethodButton;
@@ -55,6 +56,7 @@ namespace MWGui
void onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos);
void onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos);
+ void onWaterRainRippleDetailChanged(MyGUI::ComboBox* _sender, size_t pos);
void onLightingMethodButtonChanged(MyGUI::ComboBox* _sender, size_t pos);
void onLightsResetButtonClicked(MyGUI::Widget* _sender);
@@ -75,6 +77,8 @@ namespace MWGui
void updateSliderLabel(MyGUI::ScrollBar* scroller, const std::string& value);
void layoutControlsBox();
+
+ void computeMinimumWindowSize();
private:
void resetScrollbars();
diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp
index 8e6f951291..0830af0744 100644
--- a/apps/openmw/mwgui/statswindow.cpp
+++ b/apps/openmw/mwgui/statswindow.cpp
@@ -599,8 +599,7 @@ namespace MWGui
text += "\n#{fontcolourhtml=normal}#{sExpelled}";
else
{
- int rank = factionPair.second;
- rank = std::max(0, std::min(9, rank));
+ const int rank = std::clamp(factionPair.second, 0, 9);
text += std::string("\n#{fontcolourhtml=normal}") + faction->mRanks[rank];
if (rank < 9)
diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp
index 84d8b65592..3a2bd65361 100644
--- a/apps/openmw/mwgui/tooltips.cpp
+++ b/apps/openmw/mwgui/tooltips.cpp
@@ -493,6 +493,7 @@ namespace MWGui
std::vector<MyGUI::Widget*> effectItems;
int flag = info.isPotion ? Widgets::MWEffectList::EF_NoTarget : 0;
flag |= info.isIngredient ? Widgets::MWEffectList::EF_NoMagnitude : 0;
+ flag |= info.isIngredient ? Widgets::MWEffectList::EF_Constant : 0;
effectsWidget->createEffectWidgets(effectItems, effectArea, coord, true, flag);
totalSize.height += coord.top-6;
totalSize.width = std::max(totalSize.width, coord.width);
@@ -649,7 +650,7 @@ namespace MWGui
std::string ToolTips::getSoulString(const MWWorld::CellRef& cellref)
{
- std::string soul = cellref.getSoul();
+ const std::string& soul = cellref.getSoul();
if (soul.empty())
return std::string();
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
@@ -665,7 +666,7 @@ namespace MWGui
{
std::string ret;
ret += getMiscString(cellref.getOwner(), "Owner");
- const std::string factionId = cellref.getFaction();
+ const std::string& factionId = cellref.getFaction();
if (!factionId.empty())
{
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp
index 6eeb2d3654..8e7951667b 100644
--- a/apps/openmw/mwgui/windowmanagerimp.cpp
+++ b/apps/openmw/mwgui/windowmanagerimp.cpp
@@ -53,6 +53,8 @@
#include <components/misc/resourcehelpers.hpp>
#include <components/misc/frameratelimiter.hpp>
+#include <components/lua_ui/widgetlist.hpp>
+
#include "../mwbase/inputmanager.hpp"
#include "../mwbase/statemanager.hpp"
#include "../mwbase/soundmanager.hpp"
@@ -163,6 +165,7 @@ namespace MWGui
, mScreenFader(nullptr)
, mDebugWindow(nullptr)
, mJailScreen(nullptr)
+ , mContainerWindow(nullptr)
, mTranslationDataStorage (translationDataStorage)
, mCharGen(nullptr)
, mInputBlocker(nullptr)
@@ -220,6 +223,7 @@ namespace MWGui
ItemWidget::registerComponents();
SpellView::registerComponents();
Gui::registerAllWidgets();
+ LuaUi::registerAllWidgets();
MyGUI::FactoryManager::getInstance().registerFactory<MWGui::Controllers::ControllerFollowMouse>("Controller");
@@ -357,10 +361,10 @@ namespace MWGui
mGuiModeStates[GM_Dialogue] = GuiModeState(mDialogueWindow);
mTradeWindow->eventTradeDone += MyGUI::newDelegate(mDialogueWindow, &DialogueWindow::onTradeComplete);
- ContainerWindow* containerWindow = new ContainerWindow(mDragAndDrop);
- mWindows.push_back(containerWindow);
- trackWindow(containerWindow, "container");
- mGuiModeStates[GM_Container] = GuiModeState({containerWindow, mInventoryWindow});
+ mContainerWindow = new ContainerWindow(mDragAndDrop);
+ mWindows.push_back(mContainerWindow);
+ trackWindow(mContainerWindow, "container");
+ mGuiModeStates[GM_Container] = GuiModeState({mContainerWindow, mInventoryWindow});
mHud = new HUD(mCustomMarkers, mDragAndDrop, mLocalMapRender);
mWindows.push_back(mHud);
@@ -635,6 +639,7 @@ namespace MWGui
mMap->setVisible(false);
mStatsWindow->setVisible(false);
mSpellWindow->setVisible(false);
+ mHud->setDrowningBarVisible(false);
mInventoryWindow->setVisible(getMode() == GM_Container || getMode() == GM_Barter || getMode() == GM_Companion);
}
@@ -768,6 +773,11 @@ namespace MWGui
mMessageBoxManager->removeStaticMessageBox();
}
+ const std::vector<MWGui::MessageBox*> WindowManager::getActiveMessageBoxes()
+ {
+ return mMessageBoxManager->getActiveMessageBoxes();
+ }
+
int WindowManager::readPressedButton ()
{
return mMessageBoxManager->readPressedButton();
@@ -1095,12 +1105,21 @@ namespace MWGui
void WindowManager::windowResized(int x, int y)
{
- // Note: this is a side effect of resolution change or window resize.
- // There is no need to track these changes.
Settings::Manager::setInt("resolution x", "Video", x);
Settings::Manager::setInt("resolution y", "Video", y);
- Settings::Manager::resetPendingChange("resolution x", "Video");
- Settings::Manager::resetPendingChange("resolution y", "Video");
+
+ // We only want to process changes to window-size related settings.
+ Settings::CategorySettingVector filter = {{"Video", "resolution x"},
+ {"Video", "resolution y"}};
+
+ // If the HUD has not been initialised, the World singleton will not be available.
+ if (mHud)
+ {
+ MWBase::Environment::get().getWorld()->processChangedSettings(
+ Settings::Manager::getPendingChanges(filter));
+ }
+
+ Settings::Manager::resetPendingChanges(filter);
mGuiPlatform->getRenderManagerPtr()->setViewSize(x, y);
@@ -1164,6 +1183,16 @@ namespace MWGui
void WindowManager::pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg)
{
+ pushGuiMode(mode, arg, false);
+ }
+
+ void WindowManager::forceLootMode(const MWWorld::Ptr& ptr)
+ {
+ pushGuiMode(MWGui::GM_Container, ptr, true);
+ }
+
+ void WindowManager::pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg, bool force)
+ {
if (mode==GM_Inventory && mAllowed==GW_None)
return;
@@ -1185,6 +1214,8 @@ namespace MWGui
mGuiModeStates[mode].update(true);
playSound(mGuiModeStates[mode].mOpenSound);
}
+ if(force)
+ mContainerWindow->treatNextOpenAsLoot();
for (WindowBase* window : mGuiModeStates[mode].mWindows)
window->setPtr(arg);
diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp
index 9ec79e0c82..ef19329cdf 100644
--- a/apps/openmw/mwgui/windowmanagerimp.hpp
+++ b/apps/openmw/mwgui/windowmanagerimp.hpp
@@ -187,6 +187,7 @@ namespace MWGui
MWGui::CountDialog* getCountDialog() override;
MWGui::ConfirmationDialog* getConfirmationDialog() override;
MWGui::TradeWindow* getTradeWindow() override;
+ const std::vector<MWGui::MessageBox*> getActiveMessageBoxes() override;
/// Make the player use an item, while updating GUI state accordingly
void useItem(const MWWorld::Ptr& item, bool bypassBeastRestrictions=false) override;
@@ -389,6 +390,7 @@ namespace MWGui
const std::string& getVersionDescription() const override;
void onDeleteCustomData(const MWWorld::Ptr& ptr) override;
+ void forceLootMode(const MWWorld::Ptr& ptr) override;
private:
unsigned int mOldUpdateMask; unsigned int mOldCullMask;
@@ -447,6 +449,7 @@ namespace MWGui
ScreenFader* mScreenFader;
DebugWindow* mDebugWindow;
JailScreen* mJailScreen;
+ ContainerWindow* mContainerWindow;
std::vector<WindowBase*> mWindows;
@@ -573,6 +576,8 @@ namespace MWGui
void enableScene(bool enable);
void handleScheduledMessageBoxes();
+
+ void pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg, bool force);
};
}
diff --git a/apps/openmw/mwinput/actionmanager.cpp b/apps/openmw/mwinput/actionmanager.cpp
index e080437d92..63b0a197a7 100644
--- a/apps/openmw/mwinput/actionmanager.cpp
+++ b/apps/openmw/mwinput/actionmanager.cpp
@@ -22,12 +22,13 @@
#include "../mwmechanics/npcstats.hpp"
#include "../mwmechanics/actorutil.hpp"
+#include "../mwgui/messagebox.hpp"
+
#include "actions.hpp"
#include "bindingsmanager.hpp"
namespace MWInput
{
- const float ZOOM_SCALE = 10.f; /// Used for scrolling camera in and out
ActionManager::ActionManager(BindingsManager* bindingsManager,
osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation,
@@ -40,8 +41,6 @@ namespace MWInput
, mAlwaysRunActive(Settings::Manager::getBool("always run", "Input"))
, mSneaking(false)
, mAttemptJump(false)
- , mOverencumberedMessageDelay(0.f)
- , mPreviewPOVDelay(0.f)
, mTimeIdle(0.f)
{
}
@@ -90,43 +89,26 @@ namespace MWInput
{
player.setUpDown(1);
triedToMove = true;
- mOverencumberedMessageDelay = 0.f;
}
// if player tried to start moving, but can't (due to being overencumbered), display a notification.
if (triedToMove)
{
MWWorld::Ptr playerPtr = MWBase::Environment::get().getWorld ()->getPlayerPtr();
- mOverencumberedMessageDelay -= dt;
if (playerPtr.getClass().getEncumbrance(playerPtr) > playerPtr.getClass().getCapacity(playerPtr))
{
player.setAutoMove (false);
- if (mOverencumberedMessageDelay <= 0)
+ std::vector<MWGui::MessageBox*> msgboxs = MWBase::Environment::get().getWindowManager()->getActiveMessageBoxes();
+ const std::vector<MWGui::MessageBox*>::iterator it = std::find_if(msgboxs.begin(), msgboxs.end(), [](MWGui::MessageBox*& msgbox)
{
- MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage59}");
- mOverencumberedMessageDelay = 1.0;
- }
- }
- }
+ return (msgbox->getMessage() == "#{sNotifyMessage59}");
+ });
- if (MWBase::Environment::get().getInputManager()->getControlSwitch("playerviewswitch"))
- {
- const float switchLimit = 0.25;
- MWBase::World* world = MWBase::Environment::get().getWorld();
- if (mBindingsManager->actionIsActive(A_TogglePOV))
- {
- if (world->isFirstPerson() ? mPreviewPOVDelay > switchLimit : mPreviewPOVDelay == 0)
- world->togglePreviewMode(true);
- mPreviewPOVDelay += dt;
- }
- else
- {
- //disable preview mode
- if (mPreviewPOVDelay > 0)
- world->togglePreviewMode(false);
- if (mPreviewPOVDelay > 0.f && mPreviewPOVDelay <= switchLimit)
- world->togglePOV();
- mPreviewPOVDelay = 0.f;
+ // if an overencumbered messagebox is already present, reset its expiry timer, otherwise create new one.
+ if (it != msgboxs.end())
+ (*it)->mCurrentTime = 0;
+ else
+ MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage59}");
}
}
@@ -162,38 +144,16 @@ namespace MWInput
resetIdleTime();
}
else
- {
- updateIdleTime(dt);
- }
+ mTimeIdle += dt;
mAttemptJump = false;
}
-
- bool ActionManager::isPreviewModeEnabled()
- {
- return MWBase::Environment::get().getWorld()->isPreviewModeEnabled();
- }
void ActionManager::resetIdleTime()
{
- if (mTimeIdle < 0)
- MWBase::Environment::get().getWorld()->toggleVanityMode(false);
mTimeIdle = 0.f;
}
- void ActionManager::updateIdleTime(float dt)
- {
- static const float vanityDelay = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>()
- .find("fVanityDelay")->mValue.getFloat();
- if (mTimeIdle >= 0.f)
- mTimeIdle += dt;
- if (mTimeIdle > vanityDelay)
- {
- MWBase::Environment::get().getWorld()->toggleVanityMode(true);
- mTimeIdle = -1.f;
- }
- }
-
void ActionManager::executeAction(int action)
{
MWBase::Environment::get().getLuaManager()->inputEvent({MWBase::LuaManager::InputEvent::Action, action});
@@ -281,14 +241,6 @@ namespace MWInput
case A_ToggleDebug:
windowManager->toggleDebugWindow();
break;
- case A_ZoomIn:
- if (inputManager->getControlSwitch("playerviewswitch") && inputManager->getControlSwitch("playercontrols") && !windowManager->isGuiMode())
- MWBase::Environment::get().getWorld()->adjustCameraDistance(-ZOOM_SCALE);
- break;
- case A_ZoomOut:
- if (inputManager->getControlSwitch("playerviewswitch") && inputManager->getControlSwitch("playercontrols") && !windowManager->isGuiMode())
- MWBase::Environment::get().getWorld()->adjustCameraDistance(ZOOM_SCALE);
- break;
case A_QuickSave:
quickSave();
break;
diff --git a/apps/openmw/mwinput/actionmanager.hpp b/apps/openmw/mwinput/actionmanager.hpp
index 2180e1944e..4c51139d46 100644
--- a/apps/openmw/mwinput/actionmanager.hpp
+++ b/apps/openmw/mwinput/actionmanager.hpp
@@ -55,13 +55,9 @@ namespace MWInput
void setAttemptJump(bool enabled) { mAttemptJump = enabled; }
- bool isPreviewModeEnabled();
-
private:
void handleGuiArrowKey(int action);
- void updateIdleTime(float dt);
-
BindingsManager* mBindingsManager;
osg::ref_ptr<osgViewer::Viewer> mViewer;
osg::ref_ptr<osgViewer::ScreenCaptureHandler> mScreenCaptureHandler;
@@ -71,8 +67,6 @@ namespace MWInput
bool mSneaking;
bool mAttemptJump;
- float mOverencumberedMessageDelay;
- float mPreviewPOVDelay;
float mTimeIdle;
};
}
diff --git a/apps/openmw/mwinput/bindingsmanager.cpp b/apps/openmw/mwinput/bindingsmanager.cpp
index ca7911ecc2..68be849dbd 100644
--- a/apps/openmw/mwinput/bindingsmanager.cpp
+++ b/apps/openmw/mwinput/bindingsmanager.cpp
@@ -5,6 +5,8 @@
#include <extern/oics/ICSChannelListener.h>
#include <extern/oics/ICSInputControlSystem.h>
+#include <components/sdlutil/sdlmappings.hpp>
+
#include "../mwbase/environment.hpp"
#include "../mwbase/inputmanager.hpp"
#include "../mwbase/windowmanager.hpp"
@@ -13,7 +15,6 @@
#include "../mwworld/player.hpp"
#include "actions.hpp"
-#include "sdlmappings.hpp"
namespace MWInput
{
@@ -546,9 +547,9 @@ namespace MWInput
ICS::Control* c = mInputBinder->getChannel(action)->getAttachedControls().front().control;
if (mInputBinder->getJoystickAxisBinding(c, sFakeDeviceId, ICS::Control::INCREASE) != ICS::InputControlSystem::UNASSIGNED)
- return sdlControllerAxisToString(mInputBinder->getJoystickAxisBinding(c, sFakeDeviceId, ICS::Control::INCREASE));
+ return SDLUtil::sdlControllerAxisToString(mInputBinder->getJoystickAxisBinding(c, sFakeDeviceId, ICS::Control::INCREASE));
else if (mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS)
- return sdlControllerButtonToString(mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE));
+ return SDLUtil::sdlControllerButtonToString(mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE));
else
return "#{sNone}";
}
@@ -653,14 +654,13 @@ namespace MWInput
return mInputBinder->getKeyBinding(mInputBinder->getControl(actionId), ICS::Control::INCREASE);
}
- float BindingsManager::getControllerAxisValue(SDL_GameControllerAxis axis) const
+ SDL_GameController* BindingsManager::getControllerOrNull() const
{
const auto& controllers = mInputBinder->getJoystickInstanceMap();
if (controllers.empty())
- return 0;
- SDL_GameController* cntrl = controllers.begin()->second;
- constexpr int AXIS_MAX_ABSOLUTE_VALUE = 32768;
- return SDL_GameControllerGetAxis(cntrl, axis) / static_cast<float>(AXIS_MAX_ABSOLUTE_VALUE);
+ return nullptr;
+ else
+ return controllers.begin()->second;
}
void BindingsManager::actionValueChanged(int action, float currentValue, float previousValue)
diff --git a/apps/openmw/mwinput/bindingsmanager.hpp b/apps/openmw/mwinput/bindingsmanager.hpp
index 5c653f0b3e..668cccd4ca 100644
--- a/apps/openmw/mwinput/bindingsmanager.hpp
+++ b/apps/openmw/mwinput/bindingsmanager.hpp
@@ -43,7 +43,8 @@ namespace MWInput
bool actionIsActive(int id) const;
float getActionValue(int id) const; // returns value in range [0, 1]
- float getControllerAxisValue(SDL_GameControllerAxis axis) const; // returns value in range [-1, 1]
+
+ SDL_GameController* getControllerOrNull() const;
void mousePressed(const SDL_MouseButtonEvent &evt, int deviceID);
void mouseReleased(const SDL_MouseButtonEvent &arg, int deviceID);
diff --git a/apps/openmw/mwinput/controllermanager.cpp b/apps/openmw/mwinput/controllermanager.cpp
index fa10ce03cd..1e72a1c95b 100644
--- a/apps/openmw/mwinput/controllermanager.cpp
+++ b/apps/openmw/mwinput/controllermanager.cpp
@@ -5,6 +5,7 @@
#include <MyGUI_Widget.h>
#include <components/debug/debuglog.hpp>
+#include <components/sdlutil/sdlmappings.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/inputmanager.hpp"
@@ -19,7 +20,6 @@
#include "actionmanager.hpp"
#include "bindingsmanager.hpp"
#include "mousemanager.hpp"
-#include "sdlmappings.hpp"
namespace MWInput
{
@@ -34,12 +34,10 @@ namespace MWInput
, mJoystickEnabled (Settings::Manager::getBool("enable controller", "Input"))
, mGamepadCursorSpeed(Settings::Manager::getFloat("gamepad cursor speed", "Input"))
, mSneakToggleShortcutTimer(0.f)
- , mGamepadZoom(0)
, mGamepadGuiCursorEnabled(true)
, mGuiCursorEnabled(true)
, mJoystickLastUsed(false)
, mSneakGamepadShortcut(false)
- , mGamepadPreviewMode(false)
{
if (!controllerBindingsFile.empty())
{
@@ -70,7 +68,7 @@ namespace MWInput
}
float deadZoneRadius = Settings::Manager::getFloat("joystick dead zone", "Input");
- deadZoneRadius = std::min(std::max(deadZoneRadius, 0.0f), 0.5f);
+ deadZoneRadius = std::clamp(deadZoneRadius, 0.f, 0.5f);
mBindingsManager->setJoystickDeadZone(deadZoneRadius);
}
@@ -85,8 +83,6 @@ namespace MWInput
bool ControllerManager::update(float dt)
{
- mGamepadPreviewMode = mActionManager->isPreviewModeEnabled();
-
if (mGuiCursorEnabled && !(mJoystickLastUsed && !mGamepadGuiCursorEnabled))
{
float xAxis = mBindingsManager->getActionValue(A_MoveLeftRight) * 2.0f - 1.0f;
@@ -115,7 +111,6 @@ namespace MWInput
if (MWBase::Environment::get().getWindowManager()->isGuiMode()
|| MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_Running)
{
- mGamepadZoom = 0;
return false;
}
@@ -182,15 +177,6 @@ namespace MWInput
}
}
- if (MWBase::Environment::get().getInputManager()->getControlSwitch("playerviewswitch"))
- {
- if (!mBindingsManager->actionIsActive(A_TogglePOV))
- mGamepadZoom = 0;
-
- if (mGamepadZoom)
- MWBase::Environment::get().getWorld()->adjustCameraDistance(-mGamepadZoom);
- }
-
return triedToMove;
}
@@ -229,7 +215,7 @@ namespace MWInput
mBindingsManager->setPlayerControlsEnabled(true);
//esc, to leave initial movie screen
- auto kc = sdlKeyToMyGUI(SDLK_ESCAPE);
+ auto kc = SDLUtil::sdlKeyToMyGUI(SDLK_ESCAPE);
mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyPress(kc, 0));
if (!MWBase::Environment::get().getInputManager()->controlsDisabled())
@@ -273,7 +259,7 @@ namespace MWInput
mBindingsManager->setPlayerControlsEnabled(true);
//esc, to leave initial movie screen
- auto kc = sdlKeyToMyGUI(SDLK_ESCAPE);
+ auto kc = SDLUtil::sdlKeyToMyGUI(SDLK_ESCAPE);
mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(kc));
mBindingsManager->controllerButtonReleased(deviceID, arg);
@@ -289,21 +275,11 @@ namespace MWInput
{
gamepadToGuiControl(arg);
}
- else
+ else if (MWBase::Environment::get().getWorld()->isPreviewModeEnabled() &&
+ (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT || arg.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT))
{
- if (mGamepadPreviewMode) // Preview Mode Gamepad Zooming
- {
- if (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT)
- {
- mGamepadZoom = arg.value * 0.85f / 1000.f / 12.f;
- return; // Do not propagate event.
- }
- else if (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT)
- {
- mGamepadZoom = -arg.value * 0.85f / 1000.f / 12.f;
- return; // Do not propagate event.
- }
- }
+ // Preview Mode Gamepad Zooming; do not propagate to mBindingsManager
+ return;
}
mBindingsManager->controllerAxisMoved(deviceID, arg);
}
@@ -403,4 +379,24 @@ namespace MWInput
return true;
}
+
+ float ControllerManager::getAxisValue(SDL_GameControllerAxis axis) const
+ {
+ SDL_GameController* cntrl = mBindingsManager->getControllerOrNull();
+ constexpr int AXIS_MAX_ABSOLUTE_VALUE = 32768;
+ if (cntrl)
+ return SDL_GameControllerGetAxis(cntrl, axis) / static_cast<float>(AXIS_MAX_ABSOLUTE_VALUE);
+ else
+ return 0;
+ }
+
+ bool ControllerManager::isButtonPressed(SDL_GameControllerButton button) const
+ {
+ SDL_GameController* cntrl = mBindingsManager->getControllerOrNull();
+ if (cntrl)
+ return SDL_GameControllerGetButton(cntrl, button) > 0;
+ else
+ return false;
+ }
+
}
diff --git a/apps/openmw/mwinput/controllermanager.hpp b/apps/openmw/mwinput/controllermanager.hpp
index d8c62d57c4..948b48d538 100644
--- a/apps/openmw/mwinput/controllermanager.hpp
+++ b/apps/openmw/mwinput/controllermanager.hpp
@@ -34,12 +34,15 @@ namespace MWInput
void processChangedSettings(const Settings::CategorySettingVector& changed);
void setJoystickLastUsed(bool enabled) { mJoystickLastUsed = enabled; }
- bool joystickLastUsed() { return mJoystickLastUsed; }
+ bool joystickLastUsed() const { return mJoystickLastUsed; }
void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; }
void setGamepadGuiCursorEnabled(bool enabled) { mGamepadGuiCursorEnabled = enabled; }
- bool gamepadGuiCursorEnabled() { return mGamepadGuiCursorEnabled; }
+ bool gamepadGuiCursorEnabled() const { return mGamepadGuiCursorEnabled; }
+
+ float getAxisValue(SDL_GameControllerAxis axis) const; // returns value in range [-1, 1]
+ bool isButtonPressed(SDL_GameControllerButton button) const;
private:
// Return true if GUI consumes input.
@@ -53,12 +56,10 @@ namespace MWInput
bool mJoystickEnabled;
float mGamepadCursorSpeed;
float mSneakToggleShortcutTimer;
- float mGamepadZoom;
bool mGamepadGuiCursorEnabled;
bool mGuiCursorEnabled;
bool mJoystickLastUsed;
bool mSneakGamepadShortcut;
- bool mGamepadPreviewMode;
};
}
#endif
diff --git a/apps/openmw/mwinput/controlswitch.cpp b/apps/openmw/mwinput/controlswitch.cpp
index f31744fca6..6c22e133bc 100644
--- a/apps/openmw/mwinput/controlswitch.cpp
+++ b/apps/openmw/mwinput/controlswitch.cpp
@@ -29,12 +29,15 @@ namespace MWInput
mSwitches["vanitymode"] = true;
}
- bool ControlSwitch::get(const std::string& key)
+ bool ControlSwitch::get(std::string_view key)
{
- return mSwitches[key];
+ auto it = mSwitches.find(key);
+ if (it == mSwitches.end())
+ throw std::runtime_error("Incorrect ControlSwitch: " + std::string(key));
+ return it->second;
}
- void ControlSwitch::set(const std::string& key, bool value)
+ void ControlSwitch::set(std::string_view key, bool value)
{
MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer();
@@ -51,15 +54,14 @@ namespace MWInput
/// \fixme maybe crouching at this time
player.setUpDown(0);
}
- else if (key == "vanitymode")
- {
- MWBase::Environment::get().getWorld()->allowVanityMode(value);
- }
else if (key == "playerlooking" && !value)
{
MWBase::Environment::get().getWorld()->rotateObject(player.getPlayer(), osg::Vec3f());
}
- mSwitches[key] = value;
+ auto it = mSwitches.find(key);
+ if (it == mSwitches.end())
+ throw std::runtime_error("Incorrect ControlSwitch: " + std::string(key));
+ it->second = value;
}
void ControlSwitch::write(ESM::ESMWriter& writer, Loading::Listener& /*progress*/)
diff --git a/apps/openmw/mwinput/controlswitch.hpp b/apps/openmw/mwinput/controlswitch.hpp
index 38d01066bd..b4353c31f5 100644
--- a/apps/openmw/mwinput/controlswitch.hpp
+++ b/apps/openmw/mwinput/controlswitch.hpp
@@ -3,6 +3,7 @@
#include <map>
#include <string>
+#include <string_view>
namespace ESM
{
@@ -23,8 +24,8 @@ namespace MWInput
public:
ControlSwitch();
- bool get(const std::string& key);
- void set(const std::string& key, bool value);
+ bool get(std::string_view key);
+ void set(std::string_view key, bool value);
void clear();
void write(ESM::ESMWriter& writer, Loading::Listener& progress);
@@ -32,7 +33,7 @@ namespace MWInput
int countSavedGameRecords() const;
private:
- std::map<std::string, bool> mSwitches;
+ std::map<std::string, bool, std::less<>> mSwitches;
};
}
#endif
diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp
index 31f515afb0..4ebe56bf94 100644
--- a/apps/openmw/mwinput/inputmanagerimp.cpp
+++ b/apps/openmw/mwinput/inputmanagerimp.cpp
@@ -18,7 +18,6 @@
#include "controlswitch.hpp"
#include "keyboardmanager.hpp"
#include "mousemanager.hpp"
-#include "sdlmappings.hpp"
#include "sensormanager.hpp"
namespace MWInput
@@ -101,8 +100,6 @@ namespace MWInput
mMouseManager->update(dt);
mSensorManager->update(dt);
mActionManager->update(dt, controllerMove);
-
- MWBase::Environment::get().getWorld()->applyDeferredPreviewRotationToPlayer(dt);
}
void InputManager::setDragDrop(bool dragDrop)
@@ -135,12 +132,12 @@ namespace MWInput
mSensorManager->processChangedSettings(changed);
}
- bool InputManager::getControlSwitch(const std::string& sw)
+ bool InputManager::getControlSwitch(std::string_view sw)
{
return mControlSwitch->get(sw);
}
- void InputManager::toggleControlSwitch(const std::string& sw, bool value)
+ void InputManager::toggleControlSwitch(std::string_view sw, bool value)
{
mControlSwitch->set(sw, value);
}
@@ -180,14 +177,14 @@ namespace MWInput
return mBindingsManager->getActionValue(action);
}
- float InputManager::getControllerAxisValue(SDL_GameControllerAxis axis) const
+ bool InputManager::isControllerButtonPressed(SDL_GameControllerButton button) const
{
- return mBindingsManager->getControllerAxisValue(axis);
+ return mControllerManager->isButtonPressed(button);
}
- uint32_t InputManager::getMouseButtonsState() const
+ float InputManager::getControllerAxisValue(SDL_GameControllerAxis axis) const
{
- return mMouseManager->getButtonsState();
+ return mControllerManager->getAxisValue(axis);
}
int InputManager::getMouseMoveX() const
diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp
index adb8319498..41478d5dcb 100644
--- a/apps/openmw/mwinput/inputmanagerimp.hpp
+++ b/apps/openmw/mwinput/inputmanagerimp.hpp
@@ -70,8 +70,8 @@ namespace MWInput
void setGamepadGuiCursorEnabled(bool enabled) override;
void setAttemptJump(bool jumping) override;
- void toggleControlSwitch (const std::string& sw, bool value) override;
- bool getControlSwitch (const std::string& sw) override;
+ void toggleControlSwitch(std::string_view sw, bool value) override;
+ bool getControlSwitch(std::string_view sw) override;
std::string getActionDescription (int action) const override;
std::string getActionKeyBindingName (int action) const override;
@@ -79,8 +79,8 @@ namespace MWInput
bool actionIsActive(int action) const override;
float getActionValue(int action) const override;
+ bool isControllerButtonPressed(SDL_GameControllerButton button) const override;
float getControllerAxisValue(SDL_GameControllerAxis axis) const override;
- uint32_t getMouseButtonsState() const override;
int getMouseMoveX() const override;
int getMouseMoveY() const override;
diff --git a/apps/openmw/mwinput/keyboardmanager.cpp b/apps/openmw/mwinput/keyboardmanager.cpp
index b8019b12ba..d8fc548f25 100644
--- a/apps/openmw/mwinput/keyboardmanager.cpp
+++ b/apps/openmw/mwinput/keyboardmanager.cpp
@@ -4,6 +4,8 @@
#include <MyGUI_InputManager.h>
+#include <components/sdlutil/sdlmappings.hpp>
+
#include "../mwbase/environment.hpp"
#include "../mwbase/inputmanager.hpp"
#include "../mwbase/luamanager.hpp"
@@ -13,7 +15,6 @@
#include "actions.hpp"
#include "bindingsmanager.hpp"
-#include "sdlmappings.hpp"
namespace MWInput
{
@@ -35,16 +36,16 @@ namespace MWInput
// HACK: to make default keybinding for the console work without printing an extra "^" upon closing
// This assumes that SDL_TextInput events always come *after* the key event
// (which is somewhat reasonable, and hopefully true for all SDL platforms)
- auto kc = sdlKeyToMyGUI(arg.keysym.sym);
+ auto kc = SDLUtil::sdlKeyToMyGUI(arg.keysym.sym);
if (mBindingsManager->getKeyBinding(A_Console) == arg.keysym.scancode
&& MWBase::Environment::get().getWindowManager()->isConsoleMode())
SDL_StopTextInput();
bool consumed = SDL_IsTextInputActive() && // Little trick to check if key is printable
(!(SDLK_SCANCODE_MASK & arg.keysym.sym) &&
- (std::isprint(arg.keysym.sym) ||
// Don't trust isprint for symbols outside the extended ASCII range
- (kc == MyGUI::KeyCode::None && arg.keysym.sym > 0xff)));
+ ((kc == MyGUI::KeyCode::None && arg.keysym.sym > 0xff) ||
+ (arg.keysym.sym >= 0 && arg.keysym.sym <= 255 && std::isprint(arg.keysym.sym))));
if (kc != MyGUI::KeyCode::None && !mBindingsManager->isDetectingBindingState())
{
if (MWBase::Environment::get().getWindowManager()->injectKeyPress(kc, 0, arg.repeat))
@@ -71,7 +72,7 @@ namespace MWInput
void KeyboardManager::keyReleased(const SDL_KeyboardEvent &arg)
{
MWBase::Environment::get().getInputManager()->setJoystickLastUsed(false);
- auto kc = sdlKeyToMyGUI(arg.keysym.sym);
+ auto kc = SDLUtil::sdlKeyToMyGUI(arg.keysym.sym);
if (!mBindingsManager->isDetectingBindingState())
mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(kc));
diff --git a/apps/openmw/mwinput/mousemanager.cpp b/apps/openmw/mwinput/mousemanager.cpp
index 7810a40ad2..f646501607 100644
--- a/apps/openmw/mwinput/mousemanager.cpp
+++ b/apps/openmw/mwinput/mousemanager.cpp
@@ -7,6 +7,7 @@
#include <components/debug/debuglog.hpp>
#include <components/sdlutil/sdlinputwrapper.hpp>
+#include <components/sdlutil/sdlmappings.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/inputmanager.hpp"
@@ -17,7 +18,6 @@
#include "actions.hpp"
#include "bindingsmanager.hpp"
-#include "sdlmappings.hpp"
namespace MWInput
{
@@ -34,7 +34,6 @@ namespace MWInput
, mMouseWheel(0)
, mMouseLookEnabled(false)
, mGuiCursorEnabled(true)
- , mButtonsState(0)
, mMouseMoveX(0)
, mMouseMoveY(0)
{
@@ -126,7 +125,11 @@ namespace MWInput
else
{
bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode();
- guiMode = MyGUI::InputManager::getInstance().injectMouseRelease(static_cast<int>(mGuiCursorX), static_cast<int>(mGuiCursorY), sdlButtonToMyGUI(id)) && guiMode;
+ guiMode = MyGUI::InputManager::getInstance().injectMouseRelease(
+ static_cast<int>(mGuiCursorX),
+ static_cast<int>(mGuiCursorY),
+ SDLUtil::sdlMouseButtonToMyGui(id)
+ ) && guiMode;
if (mBindingsManager->isDetectingBindingState())
return; // don't allow same mouseup to bind as initiated bind
@@ -154,7 +157,11 @@ namespace MWInput
if (id == SDL_BUTTON_LEFT || id == SDL_BUTTON_RIGHT) // MyGUI only uses these mouse events
{
guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode();
- guiMode = MyGUI::InputManager::getInstance().injectMousePress(static_cast<int>(mGuiCursorX), static_cast<int>(mGuiCursorY), sdlButtonToMyGUI(id)) && guiMode;
+ guiMode = MyGUI::InputManager::getInstance().injectMousePress(
+ static_cast<int>(mGuiCursorX),
+ static_cast<int>(mGuiCursorY),
+ SDLUtil::sdlMouseButtonToMyGui(id)
+ ) && guiMode;
if (MyGUI::InputManager::getInstance().getMouseFocusWidget () != nullptr)
{
MyGUI::Button* b = MyGUI::InputManager::getInstance().getMouseFocusWidget()->castType<MyGUI::Button>(false);
@@ -199,7 +206,7 @@ namespace MWInput
void MouseManager::update(float dt)
{
- mButtonsState = SDL_GetRelativeMouseState(&mMouseMoveX, &mMouseMoveY);
+ SDL_GetRelativeMouseState(&mMouseMoveX, &mMouseMoveY);
if (!mMouseLookEnabled)
return;
@@ -230,12 +237,18 @@ namespace MWInput
bool MouseManager::injectMouseButtonPress(Uint8 button)
{
- return MyGUI::InputManager::getInstance().injectMousePress(static_cast<int>(mGuiCursorX), static_cast<int>(mGuiCursorY), sdlButtonToMyGUI(button));
+ return MyGUI::InputManager::getInstance().injectMousePress(
+ static_cast<int>(mGuiCursorX),
+ static_cast<int>(mGuiCursorY),
+ SDLUtil::sdlMouseButtonToMyGui(button));
}
bool MouseManager::injectMouseButtonRelease(Uint8 button)
{
- return MyGUI::InputManager::getInstance().injectMouseRelease(static_cast<int>(mGuiCursorX), static_cast<int>(mGuiCursorY), sdlButtonToMyGUI(button));
+ return MyGUI::InputManager::getInstance().injectMouseRelease(
+ static_cast<int>(mGuiCursorX),
+ static_cast<int>(mGuiCursorY),
+ SDLUtil::sdlMouseButtonToMyGui(button));
}
void MouseManager::injectMouseMove(float xMove, float yMove, float mouseWheelMove)
@@ -245,8 +258,8 @@ namespace MWInput
mMouseWheel += mouseWheelMove;
const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize();
- mGuiCursorX = std::max(0.f, std::min(mGuiCursorX, float(viewSize.width - 1)));
- mGuiCursorY = std::max(0.f, std::min(mGuiCursorY, float(viewSize.height - 1)));
+ mGuiCursorX = std::clamp<float>(mGuiCursorX, 0.f, viewSize.width - 1);
+ mGuiCursorY = std::clamp<float>(mGuiCursorY, 0.f, viewSize.height - 1);
MyGUI::InputManager::getInstance().injectMouseMove(static_cast<int>(mGuiCursorX), static_cast<int>(mGuiCursorY), static_cast<int>(mMouseWheel));
}
diff --git a/apps/openmw/mwinput/mousemanager.hpp b/apps/openmw/mwinput/mousemanager.hpp
index d5504c5f5a..16ea56d62b 100644
--- a/apps/openmw/mwinput/mousemanager.hpp
+++ b/apps/openmw/mwinput/mousemanager.hpp
@@ -38,7 +38,6 @@ namespace MWInput
void setMouseLookEnabled(bool enabled) { mMouseLookEnabled = enabled; }
void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; }
- uint32_t getButtonsState() const { return mButtonsState; }
int getMouseMoveX() const { return mMouseMoveX; }
int getMouseMoveY() const { return mMouseMoveY; }
@@ -58,7 +57,6 @@ namespace MWInput
bool mMouseLookEnabled;
bool mGuiCursorEnabled;
- uint32_t mButtonsState;
int mMouseMoveX;
int mMouseMoveY;
};
diff --git a/apps/openmw/mwinput/sdlmappings.cpp b/apps/openmw/mwinput/sdlmappings.cpp
deleted file mode 100644
index 0c3f5c5d85..0000000000
--- a/apps/openmw/mwinput/sdlmappings.cpp
+++ /dev/null
@@ -1,218 +0,0 @@
-#include "sdlmappings.hpp"
-
-#include <map>
-
-#include <MyGUI_MouseButton.h>
-
-#include <SDL_gamecontroller.h>
-#include <SDL_mouse.h>
-
-namespace MWInput
-{
- std::string sdlControllerButtonToString(int button)
- {
- switch(button)
- {
- case SDL_CONTROLLER_BUTTON_A:
- return "A Button";
- case SDL_CONTROLLER_BUTTON_B:
- return "B Button";
- case SDL_CONTROLLER_BUTTON_BACK:
- return "Back Button";
- case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
- return "DPad Down";
- case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
- return "DPad Left";
- case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
- return "DPad Right";
- case SDL_CONTROLLER_BUTTON_DPAD_UP:
- return "DPad Up";
- case SDL_CONTROLLER_BUTTON_GUIDE:
- return "Guide Button";
- case SDL_CONTROLLER_BUTTON_LEFTSHOULDER:
- return "Left Shoulder";
- case SDL_CONTROLLER_BUTTON_LEFTSTICK:
- return "Left Stick Button";
- case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER:
- return "Right Shoulder";
- case SDL_CONTROLLER_BUTTON_RIGHTSTICK:
- return "Right Stick Button";
- case SDL_CONTROLLER_BUTTON_START:
- return "Start Button";
- case SDL_CONTROLLER_BUTTON_X:
- return "X Button";
- case SDL_CONTROLLER_BUTTON_Y:
- return "Y Button";
- default:
- return "Button " + std::to_string(button);
- }
- }
-
- std::string sdlControllerAxisToString(int axis)
- {
- switch(axis)
- {
- case SDL_CONTROLLER_AXIS_LEFTX:
- return "Left Stick X";
- case SDL_CONTROLLER_AXIS_LEFTY:
- return "Left Stick Y";
- case SDL_CONTROLLER_AXIS_RIGHTX:
- return "Right Stick X";
- case SDL_CONTROLLER_AXIS_RIGHTY:
- return "Right Stick Y";
- case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
- return "Left Trigger";
- case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
- return "Right Trigger";
- default:
- return "Axis " + std::to_string(axis);
- }
- }
-
- MyGUI::MouseButton sdlButtonToMyGUI(Uint8 button)
- {
- //The right button is the second button, according to MyGUI
- if(button == SDL_BUTTON_RIGHT)
- button = SDL_BUTTON_MIDDLE;
- else if(button == SDL_BUTTON_MIDDLE)
- button = SDL_BUTTON_RIGHT;
-
- //MyGUI's buttons are 0 indexed
- return MyGUI::MouseButton::Enum(button - 1);
- }
-
- void initKeyMap(std::map<SDL_Keycode, MyGUI::KeyCode>& keyMap)
- {
- keyMap[SDLK_UNKNOWN] = MyGUI::KeyCode::None;
- keyMap[SDLK_ESCAPE] = MyGUI::KeyCode::Escape;
- keyMap[SDLK_1] = MyGUI::KeyCode::One;
- keyMap[SDLK_2] = MyGUI::KeyCode::Two;
- keyMap[SDLK_3] = MyGUI::KeyCode::Three;
- keyMap[SDLK_4] = MyGUI::KeyCode::Four;
- keyMap[SDLK_5] = MyGUI::KeyCode::Five;
- keyMap[SDLK_6] = MyGUI::KeyCode::Six;
- keyMap[SDLK_7] = MyGUI::KeyCode::Seven;
- keyMap[SDLK_8] = MyGUI::KeyCode::Eight;
- keyMap[SDLK_9] = MyGUI::KeyCode::Nine;
- keyMap[SDLK_0] = MyGUI::KeyCode::Zero;
- keyMap[SDLK_MINUS] = MyGUI::KeyCode::Minus;
- keyMap[SDLK_EQUALS] = MyGUI::KeyCode::Equals;
- keyMap[SDLK_BACKSPACE] = MyGUI::KeyCode::Backspace;
- keyMap[SDLK_TAB] = MyGUI::KeyCode::Tab;
- keyMap[SDLK_q] = MyGUI::KeyCode::Q;
- keyMap[SDLK_w] = MyGUI::KeyCode::W;
- keyMap[SDLK_e] = MyGUI::KeyCode::E;
- keyMap[SDLK_r] = MyGUI::KeyCode::R;
- keyMap[SDLK_t] = MyGUI::KeyCode::T;
- keyMap[SDLK_y] = MyGUI::KeyCode::Y;
- keyMap[SDLK_u] = MyGUI::KeyCode::U;
- keyMap[SDLK_i] = MyGUI::KeyCode::I;
- keyMap[SDLK_o] = MyGUI::KeyCode::O;
- keyMap[SDLK_p] = MyGUI::KeyCode::P;
- keyMap[SDLK_RETURN] = MyGUI::KeyCode::Return;
- keyMap[SDLK_a] = MyGUI::KeyCode::A;
- keyMap[SDLK_s] = MyGUI::KeyCode::S;
- keyMap[SDLK_d] = MyGUI::KeyCode::D;
- keyMap[SDLK_f] = MyGUI::KeyCode::F;
- keyMap[SDLK_g] = MyGUI::KeyCode::G;
- keyMap[SDLK_h] = MyGUI::KeyCode::H;
- keyMap[SDLK_j] = MyGUI::KeyCode::J;
- keyMap[SDLK_k] = MyGUI::KeyCode::K;
- keyMap[SDLK_l] = MyGUI::KeyCode::L;
- keyMap[SDLK_SEMICOLON] = MyGUI::KeyCode::Semicolon;
- keyMap[SDLK_QUOTE] = MyGUI::KeyCode::Apostrophe;
- keyMap[SDLK_BACKQUOTE] = MyGUI::KeyCode::Grave;
- keyMap[SDLK_LSHIFT] = MyGUI::KeyCode::LeftShift;
- keyMap[SDLK_BACKSLASH] = MyGUI::KeyCode::Backslash;
- keyMap[SDLK_z] = MyGUI::KeyCode::Z;
- keyMap[SDLK_x] = MyGUI::KeyCode::X;
- keyMap[SDLK_c] = MyGUI::KeyCode::C;
- keyMap[SDLK_v] = MyGUI::KeyCode::V;
- keyMap[SDLK_b] = MyGUI::KeyCode::B;
- keyMap[SDLK_n] = MyGUI::KeyCode::N;
- keyMap[SDLK_m] = MyGUI::KeyCode::M;
- keyMap[SDLK_COMMA] = MyGUI::KeyCode::Comma;
- keyMap[SDLK_PERIOD] = MyGUI::KeyCode::Period;
- keyMap[SDLK_SLASH] = MyGUI::KeyCode::Slash;
- keyMap[SDLK_RSHIFT] = MyGUI::KeyCode::RightShift;
- keyMap[SDLK_KP_MULTIPLY] = MyGUI::KeyCode::Multiply;
- keyMap[SDLK_LALT] = MyGUI::KeyCode::LeftAlt;
- keyMap[SDLK_SPACE] = MyGUI::KeyCode::Space;
- keyMap[SDLK_CAPSLOCK] = MyGUI::KeyCode::Capital;
- keyMap[SDLK_F1] = MyGUI::KeyCode::F1;
- keyMap[SDLK_F2] = MyGUI::KeyCode::F2;
- keyMap[SDLK_F3] = MyGUI::KeyCode::F3;
- keyMap[SDLK_F4] = MyGUI::KeyCode::F4;
- keyMap[SDLK_F5] = MyGUI::KeyCode::F5;
- keyMap[SDLK_F6] = MyGUI::KeyCode::F6;
- keyMap[SDLK_F7] = MyGUI::KeyCode::F7;
- keyMap[SDLK_F8] = MyGUI::KeyCode::F8;
- keyMap[SDLK_F9] = MyGUI::KeyCode::F9;
- keyMap[SDLK_F10] = MyGUI::KeyCode::F10;
- keyMap[SDLK_NUMLOCKCLEAR] = MyGUI::KeyCode::NumLock;
- keyMap[SDLK_SCROLLLOCK] = MyGUI::KeyCode::ScrollLock;
- keyMap[SDLK_KP_7] = MyGUI::KeyCode::Numpad7;
- keyMap[SDLK_KP_8] = MyGUI::KeyCode::Numpad8;
- keyMap[SDLK_KP_9] = MyGUI::KeyCode::Numpad9;
- keyMap[SDLK_KP_MINUS] = MyGUI::KeyCode::Subtract;
- keyMap[SDLK_KP_4] = MyGUI::KeyCode::Numpad4;
- keyMap[SDLK_KP_5] = MyGUI::KeyCode::Numpad5;
- keyMap[SDLK_KP_6] = MyGUI::KeyCode::Numpad6;
- keyMap[SDLK_KP_PLUS] = MyGUI::KeyCode::Add;
- keyMap[SDLK_KP_1] = MyGUI::KeyCode::Numpad1;
- keyMap[SDLK_KP_2] = MyGUI::KeyCode::Numpad2;
- keyMap[SDLK_KP_3] = MyGUI::KeyCode::Numpad3;
- keyMap[SDLK_KP_0] = MyGUI::KeyCode::Numpad0;
- keyMap[SDLK_KP_PERIOD] = MyGUI::KeyCode::Decimal;
- keyMap[SDLK_F11] = MyGUI::KeyCode::F11;
- keyMap[SDLK_F12] = MyGUI::KeyCode::F12;
- keyMap[SDLK_F13] = MyGUI::KeyCode::F13;
- keyMap[SDLK_F14] = MyGUI::KeyCode::F14;
- keyMap[SDLK_F15] = MyGUI::KeyCode::F15;
- keyMap[SDLK_KP_EQUALS] = MyGUI::KeyCode::NumpadEquals;
- keyMap[SDLK_COLON] = MyGUI::KeyCode::Colon;
- keyMap[SDLK_KP_ENTER] = MyGUI::KeyCode::NumpadEnter;
- keyMap[SDLK_KP_DIVIDE] = MyGUI::KeyCode::Divide;
- keyMap[SDLK_SYSREQ] = MyGUI::KeyCode::SysRq;
- keyMap[SDLK_RALT] = MyGUI::KeyCode::RightAlt;
- keyMap[SDLK_HOME] = MyGUI::KeyCode::Home;
- keyMap[SDLK_UP] = MyGUI::KeyCode::ArrowUp;
- keyMap[SDLK_PAGEUP] = MyGUI::KeyCode::PageUp;
- keyMap[SDLK_LEFT] = MyGUI::KeyCode::ArrowLeft;
- keyMap[SDLK_RIGHT] = MyGUI::KeyCode::ArrowRight;
- keyMap[SDLK_END] = MyGUI::KeyCode::End;
- keyMap[SDLK_DOWN] = MyGUI::KeyCode::ArrowDown;
- keyMap[SDLK_PAGEDOWN] = MyGUI::KeyCode::PageDown;
- keyMap[SDLK_INSERT] = MyGUI::KeyCode::Insert;
- keyMap[SDLK_DELETE] = MyGUI::KeyCode::Delete;
- keyMap[SDLK_APPLICATION] = MyGUI::KeyCode::AppMenu;
-
-//The function of the Ctrl and Meta keys are switched on macOS compared to other platforms.
-//For instance] = Cmd+C versus Ctrl+C to copy from the system clipboard
-#if defined(__APPLE__)
- keyMap[SDLK_LGUI] = MyGUI::KeyCode::LeftControl;
- keyMap[SDLK_RGUI] = MyGUI::KeyCode::RightControl;
- keyMap[SDLK_LCTRL] = MyGUI::KeyCode::LeftWindows;
- keyMap[SDLK_RCTRL] = MyGUI::KeyCode::RightWindows;
-#else
- keyMap[SDLK_LGUI] = MyGUI::KeyCode::LeftWindows;
- keyMap[SDLK_RGUI] = MyGUI::KeyCode::RightWindows;
- keyMap[SDLK_LCTRL] = MyGUI::KeyCode::LeftControl;
- keyMap[SDLK_RCTRL] = MyGUI::KeyCode::RightControl;
-#endif
- }
-
- MyGUI::KeyCode sdlKeyToMyGUI(SDL_Keycode code)
- {
- static std::map<SDL_Keycode, MyGUI::KeyCode> keyMap;
- if (keyMap.empty())
- initKeyMap(keyMap);
-
- MyGUI::KeyCode kc = MyGUI::KeyCode::None;
- auto foundKey = keyMap.find(code);
- if (foundKey != keyMap.end())
- kc = foundKey->second;
-
- return kc;
- }
-}
diff --git a/apps/openmw/mwlua/actions.cpp b/apps/openmw/mwlua/actions.cpp
index 92a7f915bb..49bb5cfcb4 100644
--- a/apps/openmw/mwlua/actions.cpp
+++ b/apps/openmw/mwlua/actions.cpp
@@ -3,6 +3,8 @@
#include <cstring>
#include <components/debug/debuglog.hpp>
+#include <components/lua/luastate.hpp>
+#include <components/settings/settings.hpp>
#include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp"
@@ -11,15 +13,35 @@
namespace MWLua
{
+ Action::Action(LuaUtil::LuaState* state)
+ {
+ static const bool luaDebug = Settings::Manager::getBool("lua debug", "Lua");
+ if (luaDebug)
+ mCallerTraceback = state->debugTraceback();
+ }
+
+ void Action::safeApply(WorldView& w) const
+ {
+ try
+ {
+ apply(w);
+ }
+ catch (const std::exception& e)
+ {
+ Log(Debug::Error) << "Error in " << this->toString() << ": " << e.what();
+
+ if (mCallerTraceback.empty())
+ Log(Debug::Error) << "Set 'lua_debug=true' in settings.cfg to enable action tracebacks";
+ else
+ Log(Debug::Error) << "Caller " << mCallerTraceback;
+ }
+ }
void TeleportAction::apply(WorldView& worldView) const
{
MWWorld::CellStore* cell = worldView.findCell(mCell, mPos);
if (!cell)
- {
- Log(Debug::Error) << "LuaManager::applyTeleport -> cell not found: '" << mCell << "'";
- return;
- }
+ throw std::runtime_error(std::string("cell not found: '") + mCell + "'");
MWBase::World* world = MWBase::Environment::get().getWorld();
MWWorld::Ptr obj = worldView.getObjectRegistry()->getPtr(mObject, false);
diff --git a/apps/openmw/mwlua/actions.hpp b/apps/openmw/mwlua/actions.hpp
index 900b175320..e88b39e83c 100644
--- a/apps/openmw/mwlua/actions.hpp
+++ b/apps/openmw/mwlua/actions.hpp
@@ -6,6 +6,11 @@
#include "object.hpp"
#include "worldview.hpp"
+namespace LuaUtil
+{
+ class LuaState;
+}
+
namespace MWLua
{
@@ -16,17 +21,25 @@ namespace MWLua
class Action
{
public:
+ Action(LuaUtil::LuaState* state);
virtual ~Action() {}
+
+ void safeApply(WorldView&) const;
virtual void apply(WorldView&) const = 0;
+ virtual std::string toString() const = 0;
+
+ private:
+ std::string mCallerTraceback;
};
class TeleportAction final : public Action
{
public:
- TeleportAction(ObjectId object, std::string cell, const osg::Vec3f& pos, const osg::Vec3f& rot)
- : mObject(object), mCell(std::move(cell)), mPos(pos), mRot(rot) {}
+ TeleportAction(LuaUtil::LuaState* state, ObjectId object, std::string cell, const osg::Vec3f& pos, const osg::Vec3f& rot)
+ : Action(state), mObject(object), mCell(std::move(cell)), mPos(pos), mRot(rot) {}
void apply(WorldView&) const override;
+ std::string toString() const override { return "TeleportAction"; }
private:
ObjectId mObject;
@@ -41,9 +54,11 @@ namespace MWLua
using Item = std::variant<std::string, ObjectId>; // recordId or ObjectId
using Equipment = std::map<int, Item>; // slot to item
- SetEquipmentAction(ObjectId actor, Equipment equipment) : mActor(actor), mEquipment(std::move(equipment)) {}
+ SetEquipmentAction(LuaUtil::LuaState* state, ObjectId actor, Equipment equipment)
+ : Action(state), mActor(actor), mEquipment(std::move(equipment)) {}
void apply(WorldView&) const override;
+ std::string toString() const override { return "SetEquipmentAction"; }
private:
ObjectId mActor;
diff --git a/apps/openmw/mwlua/asyncbindings.cpp b/apps/openmw/mwlua/asyncbindings.cpp
index 9bddf75ee4..0ffb2aad8f 100644
--- a/apps/openmw/mwlua/asyncbindings.cpp
+++ b/apps/openmw/mwlua/asyncbindings.cpp
@@ -19,36 +19,36 @@ namespace MWLua
sol::function getAsyncPackageInitializer(const Context& context)
{
- using TimeUnit = LuaUtil::ScriptsContainer::TimeUnit;
+ using TimerType = LuaUtil::ScriptsContainer::TimerType;
sol::usertype<AsyncPackageId> api = context.mLua->sol().new_usertype<AsyncPackageId>("AsyncPackage");
api["registerTimerCallback"] = [](const AsyncPackageId& asyncId, std::string_view name, sol::function callback)
{
asyncId.mContainer->registerTimerCallback(asyncId.mScriptId, name, std::move(callback));
return TimerCallback{asyncId, std::string(name)};
};
- api["newTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId&, double delay,
- const TimerCallback& callback, sol::object callbackArg)
+ api["newSimulationTimer"] = [world=context.mWorldView](const AsyncPackageId&, double delay,
+ const TimerCallback& callback, sol::object callbackArg)
{
callback.mAsyncId.mContainer->setupSerializableTimer(
- TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay,
+ TimerType::SIMULATION_TIME, world->getSimulationTime() + delay,
callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg));
};
- api["newTimerInHours"] = [world=context.mWorldView](const AsyncPackageId&, double delay,
- const TimerCallback& callback, sol::object callbackArg)
+ api["newGameTimer"] = [world=context.mWorldView](const AsyncPackageId&, double delay,
+ const TimerCallback& callback, sol::object callbackArg)
{
callback.mAsyncId.mContainer->setupSerializableTimer(
- TimeUnit::HOURS, world->getGameTimeInHours() + delay,
+ TimerType::GAME_TIME, world->getGameTime() + delay,
callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg));
};
- api["newUnsavableTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback)
+ api["newUnsavableSimulationTimer"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback)
{
asyncId.mContainer->setupUnsavableTimer(
- TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, asyncId.mScriptId, std::move(callback));
+ TimerType::SIMULATION_TIME, world->getSimulationTime() + delay, asyncId.mScriptId, std::move(callback));
};
- api["newUnsavableTimerInHours"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback)
+ api["newUnsavableGameTimer"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback)
{
asyncId.mContainer->setupUnsavableTimer(
- TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScriptId, std::move(callback));
+ TimerType::GAME_TIME, world->getGameTime() + delay, asyncId.mScriptId, std::move(callback));
};
api["callback"] = [](const AsyncPackageId& asyncId, sol::function fn)
{
diff --git a/apps/openmw/mwlua/camerabindings.cpp b/apps/openmw/mwlua/camerabindings.cpp
index 46bf079d65..abbd806cef 100644
--- a/apps/openmw/mwlua/camerabindings.cpp
+++ b/apps/openmw/mwlua/camerabindings.cpp
@@ -1,12 +1,82 @@
#include "luabindings.hpp"
+#include "../mwrender/camera.hpp"
+
namespace MWLua
{
+ using CameraMode = MWRender::Camera::Mode;
+
sol::table initCameraPackage(const Context& context)
{
+ MWRender::Camera* camera = MWBase::Environment::get().getWorld()->getCamera();
+
sol::table api(context.mLua->sol(), sol::create);
- // TODO
+ api["MODE"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with(
+ "Static", CameraMode::Static,
+ "FirstPerson", CameraMode::FirstPerson,
+ "ThirdPerson", CameraMode::ThirdPerson,
+ "Vanity", CameraMode::Vanity,
+ "Preview", CameraMode::Preview
+ ));
+
+ api["getMode"] = [camera]() -> int { return static_cast<int>(camera->getMode()); };
+ api["getQueuedMode"] = [camera]() -> sol::optional<int>
+ {
+ std::optional<CameraMode> mode = camera->getQueuedMode();
+ if (mode)
+ return static_cast<int>(*mode);
+ else
+ return sol::nullopt;
+ };
+ api["setMode"] = [camera](int mode, sol::optional<bool> force)
+ {
+ camera->setMode(static_cast<CameraMode>(mode), force ? *force : false);
+ };
+
+ api["allowCharacterDeferredRotation"] = [camera](bool v) { camera->allowCharacterDeferredRotation(v); };
+ api["showCrosshair"] = [camera](bool v) { camera->showCrosshair(v); };
+
+ api["getTrackedPosition"] = [camera]() -> osg::Vec3f { return camera->getTrackedPosition(); };
+ api["getPosition"] = [camera]() -> osg::Vec3f { return camera->getPosition(); };
+
+ // All angles are negated in order to make camera rotation consistent with objects rotation.
+ // TODO: Fix the inconsistency of rotation direction in camera.cpp.
+ api["getPitch"] = [camera]() { return -camera->getPitch(); };
+ api["getYaw"] = [camera]() { return -camera->getYaw(); };
+ api["getRoll"] = [camera]() { return -camera->getRoll(); };
+
+ api["setStaticPosition"] = [camera](const osg::Vec3f& pos) { camera->setStaticPosition(pos); };
+ api["setPitch"] = [camera](float v)
+ {
+ camera->setPitch(-v, true);
+ if (camera->getMode() == CameraMode::ThirdPerson)
+ camera->calculateDeferredRotation();
+ };
+ api["setYaw"] = [camera](float v)
+ {
+ camera->setYaw(-v, true);
+ if (camera->getMode() == CameraMode::ThirdPerson)
+ camera->calculateDeferredRotation();
+ };
+ api["setRoll"] = [camera](float v) { camera->setRoll(-v); };
+ api["setExtraPitch"] = [camera](float v) { camera->setExtraPitch(-v); };
+ api["setExtraYaw"] = [camera](float v) { camera->setExtraYaw(-v); };
+ api["getExtraPitch"] = [camera]() { return -camera->getExtraPitch(); };
+ api["getExtraYaw"] = [camera]() { return -camera->getExtraYaw(); };
+
+ api["getThirdPersonDistance"] = [camera]() { return camera->getCameraDistance(); };
+ api["setPreferredThirdPersonDistance"] = [camera](float v) { camera->setPreferredCameraDistance(v); };
+
+ api["getFirstPersonOffset"] = [camera]() { return camera->getFirstPersonOffset(); };
+ api["setFirstPersonOffset"] = [camera](const osg::Vec3f& v) { camera->setFirstPersonOffset(v); };
+
+ api["getFocalPreferredOffset"] = [camera]() -> osg::Vec2f { return camera->getFocalPointTargetOffset(); };
+ api["setFocalPreferredOffset"] = [camera](const osg::Vec2f& v) { camera->setFocalPointTargetOffset(v); };
+ api["getFocalTransitionSpeed"] = [camera]() { return camera->getFocalPointTransitionSpeed(); };
+ api["setFocalTransitionSpeed"] = [camera](float v) { camera->setFocalPointTransitionSpeed(v); };
+ api["instantTransition"] = [camera]() { camera->instantTransition(); };
+
return LuaUtil::makeReadOnly(api);
}
diff --git a/apps/openmw/mwlua/cellbindings.cpp b/apps/openmw/mwlua/cellbindings.cpp
index a23fb47c32..4ca9018cad 100644
--- a/apps/openmw/mwlua/cellbindings.cpp
+++ b/apps/openmw/mwlua/cellbindings.cpp
@@ -4,6 +4,14 @@
#include "../mwworld/cellstore.hpp"
+namespace sol
+{
+ template <>
+ struct is_automagical<MWLua::LCell> : std::false_type {};
+ template <>
+ struct is_automagical<MWLua::GCell> : std::false_type {};
+}
+
namespace MWLua
{
diff --git a/apps/openmw/mwlua/context.hpp b/apps/openmw/mwlua/context.hpp
index b3e3703a46..7ff584d8cc 100644
--- a/apps/openmw/mwlua/context.hpp
+++ b/apps/openmw/mwlua/context.hpp
@@ -7,6 +7,7 @@ namespace LuaUtil
{
class LuaState;
class UserdataSerializer;
+ class I18nManager;
}
namespace MWLua
@@ -20,6 +21,7 @@ namespace MWLua
LuaManager* mLuaManager;
LuaUtil::LuaState* mLua;
LuaUtil::UserdataSerializer* mSerializer;
+ LuaUtil::I18nManager* mI18n;
WorldView* mWorldView;
LocalEventQueue* mLocalEventQueue;
GlobalEventQueue* mGlobalEventQueue;
diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp
index aaa00f3da9..9ca2d94770 100644
--- a/apps/openmw/mwlua/inputbindings.cpp
+++ b/apps/openmw/mwlua/inputbindings.cpp
@@ -2,6 +2,7 @@
#include <SDL_events.h>
#include <SDL_gamecontroller.h>
+#include <SDL_mouse.h>
#include "../mwbase/inputmanager.hpp"
#include "../mwinput/actions.hpp"
@@ -18,9 +19,14 @@ namespace MWLua
sol::table initInputPackage(const Context& context)
{
sol::usertype<SDL_Keysym> keyEvent = context.mLua->sol().new_usertype<SDL_Keysym>("KeyEvent");
- keyEvent["symbol"] = sol::readonly_property([](const SDL_Keysym& e) { return std::string(1, static_cast<char>(e.sym)); });
- keyEvent["code"] = sol::readonly_property([](const SDL_Keysym& e) -> int { return e.sym; });
- keyEvent["modifiers"] = sol::readonly_property([](const SDL_Keysym& e) -> int { return e.mod; });
+ keyEvent["symbol"] = sol::readonly_property([](const SDL_Keysym& e)
+ {
+ if (e.sym > 0 && e.sym <= 255)
+ return std::string(1, static_cast<char>(e.sym));
+ else
+ return std::string();
+ });
+ keyEvent["code"] = sol::readonly_property([](const SDL_Keysym& e) -> int { return e.scancode; });
keyEvent["withShift"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_SHIFT; });
keyEvent["withCtrl"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_CTRL; });
keyEvent["withAlt"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_ALT; });
@@ -31,9 +37,26 @@ namespace MWLua
api["isIdle"] = [input]() { return input->isIdle(); };
api["isActionPressed"] = [input](int action) { return input->actionIsActive(action); };
- api["isMouseButtonPressed"] = [input](int button) -> bool
+ api["isKeyPressed"] = [](SDL_Scancode code) -> bool
+ {
+ int maxCode;
+ const auto* state = SDL_GetKeyboardState(&maxCode);
+ if (code >= 0 && code < maxCode)
+ return state[code] != 0;
+ else
+ return false;
+ };
+ api["isShiftPressed"] = []() -> bool { return SDL_GetModState() & KMOD_SHIFT; };
+ api["isCtrlPressed"] = []() -> bool { return SDL_GetModState() & KMOD_CTRL; };
+ api["isAltPressed"] = []() -> bool { return SDL_GetModState() & KMOD_ALT; };
+ api["isSuperPressed"] = []() -> bool { return SDL_GetModState() & KMOD_GUI; };
+ api["isControllerButtonPressed"] = [input](int button)
+ {
+ return input->isControllerButtonPressed(static_cast<SDL_GameControllerButton>(button));
+ };
+ api["isMouseButtonPressed"] = [](int button) -> bool
{
- return input->getMouseButtonsState() & (1 << (button - 1));
+ return SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(button);
};
api["getMouseMoveX"] = [input]() { return input->getMouseMoveX(); };
api["getMouseMoveY"] = [input]() { return input->getMouseMoveY(); };
@@ -45,104 +68,219 @@ namespace MWLua
return input->getActionValue(axis - SDL_CONTROLLER_AXIS_MAX) * 2 - 1;
};
- api["getControlSwitch"] = [input](const std::string& key) { return input->getControlSwitch(key); };
- api["setControlSwitch"] = [input](const std::string& key, bool v) { input->toggleControlSwitch(key, v); };
-
- api["ACTION"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with(
- "GameMenu", MWInput::A_GameMenu,
- "Screenshot", MWInput::A_Screenshot,
- "Inventory", MWInput::A_Inventory,
- "Console", MWInput::A_Console,
-
- "MoveLeft", MWInput::A_MoveLeft,
- "MoveRight", MWInput::A_MoveRight,
- "MoveForward", MWInput::A_MoveForward,
- "MoveBackward", MWInput::A_MoveBackward,
-
- "Activate", MWInput::A_Activate,
- "Use", MWInput::A_Use,
- "Jump", MWInput::A_Jump,
- "AutoMove", MWInput::A_AutoMove,
- "Rest", MWInput::A_Rest,
- "Journal", MWInput::A_Journal,
- "Weapon", MWInput::A_Weapon,
- "Spell", MWInput::A_Spell,
- "Run", MWInput::A_Run,
- "CycleSpellLeft", MWInput::A_CycleSpellLeft,
- "CycleSpellRight", MWInput::A_CycleSpellRight,
- "CycleWeaponLeft", MWInput::A_CycleWeaponLeft,
- "CycleWeaponRight", MWInput::A_CycleWeaponRight,
- "ToggleSneak", MWInput::A_ToggleSneak,
- "AlwaysRun", MWInput::A_AlwaysRun,
- "Sneak", MWInput::A_Sneak,
-
- "QuickSave", MWInput::A_QuickSave,
- "QuickLoad", MWInput::A_QuickLoad,
- "QuickMenu", MWInput::A_QuickMenu,
- "ToggleWeapon", MWInput::A_ToggleWeapon,
- "ToggleSpell", MWInput::A_ToggleSpell,
- "TogglePOV", MWInput::A_TogglePOV,
-
- "QuickKey1", MWInput::A_QuickKey1,
- "QuickKey2", MWInput::A_QuickKey2,
- "QuickKey3", MWInput::A_QuickKey3,
- "QuickKey4", MWInput::A_QuickKey4,
- "QuickKey5", MWInput::A_QuickKey5,
- "QuickKey6", MWInput::A_QuickKey6,
- "QuickKey7", MWInput::A_QuickKey7,
- "QuickKey8", MWInput::A_QuickKey8,
- "QuickKey9", MWInput::A_QuickKey9,
- "QuickKey10", MWInput::A_QuickKey10,
- "QuickKeysMenu", MWInput::A_QuickKeysMenu,
-
- "ToggleHUD", MWInput::A_ToggleHUD,
- "ToggleDebug", MWInput::A_ToggleDebug,
-
- "ZoomIn", MWInput::A_ZoomIn,
- "ZoomOut", MWInput::A_ZoomOut
- ));
-
- api["CONTROL_SWITCH"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with(
- "Controls", "playercontrols",
- "Fighting", "playerfighting",
- "Jumping", "playerjumping",
- "Looking", "playerlooking",
- "Magic", "playermagic",
- "ViewMode", "playerviewswitch",
- "VanityMode", "vanitymode"
- ));
-
- api["CONTROLLER_BUTTON"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with(
- "A", SDL_CONTROLLER_BUTTON_A,
- "B", SDL_CONTROLLER_BUTTON_B,
- "X", SDL_CONTROLLER_BUTTON_X,
- "Y", SDL_CONTROLLER_BUTTON_Y,
- "Back", SDL_CONTROLLER_BUTTON_BACK,
- "Guide", SDL_CONTROLLER_BUTTON_GUIDE,
- "Start", SDL_CONTROLLER_BUTTON_START,
- "LeftStick", SDL_CONTROLLER_BUTTON_LEFTSTICK,
- "RightStick", SDL_CONTROLLER_BUTTON_RIGHTSTICK,
- "LeftShoulder", SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
- "RightShoulder", SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
- "DPadUp", SDL_CONTROLLER_BUTTON_DPAD_UP,
- "DPadDown", SDL_CONTROLLER_BUTTON_DPAD_DOWN,
- "DPadLeft", SDL_CONTROLLER_BUTTON_DPAD_LEFT,
- "DPadRight", SDL_CONTROLLER_BUTTON_DPAD_RIGHT
- ));
-
- api["CONTROLLER_AXIS"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with(
- "LeftX", SDL_CONTROLLER_AXIS_LEFTX,
- "LeftY", SDL_CONTROLLER_AXIS_LEFTY,
- "RightX", SDL_CONTROLLER_AXIS_RIGHTX,
- "RightY", SDL_CONTROLLER_AXIS_RIGHTY,
- "TriggerLeft", SDL_CONTROLLER_AXIS_TRIGGERLEFT,
- "TriggerRight", SDL_CONTROLLER_AXIS_TRIGGERRIGHT,
-
- "LookUpDown", SDL_CONTROLLER_AXIS_MAX + MWInput::A_LookUpDown,
- "LookLeftRight", SDL_CONTROLLER_AXIS_MAX + MWInput::A_LookLeftRight,
- "MoveForwardBackward", SDL_CONTROLLER_AXIS_MAX + MWInput::A_MoveForwardBackward,
- "MoveLeftRight", SDL_CONTROLLER_AXIS_MAX + MWInput::A_MoveLeftRight
- ));
+ api["getControlSwitch"] = [input](std::string_view key) { return input->getControlSwitch(key); };
+ api["setControlSwitch"] = [input](std::string_view key, bool v) { input->toggleControlSwitch(key, v); };
+
+ api["getKeyName"] = [](SDL_Scancode code) {
+ return SDL_GetKeyName(SDL_GetKeyFromScancode(code));
+ };
+
+ api["ACTION"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs<std::string_view, MWInput::Actions>({
+ {"GameMenu", MWInput::A_GameMenu},
+ {"Screenshot", MWInput::A_Screenshot},
+ {"Inventory", MWInput::A_Inventory},
+ {"Console", MWInput::A_Console},
+
+ {"MoveLeft", MWInput::A_MoveLeft},
+ {"MoveRight", MWInput::A_MoveRight},
+ {"MoveForward", MWInput::A_MoveForward},
+ {"MoveBackward", MWInput::A_MoveBackward},
+
+ {"Activate", MWInput::A_Activate},
+ {"Use", MWInput::A_Use},
+ {"Jump", MWInput::A_Jump},
+ {"AutoMove", MWInput::A_AutoMove},
+ {"Rest", MWInput::A_Rest},
+ {"Journal", MWInput::A_Journal},
+ {"Weapon", MWInput::A_Weapon},
+ {"Spell", MWInput::A_Spell},
+ {"Run", MWInput::A_Run},
+ {"CycleSpellLeft", MWInput::A_CycleSpellLeft},
+ {"CycleSpellRight", MWInput::A_CycleSpellRight},
+ {"CycleWeaponLeft", MWInput::A_CycleWeaponLeft},
+ {"CycleWeaponRight", MWInput::A_CycleWeaponRight},
+ {"ToggleSneak", MWInput::A_ToggleSneak},
+ {"AlwaysRun", MWInput::A_AlwaysRun},
+ {"Sneak", MWInput::A_Sneak},
+
+ {"QuickSave", MWInput::A_QuickSave},
+ {"QuickLoad", MWInput::A_QuickLoad},
+ {"QuickMenu", MWInput::A_QuickMenu},
+ {"ToggleWeapon", MWInput::A_ToggleWeapon},
+ {"ToggleSpell", MWInput::A_ToggleSpell},
+ {"TogglePOV", MWInput::A_TogglePOV},
+
+ {"QuickKey1", MWInput::A_QuickKey1},
+ {"QuickKey2", MWInput::A_QuickKey2},
+ {"QuickKey3", MWInput::A_QuickKey3},
+ {"QuickKey4", MWInput::A_QuickKey4},
+ {"QuickKey5", MWInput::A_QuickKey5},
+ {"QuickKey6", MWInput::A_QuickKey6},
+ {"QuickKey7", MWInput::A_QuickKey7},
+ {"QuickKey8", MWInput::A_QuickKey8},
+ {"QuickKey9", MWInput::A_QuickKey9},
+ {"QuickKey10", MWInput::A_QuickKey10},
+ {"QuickKeysMenu", MWInput::A_QuickKeysMenu},
+
+ {"ToggleHUD", MWInput::A_ToggleHUD},
+ {"ToggleDebug", MWInput::A_ToggleDebug},
+
+ {"ZoomIn", MWInput::A_ZoomIn},
+ {"ZoomOut", MWInput::A_ZoomOut}
+ }));
+
+ api["CONTROL_SWITCH"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs<std::string_view, std::string_view>({
+ {"Controls", "playercontrols"},
+ {"Fighting", "playerfighting"},
+ {"Jumping", "playerjumping"},
+ {"Looking", "playerlooking"},
+ {"Magic", "playermagic"},
+ {"ViewMode", "playerviewswitch"},
+ {"VanityMode", "vanitymode"}
+ }));
+
+ api["CONTROLLER_BUTTON"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs<std::string_view, SDL_GameControllerButton>({
+ {"A", SDL_CONTROLLER_BUTTON_A},
+ {"B", SDL_CONTROLLER_BUTTON_B},
+ {"X", SDL_CONTROLLER_BUTTON_X},
+ {"Y", SDL_CONTROLLER_BUTTON_Y},
+ {"Back", SDL_CONTROLLER_BUTTON_BACK},
+ {"Guide", SDL_CONTROLLER_BUTTON_GUIDE},
+ {"Start", SDL_CONTROLLER_BUTTON_START},
+ {"LeftStick", SDL_CONTROLLER_BUTTON_LEFTSTICK},
+ {"RightStick", SDL_CONTROLLER_BUTTON_RIGHTSTICK},
+ {"LeftShoulder", SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
+ {"RightShoulder", SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
+ {"DPadUp", SDL_CONTROLLER_BUTTON_DPAD_UP},
+ {"DPadDown", SDL_CONTROLLER_BUTTON_DPAD_DOWN},
+ {"DPadLeft", SDL_CONTROLLER_BUTTON_DPAD_LEFT},
+ {"DPadRight", SDL_CONTROLLER_BUTTON_DPAD_RIGHT}
+ }));
+
+ api["CONTROLLER_AXIS"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs<std::string_view, int>({
+ {"LeftX", SDL_CONTROLLER_AXIS_LEFTX},
+ {"LeftY", SDL_CONTROLLER_AXIS_LEFTY},
+ {"RightX", SDL_CONTROLLER_AXIS_RIGHTX},
+ {"RightY", SDL_CONTROLLER_AXIS_RIGHTY},
+ {"TriggerLeft", SDL_CONTROLLER_AXIS_TRIGGERLEFT},
+ {"TriggerRight", SDL_CONTROLLER_AXIS_TRIGGERRIGHT},
+
+ {"LookUpDown", SDL_CONTROLLER_AXIS_MAX + MWInput::A_LookUpDown},
+ {"LookLeftRight", SDL_CONTROLLER_AXIS_MAX + MWInput::A_LookLeftRight},
+ {"MoveForwardBackward", SDL_CONTROLLER_AXIS_MAX + MWInput::A_MoveForwardBackward},
+ {"MoveLeftRight", SDL_CONTROLLER_AXIS_MAX + MWInput::A_MoveLeftRight}
+ }));
+
+ api["KEY"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs<std::string_view, SDL_Scancode>({
+ {"_0", SDL_SCANCODE_0},
+ {"_1", SDL_SCANCODE_1},
+ {"_2", SDL_SCANCODE_2},
+ {"_3", SDL_SCANCODE_3},
+ {"_4", SDL_SCANCODE_4},
+ {"_5", SDL_SCANCODE_5},
+ {"_6", SDL_SCANCODE_6},
+ {"_7", SDL_SCANCODE_7},
+ {"_8", SDL_SCANCODE_8},
+ {"_9", SDL_SCANCODE_9},
+
+ {"NP_0", SDL_SCANCODE_KP_0},
+ {"NP_1", SDL_SCANCODE_KP_1},
+ {"NP_2", SDL_SCANCODE_KP_2},
+ {"NP_3", SDL_SCANCODE_KP_3},
+ {"NP_4", SDL_SCANCODE_KP_4},
+ {"NP_5", SDL_SCANCODE_KP_5},
+ {"NP_6", SDL_SCANCODE_KP_6},
+ {"NP_7", SDL_SCANCODE_KP_7},
+ {"NP_8", SDL_SCANCODE_KP_8},
+ {"NP_9", SDL_SCANCODE_KP_9},
+ {"NP_Divide", SDL_SCANCODE_KP_DIVIDE},
+ {"NP_Enter", SDL_SCANCODE_KP_ENTER},
+ {"NP_Minus", SDL_SCANCODE_KP_MINUS},
+ {"NP_Multiply", SDL_SCANCODE_KP_MULTIPLY},
+ {"NP_Delete", SDL_SCANCODE_KP_PERIOD},
+ {"NP_Plus", SDL_SCANCODE_KP_PLUS},
+
+ {"F1", SDL_SCANCODE_F1},
+ {"F2", SDL_SCANCODE_F2},
+ {"F3", SDL_SCANCODE_F3},
+ {"F4", SDL_SCANCODE_F4},
+ {"F5", SDL_SCANCODE_F5},
+ {"F6", SDL_SCANCODE_F6},
+ {"F7", SDL_SCANCODE_F7},
+ {"F8", SDL_SCANCODE_F8},
+ {"F9", SDL_SCANCODE_F9},
+ {"F10", SDL_SCANCODE_F10},
+ {"F11", SDL_SCANCODE_F11},
+ {"F12", SDL_SCANCODE_F12},
+
+ {"A", SDL_SCANCODE_A},
+ {"B", SDL_SCANCODE_B},
+ {"C", SDL_SCANCODE_C},
+ {"D", SDL_SCANCODE_D},
+ {"E", SDL_SCANCODE_E},
+ {"F", SDL_SCANCODE_F},
+ {"G", SDL_SCANCODE_G},
+ {"H", SDL_SCANCODE_H},
+ {"I", SDL_SCANCODE_I},
+ {"J", SDL_SCANCODE_J},
+ {"K", SDL_SCANCODE_K},
+ {"L", SDL_SCANCODE_L},
+ {"M", SDL_SCANCODE_M},
+ {"N", SDL_SCANCODE_N},
+ {"O", SDL_SCANCODE_O},
+ {"P", SDL_SCANCODE_P},
+ {"Q", SDL_SCANCODE_Q},
+ {"R", SDL_SCANCODE_R},
+ {"S", SDL_SCANCODE_S},
+ {"T", SDL_SCANCODE_T},
+ {"U", SDL_SCANCODE_U},
+ {"V", SDL_SCANCODE_V},
+ {"W", SDL_SCANCODE_W},
+ {"X", SDL_SCANCODE_X},
+ {"Y", SDL_SCANCODE_Y},
+ {"Z", SDL_SCANCODE_Z},
+
+ {"LeftArrow", SDL_SCANCODE_LEFT},
+ {"RightArrow", SDL_SCANCODE_RIGHT},
+ {"UpArrow", SDL_SCANCODE_UP},
+ {"DownArrow", SDL_SCANCODE_DOWN},
+
+ {"LeftAlt", SDL_SCANCODE_LALT},
+ {"LeftCtrl", SDL_SCANCODE_LCTRL},
+ {"LeftBracket", SDL_SCANCODE_LEFTBRACKET},
+ {"LeftSuper", SDL_SCANCODE_LGUI},
+ {"LeftShift", SDL_SCANCODE_LSHIFT},
+ {"RightAlt", SDL_SCANCODE_RALT},
+ {"RightCtrl", SDL_SCANCODE_RCTRL},
+ {"RightSuper", SDL_SCANCODE_RGUI},
+ {"RightBracket", SDL_SCANCODE_RIGHTBRACKET},
+ {"RightShift", SDL_SCANCODE_RSHIFT},
+
+ {"Apostrophe", SDL_SCANCODE_APOSTROPHE},
+ {"BackSlash", SDL_SCANCODE_BACKSLASH},
+ {"Backspace", SDL_SCANCODE_BACKSPACE},
+ {"CapsLock", SDL_SCANCODE_CAPSLOCK},
+ {"Comma", SDL_SCANCODE_COMMA},
+ {"Delete", SDL_SCANCODE_DELETE},
+ {"End", SDL_SCANCODE_END},
+ {"Enter", SDL_SCANCODE_RETURN},
+ {"Equals", SDL_SCANCODE_EQUALS},
+ {"Escape", SDL_SCANCODE_ESCAPE},
+ {"Home", SDL_SCANCODE_HOME},
+ {"Insert", SDL_SCANCODE_INSERT},
+ {"Minus", SDL_SCANCODE_MINUS},
+ {"NumLock", SDL_SCANCODE_NUMLOCKCLEAR},
+ {"PageDown", SDL_SCANCODE_PAGEDOWN},
+ {"PageUp", SDL_SCANCODE_PAGEUP},
+ {"Period", SDL_SCANCODE_PERIOD},
+ {"Pause", SDL_SCANCODE_PAUSE},
+ {"PrintScreen", SDL_SCANCODE_PRINTSCREEN},
+ {"ScrollLock", SDL_SCANCODE_SCROLLLOCK},
+ {"Semicolon", SDL_SCANCODE_SEMICOLON},
+ {"Slash", SDL_SCANCODE_SLASH},
+ {"Space", SDL_SCANCODE_SPACE},
+ {"Tab", SDL_SCANCODE_TAB}
+ }));
return LuaUtil::makeReadOnly(api);
}
diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp
index ee23b4b90c..98cf0a0b1a 100644
--- a/apps/openmw/mwlua/localscripts.cpp
+++ b/apps/openmw/mwlua/localscripts.cpp
@@ -39,7 +39,7 @@ namespace MWLua
selfAPI["controls"] = sol::readonly_property([](SelfObject& self) { return &self.mControls; });
selfAPI["isActive"] = [](SelfObject& self) { return &self.mIsActive; };
selfAPI["enableAI"] = [](SelfObject& self, bool v) { self.mControls.mDisableAI = !v; };
- selfAPI["setEquipment"] = [manager=context.mLuaManager](const SelfObject& obj, sol::table equipment)
+ selfAPI["setEquipment"] = [context](const SelfObject& obj, sol::table equipment)
{
if (!obj.ptr().getClass().hasInventoryStore(obj.ptr()))
{
@@ -56,7 +56,7 @@ namespace MWLua
else
eqp[slot] = value.as<std::string>();
}
- manager->addAction(std::make_unique<SetEquipmentAction>(obj.id(), std::move(eqp)));
+ context.mLuaManager->addAction(std::make_unique<SetEquipmentAction>(context.mLua, obj.id(), std::move(eqp)));
};
selfAPI["getCombatTarget"] = [worldView=context.mWorldView](SelfObject& self) -> sol::optional<LObject>
{
diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp
index 1c05debc73..cf60e00728 100644
--- a/apps/openmw/mwlua/luabindings.cpp
+++ b/apps/openmw/mwlua/luabindings.cpp
@@ -1,11 +1,14 @@
#include "luabindings.hpp"
#include <components/lua/luastate.hpp>
+#include <components/lua/i18n.hpp>
#include <components/queries/luabindings.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/statemanager.hpp"
+#include "../mwworld/esmstore.hpp"
#include "../mwworld/inventorystore.hpp"
+#include "../mwworld/store.hpp"
#include "eventqueue.hpp"
#include "worldview.hpp"
@@ -21,49 +24,80 @@ namespace MWLua
return LuaUtil::makeReadOnly(res);
}
+ static void addTimeBindings(sol::table& api, const Context& context, bool global)
+ {
+ api["getSimulationTime"] = [world=context.mWorldView]() { return world->getSimulationTime(); };
+ api["getSimulationTimeScale"] = [world=context.mWorldView]() { return world->getSimulationTimeScale(); };
+ api["getGameTime"] = [world=context.mWorldView]() { return world->getGameTime(); };
+ api["getGameTimeScale"] = [world=context.mWorldView]() { return world->getGameTimeScale(); };
+ api["isWorldPaused"] = [world=context.mWorldView]() { return world->isPaused(); };
+
+ if (!global)
+ return;
+
+ api["setGameTimeScale"] = [world=context.mWorldView](double scale) { world->setGameTimeScale(scale); };
+
+ // TODO: Ability to make game time slower or faster than real time (needed for example for mechanics like VATS)
+ // api["setSimulationTimeScale"] = [](double scale) {};
+
+ // TODO: Ability to pause/resume world from Lua (needed for UI dehardcoding)
+ // api["pause"] = []() {};
+ // api["resume"] = []() {};
+ }
+
sol::table initCorePackage(const Context& context)
{
auto* lua = context.mLua;
sol::table api(lua->sol(), sol::create);
- api["API_REVISION"] = 8;
+ api["API_REVISION"] = 14;
api["quit"] = [lua]()
{
- std::string traceback = lua->sol()["debug"]["traceback"]().get<std::string>();
- Log(Debug::Warning) << "Quit requested by a Lua script.\n" << traceback;
+ Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback();
MWBase::Environment::get().getStateManager()->requestQuit();
};
api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData)
{
context.mGlobalEventQueue->push_back({std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer)});
};
- api["getGameTimeInSeconds"] = [world=context.mWorldView]() { return world->getGameTimeInSeconds(); };
- api["getGameTimeInHours"] = [world=context.mWorldView]() { return world->getGameTimeInHours(); };
+ addTimeBindings(api, context, false);
api["OBJECT_TYPE"] = definitionList(*lua,
{
"Activator", "Armor", "Book", "Clothing", "Creature", "Door", "Ingredient",
"Light", "Miscellaneous", "NPC", "Player", "Potion", "Static", "Weapon"
});
- api["EQUIPMENT_SLOT"] = LuaUtil::makeReadOnly(lua->sol().create_table_with(
- "Helmet", MWWorld::InventoryStore::Slot_Helmet,
- "Cuirass", MWWorld::InventoryStore::Slot_Cuirass,
- "Greaves", MWWorld::InventoryStore::Slot_Greaves,
- "LeftPauldron", MWWorld::InventoryStore::Slot_LeftPauldron,
- "RightPauldron", MWWorld::InventoryStore::Slot_RightPauldron,
- "LeftGauntlet", MWWorld::InventoryStore::Slot_LeftGauntlet,
- "RightGauntlet", MWWorld::InventoryStore::Slot_RightGauntlet,
- "Boots", MWWorld::InventoryStore::Slot_Boots,
- "Shirt", MWWorld::InventoryStore::Slot_Shirt,
- "Pants", MWWorld::InventoryStore::Slot_Pants,
- "Skirt", MWWorld::InventoryStore::Slot_Skirt,
- "Robe", MWWorld::InventoryStore::Slot_Robe,
- "LeftRing", MWWorld::InventoryStore::Slot_LeftRing,
- "RightRing", MWWorld::InventoryStore::Slot_RightRing,
- "Amulet", MWWorld::InventoryStore::Slot_Amulet,
- "Belt", MWWorld::InventoryStore::Slot_Belt,
- "CarriedRight", MWWorld::InventoryStore::Slot_CarriedRight,
- "CarriedLeft", MWWorld::InventoryStore::Slot_CarriedLeft,
- "Ammunition", MWWorld::InventoryStore::Slot_Ammunition
- ));
+ api["EQUIPMENT_SLOT"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs<std::string_view, int>({
+ {"Helmet", MWWorld::InventoryStore::Slot_Helmet},
+ {"Cuirass", MWWorld::InventoryStore::Slot_Cuirass},
+ {"Greaves", MWWorld::InventoryStore::Slot_Greaves},
+ {"LeftPauldron", MWWorld::InventoryStore::Slot_LeftPauldron},
+ {"RightPauldron", MWWorld::InventoryStore::Slot_RightPauldron},
+ {"LeftGauntlet", MWWorld::InventoryStore::Slot_LeftGauntlet},
+ {"RightGauntlet", MWWorld::InventoryStore::Slot_RightGauntlet},
+ {"Boots", MWWorld::InventoryStore::Slot_Boots},
+ {"Shirt", MWWorld::InventoryStore::Slot_Shirt},
+ {"Pants", MWWorld::InventoryStore::Slot_Pants},
+ {"Skirt", MWWorld::InventoryStore::Slot_Skirt},
+ {"Robe", MWWorld::InventoryStore::Slot_Robe},
+ {"LeftRing", MWWorld::InventoryStore::Slot_LeftRing},
+ {"RightRing", MWWorld::InventoryStore::Slot_RightRing},
+ {"Amulet", MWWorld::InventoryStore::Slot_Amulet},
+ {"Belt", MWWorld::InventoryStore::Slot_Belt},
+ {"CarriedRight", MWWorld::InventoryStore::Slot_CarriedRight},
+ {"CarriedLeft", MWWorld::InventoryStore::Slot_CarriedLeft},
+ {"Ammunition", MWWorld::InventoryStore::Slot_Ammunition}
+ }));
+ api["i18n"] = [i18n=context.mI18n](const std::string& context) { return i18n->getContext(context); };
+ const MWWorld::Store<ESM::GameSetting>* gmst = &MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
+ api["getGMST"] = [lua=context.mLua, gmst](const std::string& setting) -> sol::object
+ {
+ const ESM::Variant& value = gmst->find(setting)->mValue;
+ if (value.getType() == ESM::VT_String)
+ return sol::make_object<std::string>(lua->sol(), value.getString());
+ else if (value.getType() == ESM::VT_Int)
+ return sol::make_object<int>(lua->sol(), value.getInteger());
+ else
+ return sol::make_object<float>(lua->sol(), value.getFloat());
+ };
return LuaUtil::makeReadOnly(api);
}
@@ -71,6 +105,7 @@ namespace MWLua
{
sol::table api(context.mLua->sol(), sol::create);
WorldView* worldView = context.mWorldView;
+ addTimeBindings(api, context, true);
api["getCellByName"] = [worldView=context.mWorldView](const std::string& name) -> sol::optional<GCell>
{
MWWorld::CellStore* cell = worldView->findNamedCell(name);
@@ -141,5 +176,29 @@ namespace MWLua
return LuaUtil::makeReadOnly(res);
}
+ sol::table initGlobalStoragePackage(const Context& context, LuaUtil::LuaStorage* globalStorage)
+ {
+ sol::table res(context.mLua->sol(), sol::create);
+ res["globalSection"] = [globalStorage](std::string_view section) { return globalStorage->getMutableSection(section); };
+ res["allGlobalSections"] = [globalStorage]() { return globalStorage->getAllSections(); };
+ return LuaUtil::makeReadOnly(res);
+ }
+
+ sol::table initLocalStoragePackage(const Context& context, LuaUtil::LuaStorage* globalStorage)
+ {
+ sol::table res(context.mLua->sol(), sol::create);
+ res["globalSection"] = [globalStorage](std::string_view section) { return globalStorage->getReadOnlySection(section); };
+ return LuaUtil::makeReadOnly(res);
+ }
+
+ sol::table initPlayerStoragePackage(const Context& context, LuaUtil::LuaStorage* globalStorage, LuaUtil::LuaStorage* playerStorage)
+ {
+ sol::table res(context.mLua->sol(), sol::create);
+ res["globalSection"] = [globalStorage](std::string_view section) { return globalStorage->getReadOnlySection(section); };
+ res["playerSection"] = [playerStorage](std::string_view section) { return playerStorage->getMutableSection(section); };
+ res["allPlayerSections"] = [playerStorage]() { return playerStorage->getAllSections(); };
+ return LuaUtil::makeReadOnly(res);
+ }
+
}
diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp
index aad3183734..cfaf50c4f6 100644
--- a/apps/openmw/mwlua/luabindings.hpp
+++ b/apps/openmw/mwlua/luabindings.hpp
@@ -4,6 +4,7 @@
#include <components/lua/luastate.hpp>
#include <components/lua/serialization.hpp>
#include <components/lua/scriptscontainer.hpp>
+#include <components/lua/storage.hpp>
#include "context.hpp"
#include "eventqueue.hpp"
@@ -25,6 +26,10 @@ namespace MWLua
sol::table initFieldGroup(const Context&, const QueryFieldGroup&);
+ sol::table initGlobalStoragePackage(const Context&, LuaUtil::LuaStorage* globalStorage);
+ sol::table initLocalStoragePackage(const Context&, LuaUtil::LuaStorage* globalStorage);
+ sol::table initPlayerStoragePackage(const Context&, LuaUtil::LuaStorage* globalStorage, LuaUtil::LuaStorage* playerStorage);
+
// Implemented in nearbybindings.cpp
sol::table initNearbyPackage(const Context&);
@@ -58,13 +63,13 @@ namespace MWLua
// Implemented in uibindings.cpp
sol::table initUserInterfacePackage(const Context&);
+ void clearUserInterface();
// Implemented in inputbindings.cpp
sol::table initInputPackage(const Context&);
// Implemented in settingsbindings.cpp
sol::table initGlobalSettingsPackage(const Context&);
- sol::table initLocalSettingsPackage(const Context&);
sol::table initPlayerSettingsPackage(const Context&);
// openmw.self package is implemented in localscripts.cpp
diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp
index 4e47bf7167..fc6dc86285 100644
--- a/apps/openmw/mwlua/luamanagerimp.cpp
+++ b/apps/openmw/mwlua/luamanagerimp.cpp
@@ -1,11 +1,15 @@
#include "luamanagerimp.hpp"
+#include <filesystem>
+
#include <components/debug/debuglog.hpp>
#include <components/esm/esmreader.hpp>
#include <components/esm/esmwriter.hpp>
#include <components/esm/luascripts.hpp>
+#include <components/settings/settings.hpp>
+
#include <components/lua/utilpackage.hpp>
#include "../mwbase/windowmanager.hpp"
@@ -20,9 +24,10 @@
namespace MWLua
{
- LuaManager::LuaManager(const VFS::Manager* vfs) : mLua(vfs, &mConfiguration)
+ LuaManager::LuaManager(const VFS::Manager* vfs, const std::string& libsDir) : mLua(vfs, &mConfiguration), mI18n(vfs, &mLua)
{
Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion();
+ mLua.addInternalLibSearchPath(libsDir);
mGlobalSerializer = createUserdataSerializer(false, mWorldView.getObjectRegistry());
mLocalSerializer = createUserdataSerializer(true, mWorldView.getObjectRegistry());
@@ -46,6 +51,7 @@ namespace MWLua
context.mIsGlobal = true;
context.mLuaManager = this;
context.mLua = &mLua;
+ context.mI18n = &mI18n;
context.mWorldView = &mWorldView;
context.mLocalEventQueue = &mLocalEvents;
context.mGlobalEventQueue = &mGlobalEvents;
@@ -55,11 +61,17 @@ namespace MWLua
localContext.mIsGlobal = false;
localContext.mSerializer = mLocalSerializer.get();
+ mI18n.init();
+ std::vector<std::string> preferredLanguages;
+ Misc::StringUtils::split(Settings::Manager::getString("i18n preferred languages", "Lua"), preferredLanguages, ", ");
+ mI18n.setPreferredLanguages(preferredLanguages);
+
initObjectBindingsForGlobalScripts(context);
initCellBindingsForGlobalScripts(context);
initObjectBindingsForLocalScripts(localContext);
initCellBindingsForLocalScripts(localContext);
LocalScripts::initializeSelfPackage(localContext);
+ LuaUtil::LuaStorage::initLuaBindings(mLua.sol());
mLua.addCommonPackage("openmw.async", getAsyncPackageInitializer(context));
mLua.addCommonPackage("openmw.util", LuaUtil::initUtilPackage(mLua.sol()));
@@ -67,53 +79,70 @@ namespace MWLua
mLua.addCommonPackage("openmw.query", initQueryPackage(context));
mGlobalScripts.addPackage("openmw.world", initWorldPackage(context));
mGlobalScripts.addPackage("openmw.settings", initGlobalSettingsPackage(context));
+ mGlobalScripts.addPackage("openmw.storage", initGlobalStoragePackage(context, &mGlobalStorage));
mCameraPackage = initCameraPackage(localContext);
mUserInterfacePackage = initUserInterfacePackage(localContext);
mInputPackage = initInputPackage(localContext);
mNearbyPackage = initNearbyPackage(localContext);
- mLocalSettingsPackage = initLocalSettingsPackage(localContext);
+ mLocalSettingsPackage = initGlobalSettingsPackage(localContext);
mPlayerSettingsPackage = initPlayerSettingsPackage(localContext);
+ mLocalStoragePackage = initLocalStoragePackage(localContext, &mGlobalStorage);
+ mPlayerStoragePackage = initPlayerStoragePackage(localContext, &mGlobalStorage, &mPlayerStorage);
initConfiguration();
mInitialized = true;
}
- void LuaManager::update(bool paused, float dt)
+ void LuaManager::loadPermanentStorage(const std::string& userConfigPath)
+ {
+ auto globalPath = std::filesystem::path(userConfigPath) / "global_storage.bin";
+ auto playerPath = std::filesystem::path(userConfigPath) / "player_storage.bin";
+ if (std::filesystem::exists(globalPath))
+ mGlobalStorage.load(globalPath.string());
+ if (std::filesystem::exists(playerPath))
+ mPlayerStorage.load(playerPath.string());
+ }
+
+ void LuaManager::savePermanentStorage(const std::string& userConfigPath)
{
+ std::filesystem::path confDir(userConfigPath);
+ mGlobalStorage.save((confDir / "global_storage.bin").string());
+ mPlayerStorage.save((confDir / "player_storage.bin").string());
+ }
+
+ void LuaManager::update()
+ {
+ if (mPlayer.isEmpty())
+ return; // The game is not started yet.
+
+ float frameDuration = MWBase::Environment::get().getFrameDuration();
ObjectRegistry* objectRegistry = mWorldView.getObjectRegistry();
- if (!mPlayer.isEmpty())
+ MWWorld::Ptr newPlayerPtr = MWBase::Environment::get().getWorld()->getPlayerPtr();
+ if (!(getId(mPlayer) == getId(newPlayerPtr)))
+ throw std::logic_error("Player Refnum was changed unexpectedly");
+ if (!mPlayer.isInCell() || !newPlayerPtr.isInCell() || mPlayer.getCell() != newPlayerPtr.getCell())
{
- MWWorld::Ptr newPlayerPtr = MWBase::Environment::get().getWorld()->getPlayerPtr();
- if (!(getId(mPlayer) == getId(newPlayerPtr)))
- throw std::logic_error("Player Refnum was changed unexpectedly");
- if (!mPlayer.isInCell() || !newPlayerPtr.isInCell() || mPlayer.getCell() != newPlayerPtr.getCell())
- {
- mPlayer = newPlayerPtr; // player was moved to another cell, update ptr in registry
- objectRegistry->registerPtr(mPlayer);
- }
+ mPlayer = newPlayerPtr; // player was moved to another cell, update ptr in registry
+ objectRegistry->registerPtr(mPlayer);
}
- mWorldView.update();
- if (paused)
- {
- mInputEvents.clear();
- return;
- }
+ mWorldView.update();
std::vector<GlobalEvent> globalEvents = std::move(mGlobalEvents);
std::vector<LocalEvent> localEvents = std::move(mLocalEvents);
mGlobalEvents = std::vector<GlobalEvent>();
mLocalEvents = std::vector<LocalEvent>();
+ if (!mWorldView.isPaused())
{ // Update time and process timers
- double seconds = mWorldView.getGameTimeInSeconds() + dt;
- mWorldView.setGameTimeInSeconds(seconds);
- double hours = mWorldView.getGameTimeInHours();
+ double simulationTime = mWorldView.getSimulationTime() + frameDuration;
+ mWorldView.setSimulationTime(simulationTime);
+ double gameTime = mWorldView.getGameTime();
- mGlobalScripts.processTimers(seconds, hours);
+ mGlobalScripts.processTimers(simulationTime, gameTime);
for (LocalScripts* scripts : mActiveLocalScripts)
- scripts->processTimers(seconds, hours);
+ scripts->processTimers(simulationTime, gameTime);
}
// Receive events
@@ -136,14 +165,6 @@ namespace MWLua
mQueuedCallbacks.clear();
// Engine handlers in local scripts
- PlayerScripts* playerScripts = dynamic_cast<PlayerScripts*>(mPlayer.getRefData().getLuaScripts());
- if (playerScripts)
- {
- for (const auto& event : mInputEvents)
- playerScripts->processInputEvent(event);
- }
- mInputEvents.clear();
-
for (const LocalEngineEvent& e : mLocalEngineEvents)
{
LObject obj(e.mDest, objectRegistry);
@@ -158,8 +179,11 @@ namespace MWLua
}
mLocalEngineEvents.clear();
- for (LocalScripts* scripts : mActiveLocalScripts)
- scripts->update(dt);
+ if (!mWorldView.isPaused())
+ {
+ for (LocalScripts* scripts : mActiveLocalScripts)
+ scripts->update(frameDuration);
+ }
// Engine handlers in global scripts
if (mPlayerChanged)
@@ -177,22 +201,37 @@ namespace MWLua
mGlobalScripts.actorActive(GObject(id, objectRegistry));
mActorAddedEvents.clear();
- mGlobalScripts.update(dt);
+ if (!mWorldView.isPaused())
+ mGlobalScripts.update(frameDuration);
}
- void LuaManager::applyQueuedChanges()
+ void LuaManager::synchronizedUpdate()
{
+ if (mPlayer.isEmpty())
+ return; // The game is not started yet.
+
+ // We apply input events in `synchronizedUpdate` rather than in `update` in order to reduce input latency.
+ PlayerScripts* playerScripts = dynamic_cast<PlayerScripts*>(mPlayer.getRefData().getLuaScripts());
+ if (playerScripts && !MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu))
+ {
+ for (const auto& event : mInputEvents)
+ playerScripts->processInputEvent(event);
+ }
+ mInputEvents.clear();
+ if (playerScripts && !mWorldView.isPaused())
+ playerScripts->inputUpdate(MWBase::Environment::get().getFrameDuration());
+
MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager();
for (const std::string& message : mUIMessages)
windowManager->messageBox(message);
mUIMessages.clear();
for (std::unique_ptr<Action>& action : mActionQueue)
- action->apply(mWorldView);
+ action->safeApply(mWorldView);
mActionQueue.clear();
if (mTeleportPlayerAction)
- mTeleportPlayerAction->apply(mWorldView);
+ mTeleportPlayerAction->safeApply(mWorldView);
mTeleportPlayerAction.reset();
}
@@ -215,6 +254,9 @@ namespace MWLua
mPlayer.getRefData().setLuaScripts(nullptr);
mPlayer = MWWorld::Ptr();
}
+ clearUserInterface();
+ mGlobalStorage.clearTemporary();
+ mPlayerStorage.clearTemporary();
}
void LuaManager::setupPlayer(const MWWorld::Ptr& ptr)
@@ -227,7 +269,10 @@ namespace MWLua
mPlayer = ptr;
LocalScripts* localScripts = ptr.getRefData().getLuaScripts();
if (!localScripts)
+ {
localScripts = createLocalScripts(ptr, ESM::LuaScriptCfg::sPlayer);
+ localScripts->addAutoStartedScripts();
+ }
mActiveLocalScripts.insert(localScripts);
mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}});
mPlayerChanged = true;
@@ -257,7 +302,10 @@ namespace MWLua
{
ESM::LuaScriptCfg::Flags flag = getLuaScriptFlag(ptr);
if (!mConfiguration.getListByFlag(flag).empty())
- localScripts = createLocalScripts(ptr, flag); // TODO: put to a queue and apply on next `update()`
+ {
+ localScripts = createLocalScripts(ptr, flag);
+ localScripts->addAutoStartedScripts(); // TODO: put to a queue and apply on next `update()`
+ }
}
if (localScripts)
{
@@ -310,6 +358,7 @@ namespace MWLua
if (!localScripts)
{
localScripts = createLocalScripts(ptr, getLuaScriptFlag(ptr));
+ localScripts->addAutoStartedScripts();
if (ptr.isInCell() && MWBase::Environment::get().getWorld()->isCellActive(ptr.getCell()))
mActiveLocalScripts.insert(localScripts);
}
@@ -329,15 +378,16 @@ namespace MWLua
scripts->addPackage("openmw.camera", mCameraPackage);
scripts->addPackage("openmw.input", mInputPackage);
scripts->addPackage("openmw.settings", mPlayerSettingsPackage);
+ scripts->addPackage("openmw.storage", mPlayerStoragePackage);
}
else
{
scripts = std::make_shared<LocalScripts>(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()), flag);
scripts->addPackage("openmw.settings", mLocalSettingsPackage);
+ scripts->addPackage("openmw.storage", mLocalStoragePackage);
}
scripts->addPackage("openmw.nearby", mNearbyPackage);
scripts->setSerializer(mLocalSerializer.get());
- scripts->addAutoStartedScripts();
MWWorld::RefData& refData = ptr.getRefData();
refData.setLuaScripts(std::move(scripts));
@@ -420,6 +470,7 @@ namespace MWLua
continue;
ESM::LuaScripts data;
scripts->save(data);
+ clearUserInterface();
scripts->load(data);
}
for (LocalScripts* scripts : mActiveLocalScripts)
diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp
index f5ffe9d258..753cc6ca49 100644
--- a/apps/openmw/mwlua/luamanagerimp.hpp
+++ b/apps/openmw/mwlua/luamanagerimp.hpp
@@ -4,7 +4,9 @@
#include <map>
#include <set>
+#include <components/lua/i18n.hpp>
#include <components/lua/luastate.hpp>
+#include <components/lua/storage.hpp>
#include "../mwbase/luamanager.hpp"
@@ -22,17 +24,20 @@ namespace MWLua
class LuaManager : public MWBase::LuaManager
{
public:
- LuaManager(const VFS::Manager* vfs);
+ LuaManager(const VFS::Manager* vfs, const std::string& libsDir);
// Called by engine.cpp when the environment is fully initialized.
void init();
+ void loadPermanentStorage(const std::string& userConfigPath);
+ void savePermanentStorage(const std::string& userConfigPath);
+
// Called by engine.cpp every frame. For performance reasons it works in a separate
// thread (in parallel with osg Cull). Can not use scene graph.
- void update(bool paused, float dt);
+ void update();
// Called by engine.cpp from the main thread. Can use scene graph.
- void applyQueuedChanges();
+ void synchronizedUpdate();
// Available everywhere through the MWBase::LuaManager interface.
// LuaManager queues these events and propagates to scripts on the next `update` call.
@@ -91,12 +96,15 @@ namespace MWLua
bool mGlobalScriptsStarted = false;
LuaUtil::ScriptsConfiguration mConfiguration;
LuaUtil::LuaState mLua;
+ LuaUtil::I18nManager mI18n;
sol::table mNearbyPackage;
sol::table mUserInterfacePackage;
sol::table mCameraPackage;
sol::table mInputPackage;
sol::table mLocalSettingsPackage;
sol::table mPlayerSettingsPackage;
+ sol::table mLocalStoragePackage;
+ sol::table mPlayerStoragePackage;
GlobalScripts mGlobalScripts{&mLua};
std::set<LocalScripts*> mActiveLocalScripts;
@@ -137,6 +145,9 @@ namespace MWLua
std::vector<std::unique_ptr<Action>> mActionQueue;
std::unique_ptr<TeleportAction> mTeleportPlayerAction;
std::vector<std::string> mUIMessages;
+
+ LuaUtil::LuaStorage mGlobalStorage{mLua.sol()};
+ LuaUtil::LuaStorage mPlayerStorage{mLua.sol()};
};
}
diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp
index 011d0ae9f3..624cf69da6 100644
--- a/apps/openmw/mwlua/nearbybindings.cpp
+++ b/apps/openmw/mwlua/nearbybindings.cpp
@@ -47,14 +47,15 @@ namespace MWLua
return LObject(getId(r.mHitObject), worldView->getObjectRegistry());
});
- api["COLLISION_TYPE"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with(
- "World", MWPhysics::CollisionType_World,
- "Door", MWPhysics::CollisionType_Door,
- "Actor", MWPhysics::CollisionType_Actor,
- "HeightMap", MWPhysics::CollisionType_HeightMap,
- "Projectile", MWPhysics::CollisionType_Projectile,
- "Water", MWPhysics::CollisionType_Water,
- "Default", MWPhysics::CollisionType_Default));
+ api["COLLISION_TYPE"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs<std::string_view, MWPhysics::CollisionType>({
+ {"World", MWPhysics::CollisionType_World},
+ {"Door", MWPhysics::CollisionType_Door},
+ {"Actor", MWPhysics::CollisionType_Actor},
+ {"HeightMap", MWPhysics::CollisionType_HeightMap},
+ {"Projectile", MWPhysics::CollisionType_Projectile},
+ {"Water", MWPhysics::CollisionType_Water},
+ {"Default", MWPhysics::CollisionType_Default}
+ }));
api["castRay"] = [](const osg::Vec3f& from, const osg::Vec3f& to, sol::optional<sol::table> options)
{
diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp
index 2eceecc061..9857170b32 100644
--- a/apps/openmw/mwlua/objectbindings.cpp
+++ b/apps/openmw/mwlua/objectbindings.cpp
@@ -179,16 +179,16 @@ namespace MWLua
localScripts->removeScript(*scriptId);
};
- objectT["teleport"] = [luaManager=context.mLuaManager](const GObject& object, std::string_view cell,
- const osg::Vec3f& pos, const sol::optional<osg::Vec3f>& optRot)
+ objectT["teleport"] = [context](const GObject& object, std::string_view cell,
+ const osg::Vec3f& pos, const sol::optional<osg::Vec3f>& optRot)
{
MWWorld::Ptr ptr = object.ptr();
osg::Vec3f rot = optRot ? *optRot : ptr.getRefData().getPosition().asRotationVec3();
- auto action = std::make_unique<TeleportAction>(object.id(), std::string(cell), pos, rot);
+ auto action = std::make_unique<TeleportAction>(context.mLua, object.id(), std::string(cell), pos, rot);
if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr())
- luaManager->addTeleportPlayerAction(std::move(action));
+ context.mLuaManager->addTeleportPlayerAction(std::move(action));
else
- luaManager->addAction(std::move(action));
+ context.mLuaManager->addAction(std::move(action));
};
}
else
@@ -352,7 +352,7 @@ namespace MWLua
if constexpr (std::is_same_v<ObjectT, GObject>)
{ // Only for global scripts
- objectT["setEquipment"] = [manager=context.mLuaManager](const GObject& obj, sol::table equipment)
+ objectT["setEquipment"] = [context](const GObject& obj, sol::table equipment)
{
if (!obj.ptr().getClass().hasInventoryStore(obj.ptr()))
{
@@ -360,7 +360,8 @@ namespace MWLua
throw std::runtime_error(ptrToString(obj.ptr()) + " has no equipment slots");
return;
}
- manager->addAction(std::make_unique<SetEquipmentAction>(obj.id(), parseEquipmentTable(equipment)));
+ context.mLuaManager->addAction(std::make_unique<SetEquipmentAction>(
+ context.mLua, obj.id(), parseEquipmentTable(equipment)));
};
// TODO
diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp
index 0393a1375d..e8cdd120ac 100644
--- a/apps/openmw/mwlua/playerscripts.hpp
+++ b/apps/openmw/mwlua/playerscripts.hpp
@@ -17,7 +17,7 @@ namespace MWLua
{
registerEngineHandlers({&mKeyPressHandlers, &mKeyReleaseHandlers,
&mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers,
- &mActionHandlers});
+ &mActionHandlers, &mInputUpdateHandlers});
}
void processInputEvent(const MWBase::LuaManager::InputEvent& event)
@@ -43,12 +43,15 @@ namespace MWLua
}
}
+ void inputUpdate(float dt) { callEngineHandlers(mInputUpdateHandlers, dt); }
+
private:
EngineHandlerList mKeyPressHandlers{"onKeyPress"};
EngineHandlerList mKeyReleaseHandlers{"onKeyRelease"};
EngineHandlerList mControllerButtonPressHandlers{"onControllerButtonPress"};
EngineHandlerList mControllerButtonReleaseHandlers{"onControllerButtonRelease"};
EngineHandlerList mActionHandlers{"onInputAction"};
+ EngineHandlerList mInputUpdateHandlers{"onInputUpdate"};
};
}
diff --git a/apps/openmw/mwlua/settingsbindings.cpp b/apps/openmw/mwlua/settingsbindings.cpp
index 12dd69f73a..afd852b1a0 100644
--- a/apps/openmw/mwlua/settingsbindings.cpp
+++ b/apps/openmw/mwlua/settingsbindings.cpp
@@ -8,7 +8,7 @@
namespace MWLua
{
- static sol::table initSettingsPackage(const Context& context, bool /*global*/, bool player)
+ static sol::table initSettingsPackage(const Context& context, bool player)
{
LuaUtil::LuaState* lua = context.mLua;
sol::table config(lua->sol(), sol::create);
@@ -65,8 +65,7 @@ namespace MWLua
return LuaUtil::makeReadOnly(config);
}
- sol::table initGlobalSettingsPackage(const Context& context) { return initSettingsPackage(context, true, false); }
- sol::table initLocalSettingsPackage(const Context& context) { return initSettingsPackage(context, false, false); }
- sol::table initPlayerSettingsPackage(const Context& context) { return initSettingsPackage(context, false, true); }
+ sol::table initGlobalSettingsPackage(const Context& context) { return initSettingsPackage(context, false); }
+ sol::table initPlayerSettingsPackage(const Context& context) { return initSettingsPackage(context, true); }
}
diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp
index 4fae84cd40..987304d4c3 100644
--- a/apps/openmw/mwlua/uibindings.cpp
+++ b/apps/openmw/mwlua/uibindings.cpp
@@ -1,18 +1,254 @@
-#include "luabindings.hpp"
+#include <components/lua_ui/widgetlist.hpp>
+#include <components/lua_ui/element.hpp>
+#include <components/lua_ui/layers.hpp>
+#include <components/lua_ui/content.hpp>
+#include "context.hpp"
+#include "actions.hpp"
#include "luamanagerimp.hpp"
namespace MWLua
{
+ namespace
+ {
+ std::set<LuaUi::Element*> allElements;
+
+ class UiAction final : public Action
+ {
+ public:
+ enum Type
+ {
+ CREATE = 0,
+ UPDATE,
+ DESTROY,
+ };
+
+ UiAction(Type type, std::shared_ptr<LuaUi::Element> element, LuaUtil::LuaState* state)
+ : Action(state)
+ , mType{ type }
+ , mElement{ std::move(element) }
+ {}
+
+ void apply(WorldView&) const override
+ {
+ try {
+ switch (mType)
+ {
+ case CREATE:
+ mElement->create();
+ break;
+ case UPDATE:
+ mElement->update();
+ break;
+ case DESTROY:
+ mElement->destroy();
+ break;
+ }
+ }
+ catch (std::exception& e)
+ {
+ // prevent any actions on a potentially corrupted widget
+ mElement->mRoot = nullptr;
+ throw;
+ }
+ }
+
+ std::string toString() const override
+ {
+ std::string result;
+ switch (mType)
+ {
+ case CREATE:
+ result += "Create";
+ break;
+ case UPDATE:
+ result += "Update";
+ break;
+ case DESTROY:
+ result += "Destroy";
+ break;
+ }
+ result += " UI";
+ return result;
+ }
+
+ private:
+ Type mType;
+ std::shared_ptr<LuaUi::Element> mElement;
+ };
+
+ // Lua arrays index from 1
+ inline size_t fromLuaIndex(size_t i) { return i - 1; }
+ inline size_t toLuaIndex(size_t i) { return i + 1; }
+ }
+
+ class LayerAction final : public Action
+ {
+ public:
+ LayerAction(std::string_view name, std::string_view afterName,
+ LuaUi::Layers::Options options, LuaUtil::LuaState* state)
+ : Action(state)
+ , mName(name)
+ , mAfterName(afterName)
+ , mOptions(options)
+ {}
+
+ void apply(WorldView&) const override
+ {
+ size_t index = LuaUi::Layers::indexOf(mAfterName);
+ if (index == LuaUi::Layers::size())
+ throw std::logic_error(std::string("Layer not found"));
+ LuaUi::Layers::insert(index, mName, mOptions);
+ }
+
+ std::string toString() const override
+ {
+ std::string result("Insert UI layer \"");
+ result += mName;
+ result += "\" after \"";
+ result += mAfterName;
+ result += "\"";
+ return result;
+ }
+
+ private:
+ std::string mName;
+ std::string mAfterName;
+ LuaUi::Layers::Options mOptions;
+ };
sol::table initUserInterfacePackage(const Context& context)
{
- sol::table api(context.mLua->sol(), sol::create);
+ auto uiContent = context.mLua->sol().new_usertype<LuaUi::Content>("UiContent");
+ uiContent[sol::meta_function::length] = [](const LuaUi::Content& content)
+ {
+ return content.size();
+ };
+ uiContent[sol::meta_function::index] = sol::overload(
+ [](const LuaUi::Content& content, size_t index)
+ {
+ return content.at(fromLuaIndex(index));
+ },
+ [](const LuaUi::Content& content, std::string_view name)
+ {
+ return content.at(name);
+ });
+ uiContent[sol::meta_function::new_index] = sol::overload(
+ [](LuaUi::Content& content, size_t index, const sol::table& table)
+ {
+ content.assign(fromLuaIndex(index), table);
+ },
+ [](LuaUi::Content& content, size_t index, sol::nil_t nil)
+ {
+ content.remove(fromLuaIndex(index));
+ },
+ [](LuaUi::Content& content, std::string_view name, const sol::table& table)
+ {
+ content.assign(name, table);
+ },
+ [](LuaUi::Content& content, std::string_view name, sol::nil_t nil)
+ {
+ content.remove(name);
+ });
+ uiContent["insert"] = [](LuaUi::Content& content, size_t index, const sol::table& table)
+ {
+ content.insert(fromLuaIndex(index), table);
+ };
+ uiContent["add"] = [](LuaUi::Content& content, const sol::table& table)
+ {
+ content.insert(content.size(), table);
+ };
+ uiContent["indexOf"] = [](LuaUi::Content& content, const sol::table& table) -> sol::optional<size_t>
+ {
+ size_t index = content.indexOf(table);
+ if (index < content.size())
+ return toLuaIndex(index);
+ else
+ return sol::nullopt;
+ };
+
+ auto element = context.mLua->sol().new_usertype<LuaUi::Element>("Element");
+ element["layout"] = sol::property(
+ [](LuaUi::Element& element)
+ {
+ return element.mLayout;
+ },
+ [](LuaUi::Element& element, const sol::table& layout)
+ {
+ element.mLayout = layout;
+ }
+ );
+ element["update"] = [context](const std::shared_ptr<LuaUi::Element>& element)
+ {
+ if (element->mDestroy || element->mUpdate)
+ return;
+ element->mUpdate = true;
+ context.mLuaManager->addAction(std::make_unique<UiAction>(UiAction::UPDATE, element, context.mLua));
+ };
+ element["destroy"] = [context](const std::shared_ptr<LuaUi::Element>& element)
+ {
+ if (element->mDestroy)
+ return;
+ allElements.erase(element.get());
+ element->mDestroy = true;
+ context.mLuaManager->addAction(std::make_unique<UiAction>(UiAction::DESTROY, element, context.mLua));
+ };
+
+ sol::table api = context.mLua->newTable();
api["showMessage"] = [luaManager=context.mLuaManager](std::string_view message)
{
luaManager->addUIMessage(message);
};
+ api["content"] = [](const sol::table& table)
+ {
+ return LuaUi::Content(table);
+ };
+ api["create"] = [context](const sol::table& layout)
+ {
+ auto element = std::make_shared<LuaUi::Element>(layout);
+ allElements.emplace(element.get());
+ context.mLuaManager->addAction(std::make_unique<UiAction>(UiAction::CREATE, element, context.mLua));
+ return element;
+ };
+
+ sol::table layers = context.mLua->newTable();
+ layers[sol::meta_function::length] = []()
+ {
+ return LuaUi::Layers::size();
+ };
+ layers[sol::meta_function::index] = [](size_t index)
+ {
+ index = fromLuaIndex(index);
+ return LuaUi::Layers::at(index);
+ };
+ layers["indexOf"] = [](std::string_view name) -> sol::optional<size_t>
+ {
+ size_t index = LuaUi::Layers::indexOf(name);
+ if (index == LuaUi::Layers::size())
+ return sol::nullopt;
+ else
+ return toLuaIndex(index);
+ };
+ layers["insertAfter"] = [context](std::string_view afterName, std::string_view name, const sol::object& opt)
+ {
+ LuaUi::Layers::Options options;
+ options.mInteractive = LuaUtil::getValueOrDefault(LuaUtil::getFieldOrNil(opt, "interactive"), true);
+ context.mLuaManager->addAction(std::make_unique<LayerAction>(name, afterName, options, context.mLua));
+ };
+ api["layers"] = LuaUtil::makeReadOnly(layers);
+
+ sol::table typeTable = context.mLua->newTable();
+ for (const auto& it : LuaUi::widgetTypeToName())
+ typeTable.set(it.second, it.first);
+ api["TYPE"] = LuaUtil::makeReadOnly(typeTable);
+
return LuaUtil::makeReadOnly(api);
}
+ void clearUserInterface()
+ {
+ for (auto element : allElements)
+ element->destroy();
+ allElements.clear();
+ }
}
diff --git a/apps/openmw/mwlua/userdataserializer.cpp b/apps/openmw/mwlua/userdataserializer.cpp
index 6946cd5532..b675774c53 100644
--- a/apps/openmw/mwlua/userdataserializer.cpp
+++ b/apps/openmw/mwlua/userdataserializer.cpp
@@ -33,7 +33,7 @@ namespace MWLua
// Deserializes userdata of type "typeName" from binaryData. Should push the result on stack using sol::stack::push.
// Returns false if this type is not supported by this serializer.
- bool deserialize(std::string_view typeName, std::string_view binaryData, sol::state& lua) const override
+ bool deserialize(std::string_view typeName, std::string_view binaryData, lua_State* lua) const override
{
if (typeName == "o")
{
diff --git a/apps/openmw/mwlua/worldview.cpp b/apps/openmw/mwlua/worldview.cpp
index beaa2d4770..35b1db7a93 100644
--- a/apps/openmw/mwlua/worldview.cpp
+++ b/apps/openmw/mwlua/worldview.cpp
@@ -4,6 +4,8 @@
#include <components/esm/esmwriter.hpp>
#include <components/esm/loadcell.hpp>
+#include "../mwbase/windowmanager.hpp"
+
#include "../mwclass/container.hpp"
#include "../mwworld/class.hpp"
@@ -20,6 +22,7 @@ namespace MWLua
mContainersInScene.updateList();
mDoorsInScene.updateList();
mItemsInScene.updateList();
+ mPaused = MWBase::Environment::get().getWindowManager()->isGuiMode();
}
void WorldView::clear()
@@ -67,16 +70,16 @@ namespace MWLua
removeFromGroup(*group, ptr);
}
- double WorldView::getGameTimeInHours() const
+ double WorldView::getGameTime() const
{
MWBase::World* world = MWBase::Environment::get().getWorld();
MWWorld::TimeStamp timeStamp = world->getTimeStamp();
- return static_cast<double>(timeStamp.getDay()) * 24 + timeStamp.getHour();
+ return (static_cast<double>(timeStamp.getDay()) * 24 + timeStamp.getHour()) * 3600.0;
}
void WorldView::load(ESM::ESMReader& esm)
{
- esm.getHNT(mGameSeconds, "LUAW");
+ esm.getHNT(mSimulationTime, "LUAW");
ObjectId lastAssignedId;
lastAssignedId.load(esm, true);
mObjectRegistry.setLastAssignedId(lastAssignedId);
@@ -84,7 +87,7 @@ namespace MWLua
void WorldView::save(ESM::ESMWriter& esm) const
{
- esm.writeHNT("LUAW", mGameSeconds);
+ esm.writeHNT("LUAW", mSimulationTime);
mObjectRegistry.getLastAssignedId().save(esm, true);
}
diff --git a/apps/openmw/mwlua/worldview.hpp b/apps/openmw/mwlua/worldview.hpp
index ea27e0ff84..e5b5fe4f67 100644
--- a/apps/openmw/mwlua/worldview.hpp
+++ b/apps/openmw/mwlua/worldview.hpp
@@ -19,13 +19,19 @@ namespace MWLua
void update(); // Should be called every frame.
void clear(); // Should be called every time before starting or loading a new game.
- // Returns the number of seconds passed from the beginning of the game.
- double getGameTimeInSeconds() const { return mGameSeconds; }
- void setGameTimeInSeconds(double t) { mGameSeconds = t; }
+ // Whether the world is paused (i.e. game time is not changing and actors don't move).
+ bool isPaused() const { return mPaused; }
- // Returns the number of game hours passed from the beginning of the game.
- // Note that the number of seconds in a game hour is not fixed.
- double getGameTimeInHours() const;
+ // The number of seconds passed from the beginning of the game.
+ double getSimulationTime() const { return mSimulationTime; }
+ void setSimulationTime(double t) { mSimulationTime = t; }
+ double getSimulationTimeScale() const { return 1.0; }
+
+ // The game time (in game seconds) passed from the beginning of the game.
+ // Note that game time generally goes faster than the simulation time.
+ double getGameTime() const;
+ double getGameTimeScale() const { return MWBase::Environment::get().getWorld()->getTimeScaleFactor(); }
+ void setGameTimeScale(double s) { MWBase::Environment::get().getWorld()->setGlobalFloat("timescale", s); }
ObjectIdList getActivatorsInScene() const { return mActivatorsInScene.mList; }
ObjectIdList getActorsInScene() const { return mActorsInScene.mList; }
@@ -73,7 +79,8 @@ namespace MWLua
ObjectGroup mDoorsInScene;
ObjectGroup mItemsInScene;
- double mGameSeconds = 0;
+ double mSimulationTime = 0;
+ bool mPaused = false;
};
}
diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp
index d435bd6c44..0c781e81bd 100644
--- a/apps/openmw/mwmechanics/activespells.cpp
+++ b/apps/openmw/mwmechanics/activespells.cpp
@@ -9,6 +9,8 @@
#include <components/esm/loadmgef.hpp>
+#include <components/settings/settings.hpp>
+
#include "creaturestats.hpp"
#include "spellcasting.hpp"
#include "spelleffects.hpp"
@@ -216,8 +218,7 @@ namespace MWMechanics
return params.mSlot == slotIndex && params.mType == ESM::ActiveSpells::Type_Enchantment && params.mId == slot->getCellRef().getRefId();
}) != mSpells.end())
continue;
- ActiveSpellParams params(*slot, enchantment, slotIndex, ptr);
- mSpells.emplace_back(params);
+ const ActiveSpellParams& params = mSpells.emplace_back(ActiveSpellParams{*slot, enchantment, slotIndex, ptr});
for(const auto& effect : params.mEffects)
MWMechanics::playEffects(ptr, *world->getStore().get<ESM::MagicEffect>().find(effect.mEffectId), playNonLooping);
}
@@ -236,7 +237,13 @@ namespace MWMechanics
if(result == MagicApplicationResult::REFLECTED)
{
if(!reflected)
- reflected = {*spellIt, ptr};
+ {
+ static const bool keepOriginalCaster = Settings::Manager::getBool("classic reflected absorb spells behavior", "Game");
+ if(keepOriginalCaster)
+ reflected = {*spellIt, caster};
+ else
+ reflected = {*spellIt, ptr};
+ }
auto& reflectedEffect = reflected->mEffects.emplace_back(*it);
reflectedEffect.mFlags = ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption;
it = spellIt->mEffects.erase(it);
diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp
index cb4f8237e7..d5d434e128 100644
--- a/apps/openmw/mwmechanics/actors.cpp
+++ b/apps/openmw/mwmechanics/actors.cpp
@@ -443,6 +443,23 @@ namespace MWMechanics
}
}
+ void Actors::stopCombat(const MWWorld::Ptr& ptr)
+ {
+ auto& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence();
+ std::vector<MWWorld::Ptr> targets;
+ if(ai.getCombatTargets(targets))
+ {
+ std::set<MWWorld::Ptr> allySet;
+ getActorsSidingWith(ptr, allySet);
+ allySet.insert(ptr);
+ std::vector<MWWorld::Ptr> allies(allySet.begin(), allySet.end());
+ for(const auto& ally : allies)
+ ally.getClass().getCreatureStats(ally).getAiSequence().stopCombat(targets);
+ for(const auto& target : targets)
+ target.getClass().getCreatureStats(target).getAiSequence().stopCombat(allies);
+ }
+ }
+
void Actors::engageCombat (const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr> >& cachedAllies, bool againstPlayer)
{
// No combat for totally static creatures
@@ -979,7 +996,7 @@ namespace MWMechanics
// Calm witness down
if (ptr.getClass().isClass(ptr, "Guard"))
creatureStats.getAiSequence().stopPursuit();
- creatureStats.getAiSequence().stopCombat();
+ stopCombat(ptr);
// Reset factors to attack
creatureStats.setAttacked(false);
@@ -1013,13 +1030,10 @@ namespace MWMechanics
void Actors::updateProcessingRange()
{
// We have to cap it since using high values (larger than 7168) will make some quests harder or impossible to complete (bug #1876)
- static const float maxProcessingRange = 7168.f;
- static const float minProcessingRange = maxProcessingRange / 2.f;
+ static const float maxRange = 7168.f;
+ static const float minRange = maxRange / 2.f;
- float actorsProcessingRange = Settings::Manager::getFloat("actors processing range", "Game");
- actorsProcessingRange = std::min(actorsProcessingRange, maxProcessingRange);
- actorsProcessingRange = std::max(actorsProcessingRange, minProcessingRange);
- mActorsProcessingRange = actorsProcessingRange;
+ mActorsProcessingRange = std::clamp(Settings::Manager::getFloat("actors processing range", "Game"), minRange, maxRange);
}
void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately)
@@ -1315,7 +1329,7 @@ namespace MWMechanics
angleToApproachingActor = std::atan2(deltaPos.x(), deltaPos.y());
osg::Vec2f posAtT = relPos + relSpeed * t;
float coef = (posAtT.x() * relSpeed.x() + posAtT.y() * relSpeed.y()) / (collisionDist * collisionDist * maxSpeed);
- coef *= osg::clampBetween((maxDistForPartialAvoiding - dist) / (maxDistForPartialAvoiding - maxDistForStrictAvoiding), 0.f, 1.f);
+ coef *= std::clamp((maxDistForPartialAvoiding - dist) / (maxDistForPartialAvoiding - maxDistForStrictAvoiding), 0.f, 1.f);
movementCorrection = posAtT * coef;
if (otherPtr.getClass().getCreatureStats(otherPtr).isDead())
// In case of dead body still try to go around (it looks natural), but reduce the correction twice.
@@ -1601,6 +1615,7 @@ namespace MWMechanics
if (playerCharacter)
{
+ MWBase::Environment::get().getWorld()->applyDeferredPreviewRotationToPlayer(duration);
playerCharacter->update(duration);
playerCharacter->setVisibility(1.f);
}
@@ -1969,7 +1984,7 @@ namespace MWMechanics
if (stats.isDead())
continue;
- // An actor counts as siding with this actor if Follow or Escort is the current AI package, or there are only Combat and Wander packages before the Follow/Escort package
+ // An actor counts as siding with this actor if Follow or Escort is the current AI package, or there are only Wander packages before the Follow/Escort package
// Actors that are targeted by this actor's Follow or Escort packages also side with them
for (const auto& package : stats.getAiSequence())
{
@@ -1985,7 +2000,7 @@ namespace MWMechanics
}
break;
}
- else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander)
+ else if (package->getTypeId() > AiPackageTypeId::Wander && package->getTypeId() <= AiPackageTypeId::Activate) // Don't count "fake" package types
break;
}
}
diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp
index 7d44fd06cd..f922be6556 100644
--- a/apps/openmw/mwmechanics/actors.hpp
+++ b/apps/openmw/mwmechanics/actors.hpp
@@ -111,6 +111,8 @@ namespace MWMechanics
///< This function is normally called automatically during the update process, but it can
/// also be called explicitly at any time to force an update.
+ /// Removes an actor from combat and makes all of their allies stop fighting the actor's targets
+ void stopCombat(const MWWorld::Ptr& ptr);
/** Start combat between two actors
@Notes: If againstPlayer = true then actor2 should be the Player.
If one of the combatants is creature it should be actor1.
diff --git a/apps/openmw/mwmechanics/aiactivate.cpp b/apps/openmw/mwmechanics/aiactivate.cpp
index b4ddf0c030..06aef0cdb0 100644
--- a/apps/openmw/mwmechanics/aiactivate.cpp
+++ b/apps/openmw/mwmechanics/aiactivate.cpp
@@ -13,8 +13,8 @@
namespace MWMechanics
{
- AiActivate::AiActivate(const std::string &objectId)
- : mObjectId(objectId)
+ AiActivate::AiActivate(const std::string &objectId, bool repeat)
+ : TypedAiPackage<AiActivate>(repeat), mObjectId(objectId)
{
}
@@ -48,6 +48,7 @@ namespace MWMechanics
{
std::unique_ptr<ESM::AiSequence::AiActivate> activate(new ESM::AiSequence::AiActivate());
activate->mTargetId = mObjectId;
+ activate->mRepeat = getRepeat();
ESM::AiSequence::AiPackageContainer package;
package.mType = ESM::AiSequence::Ai_Activate;
@@ -56,7 +57,7 @@ namespace MWMechanics
}
AiActivate::AiActivate(const ESM::AiSequence::AiActivate *activate)
- : mObjectId(activate->mTargetId)
+ : AiActivate(activate->mTargetId, activate->mRepeat)
{
}
}
diff --git a/apps/openmw/mwmechanics/aiactivate.hpp b/apps/openmw/mwmechanics/aiactivate.hpp
index dc7e0bb26b..ad4d4e6064 100644
--- a/apps/openmw/mwmechanics/aiactivate.hpp
+++ b/apps/openmw/mwmechanics/aiactivate.hpp
@@ -24,7 +24,7 @@ namespace MWMechanics
public:
/// Constructor
/** \param objectId Reference to object to activate **/
- explicit AiActivate(const std::string &objectId);
+ explicit AiActivate(const std::string &objectId, bool repeat);
explicit AiActivate(const ESM::AiSequence::AiActivate* activate);
diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp
index eb139c918d..9eb8a00763 100644
--- a/apps/openmw/mwmechanics/aicombat.cpp
+++ b/apps/openmw/mwmechanics/aicombat.cpp
@@ -8,7 +8,7 @@
#include <components/misc/mathutil.hpp>
#include <components/sceneutil/positionattitudetransform.hpp>
-#include <components/detournavigator/navigator.hpp>
+#include <components/detournavigator/navigatorutils.hpp>
#include "../mwphysics/collisiontype.hpp"
@@ -137,7 +137,10 @@ namespace MWMechanics
}
storage.updateCombatMove(duration);
+ storage.mRotateMove = false;
if (storage.mReadyToAttack) updateActorsMovement(actor, duration, storage);
+ if (storage.mRotateMove)
+ return false;
storage.updateAttack(characterController);
}
else
@@ -277,7 +280,7 @@ namespace MWMechanics
// If there is no path, try to find a point on a line from the actor position to target projected
// on navmesh to attack the target from there.
const auto navigator = world->getNavigator();
- const auto hit = navigator->raycast(halfExtents, vActorPos, vTargetPos, navigatorFlags);
+ const auto hit = DetourNavigator::raycast(*navigator, halfExtents, vActorPos, vTargetPos, navigatorFlags);
if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack)
{
@@ -442,7 +445,7 @@ namespace MWMechanics
storage.mCurrentAction->getCombatRange(isRangedCombat);
float eps = isRangedCombat ? osg::DegreesToRadians(0.5) : osg::DegreesToRadians(3.f);
float targetAngleRadians = storage.mMovement.mRotation[axis];
- smoothTurn(actor, targetAngleRadians, axis, eps);
+ storage.mRotateMove = !smoothTurn(actor, targetAngleRadians, axis, eps);
}
MWWorld::Ptr AiCombat::getTarget() const
diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp
index 5425f1af0b..34e726412c 100644
--- a/apps/openmw/mwmechanics/aicombat.hpp
+++ b/apps/openmw/mwmechanics/aicombat.hpp
@@ -33,6 +33,7 @@ namespace MWMechanics
bool mAttack;
float mAttackRange;
bool mCombatMove;
+ bool mRotateMove;
osg::Vec3f mLastTargetPos;
const MWWorld::CellStore* mCell;
std::shared_ptr<Action> mCurrentAction;
@@ -65,6 +66,7 @@ namespace MWMechanics
mAttack(false),
mAttackRange(0.0f),
mCombatMove(false),
+ mRotateMove(false),
mLastTargetPos(0,0,0),
mCell(nullptr),
mCurrentAction(),
diff --git a/apps/openmw/mwmechanics/aiescort.cpp b/apps/openmw/mwmechanics/aiescort.cpp
index 75c0461105..0184c6e66f 100644
--- a/apps/openmw/mwmechanics/aiescort.cpp
+++ b/apps/openmw/mwmechanics/aiescort.cpp
@@ -20,16 +20,16 @@
namespace MWMechanics
{
- AiEscort::AiEscort(const std::string &actorId, int duration, float x, float y, float z)
- : mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast<float>(duration))
+ AiEscort::AiEscort(const std::string &actorId, int duration, float x, float y, float z, bool repeat)
+ : TypedAiPackage<AiEscort>(repeat), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast<float>(duration))
, mCellX(std::numeric_limits<int>::max())
, mCellY(std::numeric_limits<int>::max())
{
mTargetActorRefId = actorId;
}
- AiEscort::AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z)
- : mCellId(cellId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast<float>(duration))
+ AiEscort::AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z, bool repeat)
+ : TypedAiPackage<AiEscort>(repeat), mCellId(cellId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast<float>(duration))
, mCellX(std::numeric_limits<int>::max())
, mCellY(std::numeric_limits<int>::max())
{
@@ -37,11 +37,8 @@ namespace MWMechanics
}
AiEscort::AiEscort(const ESM::AiSequence::AiEscort *escort)
- : mCellId(escort->mCellId), mX(escort->mData.mX), mY(escort->mData.mY), mZ(escort->mData.mZ)
- // mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration.
- // The exact value of mDuration only matters for repeating packages.
- // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves.
- , mDuration(escort->mRemainingDuration > 0)
+ : TypedAiPackage<AiEscort>(escort->mRepeat), mCellId(escort->mCellId), mX(escort->mData.mX), mY(escort->mData.mY), mZ(escort->mData.mZ)
+ , mDuration(escort->mData.mDuration)
, mRemainingDuration(escort->mRemainingDuration)
, mCellX(std::numeric_limits<int>::max())
, mCellY(std::numeric_limits<int>::max())
@@ -103,10 +100,12 @@ namespace MWMechanics
escort->mData.mX = mX;
escort->mData.mY = mY;
escort->mData.mZ = mZ;
+ escort->mData.mDuration = mDuration;
escort->mTargetId = mTargetActorRefId;
escort->mTargetActorId = mTargetActorId;
escort->mRemainingDuration = mRemainingDuration;
escort->mCellId = mCellId;
+ escort->mRepeat = getRepeat();
ESM::AiSequence::AiPackageContainer package;
package.mType = ESM::AiSequence::Ai_Escort;
diff --git a/apps/openmw/mwmechanics/aiescort.hpp b/apps/openmw/mwmechanics/aiescort.hpp
index 27a177893d..c49e227e09 100644
--- a/apps/openmw/mwmechanics/aiescort.hpp
+++ b/apps/openmw/mwmechanics/aiescort.hpp
@@ -22,11 +22,11 @@ namespace MWMechanics
/// Implementation of AiEscort
/** The Actor will escort the specified actor to the world position x, y, z until they reach their position, or they run out of time
\implement AiEscort **/
- AiEscort(const std::string &actorId, int duration, float x, float y, float z);
+ AiEscort(const std::string &actorId, int duration, float x, float y, float z, bool repeat);
/// Implementation of AiEscortCell
/** The Actor will escort the specified actor to the cell position x, y, z until they reach their position, or they run out of time
\implement AiEscortCell **/
- AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z);
+ AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z, bool repeat);
AiEscort(const ESM::AiSequence::AiEscort* escort);
diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp
index ec23679977..43a0f25c62 100644
--- a/apps/openmw/mwmechanics/aifollow.cpp
+++ b/apps/openmw/mwmechanics/aifollow.cpp
@@ -28,36 +28,20 @@ namespace MWMechanics
{
int AiFollow::mFollowIndexCounter = 0;
-AiFollow::AiFollow(const std::string &actorId, float duration, float x, float y, float z)
-: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z)
+AiFollow::AiFollow(const std::string &actorId, float duration, float x, float y, float z, bool repeat)
+: TypedAiPackage<AiFollow>(repeat), mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z)
, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++)
{
mTargetActorRefId = actorId;
}
-AiFollow::AiFollow(const std::string &actorId, const std::string &cellId, float duration, float x, float y, float z)
-: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z)
+AiFollow::AiFollow(const std::string &actorId, const std::string &cellId, float duration, float x, float y, float z, bool repeat)
+: TypedAiPackage<AiFollow>(repeat), mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z)
, mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++)
{
mTargetActorRefId = actorId;
}
-AiFollow::AiFollow(const MWWorld::Ptr& actor, float duration, float x, float y, float z)
-: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z)
-, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++)
-{
- mTargetActorRefId = actor.getCellRef().getRefId();
- mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId();
-}
-
-AiFollow::AiFollow(const MWWorld::Ptr& actor, const std::string &cellId, float duration, float x, float y, float z)
-: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z)
-, mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++)
-{
- mTargetActorRefId = actor.getCellRef().getRefId();
- mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId();
-}
-
AiFollow::AiFollow(const MWWorld::Ptr& actor, bool commanded)
: TypedAiPackage<AiFollow>(makeDefaultOptions().withShouldCancelPreviousAi(!commanded))
, mAlwaysFollow(true), mDuration(0), mRemainingDuration(0), mX(0), mY(0), mZ(0)
@@ -68,12 +52,9 @@ AiFollow::AiFollow(const MWWorld::Ptr& actor, bool commanded)
}
AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow)
- : TypedAiPackage<AiFollow>(makeDefaultOptions().withShouldCancelPreviousAi(!follow->mCommanded))
+ : TypedAiPackage<AiFollow>(makeDefaultOptions().withShouldCancelPreviousAi(!follow->mCommanded).withRepeat(follow->mRepeat))
, mAlwaysFollow(follow->mAlwaysFollow)
- // mDuration isn't saved in the save file, so just giving it "1" for now if the package had a duration.
- // The exact value of mDuration only matters for repeating packages.
- // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves.
- , mDuration(follow->mRemainingDuration)
+ , mDuration(follow->mData.mDuration)
, mRemainingDuration(follow->mRemainingDuration)
, mX(follow->mData.mX), mY(follow->mData.mY), mZ(follow->mData.mZ)
, mCellId(follow->mCellId), mActive(follow->mActive), mFollowIndex(mFollowIndexCounter++)
@@ -160,12 +141,15 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte
if (actor.getCell()->isExterior()) //Outside?
{
if (mCellId == "") //No cell to travel to
+ {
+ mRemainingDuration = mDuration;
return true;
+ }
}
- else
+ else if (mCellId == actor.getCell()->getCell()->mName) //Cell to travel to
{
- if (mCellId == actor.getCell()->getCell()->mName) //Cell to travel to
- return true;
+ mRemainingDuration = mDuration;
+ return true;
}
}
}
@@ -221,6 +205,7 @@ void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const
follow->mData.mX = mX;
follow->mData.mY = mY;
follow->mData.mZ = mZ;
+ follow->mData.mDuration = mDuration;
follow->mTargetId = mTargetActorRefId;
follow->mTargetActorId = mTargetActorId;
follow->mRemainingDuration = mRemainingDuration;
@@ -228,6 +213,7 @@ void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const
follow->mAlwaysFollow = mAlwaysFollow;
follow->mCommanded = isCommanded();
follow->mActive = mActive;
+ follow->mRepeat = getRepeat();
ESM::AiSequence::AiPackageContainer package;
package.mType = ESM::AiSequence::Ai_Follow;
diff --git a/apps/openmw/mwmechanics/aifollow.hpp b/apps/openmw/mwmechanics/aifollow.hpp
index c4ac5eb3f2..b83e464fa1 100644
--- a/apps/openmw/mwmechanics/aifollow.hpp
+++ b/apps/openmw/mwmechanics/aifollow.hpp
@@ -40,12 +40,10 @@ namespace MWMechanics
class AiFollow final : public TypedAiPackage<AiFollow>
{
public:
- AiFollow(const std::string &actorId, float duration, float x, float y, float z);
- AiFollow(const std::string &actorId, const std::string &CellId, float duration, float x, float y, float z);
/// Follow Actor for duration or until you arrive at a world position
- AiFollow(const MWWorld::Ptr& actor, float duration, float X, float Y, float Z);
+ AiFollow(const std::string &actorId, float duration, float x, float y, float z, bool repeat);
/// Follow Actor for duration or until you arrive at a position in a cell
- AiFollow(const MWWorld::Ptr& actor, const std::string &CellId, float duration, float X, float Y, float Z);
+ AiFollow(const std::string &actorId, const std::string &CellId, float duration, float x, float y, float z, bool repeat);
/// Follow Actor indefinitively
AiFollow(const MWWorld::Ptr& actor, bool commanded=false);
diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp
index 5270b74166..498c6e3050 100644
--- a/apps/openmw/mwmechanics/aipackage.hpp
+++ b/apps/openmw/mwmechanics/aipackage.hpp
@@ -108,7 +108,7 @@ namespace MWMechanics
/// Upon adding this Ai package, should the Ai Sequence attempt to cancel previous Ai packages (default true)?
bool shouldCancelPreviousAi() const { return mOptions.mShouldCancelPreviousAi; }
- /// Return true if this package should repeat. Currently only used for Wander packages.
+ /// Return true if this package should repeat.
bool getRepeat() const { return mOptions.mRepeat; }
virtual osg::Vec3f getDestination() const { return osg::Vec3f(0, 0, 0); }
diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp
index bf4cf28deb..545ec7f140 100644
--- a/apps/openmw/mwmechanics/aisequence.cpp
+++ b/apps/openmw/mwmechanics/aisequence.cpp
@@ -31,14 +31,13 @@ void AiSequence::copy (const AiSequence& sequence)
sequence.mAiState.copy<AiWanderStorage>(mAiState);
}
-AiSequence::AiSequence() : mDone (false), mRepeat(false), mLastAiPackage(AiPackageTypeId::None) {}
+AiSequence::AiSequence() : mDone (false), mLastAiPackage(AiPackageTypeId::None) {}
AiSequence::AiSequence (const AiSequence& sequence)
{
copy (sequence);
mDone = sequence.mDone;
mLastAiPackage = sequence.mLastAiPackage;
- mRepeat = sequence.mRepeat;
}
AiSequence& AiSequence::operator= (const AiSequence& sequence)
@@ -159,6 +158,7 @@ bool AiSequence::isInCombat(const MWWorld::Ptr &actor) const
return false;
}
+// TODO: use std::list::remove_if for all these methods when we switch to C++20
void AiSequence::stopCombat()
{
for(auto it = mPackages.begin(); it != mPackages.end(); )
@@ -172,6 +172,19 @@ void AiSequence::stopCombat()
}
}
+void AiSequence::stopCombat(const std::vector<MWWorld::Ptr>& targets)
+{
+ for(auto it = mPackages.begin(); it != mPackages.end(); )
+ {
+ if ((*it)->getTypeId() == AiPackageTypeId::Combat && std::find(targets.begin(), targets.end(), (*it)->getTarget()) != targets.end())
+ {
+ it = mPackages.erase(it);
+ }
+ else
+ ++it;
+ }
+}
+
void AiSequence::stopPursuit()
{
for(auto it = mPackages.begin(); it != mPackages.end(); )
@@ -281,7 +294,7 @@ void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& charac
if (package->execute(actor, characterController, mAiState, duration))
{
// Put repeating noncombat AI packages on the end of the stack so they can be used again
- if (isActualAiPackage(packageTypeId) && (mRepeat || package->getRepeat()))
+ if (isActualAiPackage(packageTypeId) && package->getRepeat())
{
package->reset();
mPackages.push_back(package->clone());
@@ -355,7 +368,6 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo
else
++it;
}
- mRepeat=false;
}
// insert new package in correct place depending on priority
@@ -401,10 +413,6 @@ const AiPackage& MWMechanics::AiSequence::getActivePackage()
void AiSequence::fill(const ESM::AIPackageList &list)
{
- // If there is more than one package in the list, enable repeating
- if (list.mList.size() >= 2)
- mRepeat = true;
-
for (const auto& esmPackage : list.mList)
{
std::unique_ptr<MWMechanics::AiPackage> package;
@@ -420,22 +428,22 @@ void AiSequence::fill(const ESM::AIPackageList &list)
else if (esmPackage.mType == ESM::AI_Escort)
{
ESM::AITarget data = esmPackage.mTarget;
- package = std::make_unique<MWMechanics::AiEscort>(data.mId.toString(), data.mDuration, data.mX, data.mY, data.mZ);
+ package = std::make_unique<MWMechanics::AiEscort>(data.mId.toString(), data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0);
}
else if (esmPackage.mType == ESM::AI_Travel)
{
ESM::AITravel data = esmPackage.mTravel;
- package = std::make_unique<MWMechanics::AiTravel>(data.mX, data.mY, data.mZ);
+ package = std::make_unique<MWMechanics::AiTravel>(data.mX, data.mY, data.mZ, data.mShouldRepeat != 0);
}
else if (esmPackage.mType == ESM::AI_Activate)
{
ESM::AIActivate data = esmPackage.mActivate;
- package = std::make_unique<MWMechanics::AiActivate>(data.mName.toString());
+ package = std::make_unique<MWMechanics::AiActivate>(data.mName.toString(), data.mShouldRepeat != 0);
}
else //if (esmPackage.mType == ESM::AI_Follow)
{
ESM::AITarget data = esmPackage.mTarget;
- package = std::make_unique<MWMechanics::AiFollow>(data.mId.toString(), data.mDuration, data.mX, data.mY, data.mZ);
+ package = std::make_unique<MWMechanics::AiFollow>(data.mId.toString(), data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0);
}
mPackages.push_back(std::move(package));
}
@@ -454,24 +462,6 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence)
if (!sequence.mPackages.empty())
clear();
- // If there is more than one non-combat, non-pursue package in the list, enable repeating.
- int count = 0;
- for (auto& container : sequence.mPackages)
- {
- switch (container.mType)
- {
- case ESM::AiSequence::Ai_Wander:
- case ESM::AiSequence::Ai_Travel:
- case ESM::AiSequence::Ai_Escort:
- case ESM::AiSequence::Ai_Follow:
- case ESM::AiSequence::Ai_Activate:
- ++count;
- }
- }
-
- if (count > 1)
- mRepeat = true;
-
// Load packages
for (auto& container : sequence.mPackages)
{
diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp
index 645524d381..77f6b2f7c0 100644
--- a/apps/openmw/mwmechanics/aisequence.hpp
+++ b/apps/openmw/mwmechanics/aisequence.hpp
@@ -43,9 +43,6 @@ namespace MWMechanics
///Finished with top AIPackage, set for one frame
bool mDone;
- ///Does this AI sequence repeat (repeating of Wander packages handled separately)
- bool mRepeat;
-
///Copy AiSequence
void copy (const AiSequence& sequence);
@@ -105,6 +102,9 @@ namespace MWMechanics
/// Removes all combat packages until first non-combat or stack empty.
void stopCombat();
+ /// Removes all combat packages with the given targets
+ void stopCombat(const std::vector<MWWorld::Ptr>& targets);
+
/// Has a package been completed during the last update?
bool isPackageDone() const;
diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp
index 2594adcb34..0b4a1411eb 100644
--- a/apps/openmw/mwmechanics/aitravel.cpp
+++ b/apps/openmw/mwmechanics/aitravel.cpp
@@ -34,8 +34,8 @@ bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2)
namespace MWMechanics
{
- AiTravel::AiTravel(float x, float y, float z, AiTravel*)
- : mX(x), mY(y), mZ(z), mHidden(false)
+ AiTravel::AiTravel(float x, float y, float z, bool repeat, AiTravel*)
+ : TypedAiPackage<AiTravel>(repeat), mX(x), mY(y), mZ(z), mHidden(false)
{
}
@@ -44,13 +44,13 @@ namespace MWMechanics
{
}
- AiTravel::AiTravel(float x, float y, float z)
- : AiTravel(x, y, z, this)
+ AiTravel::AiTravel(float x, float y, float z, bool repeat)
+ : AiTravel(x, y, z, repeat, this)
{
}
AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel)
- : mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ), mHidden(false)
+ : TypedAiPackage<AiTravel>(travel->mRepeat), mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ), mHidden(false)
{
// Hidden ESM::AiSequence::AiTravel package should be converted into MWMechanics::AiInternalTravel type
assert(!travel->mHidden);
@@ -125,6 +125,7 @@ namespace MWMechanics
travel->mData.mY = mY;
travel->mData.mZ = mZ;
travel->mHidden = mHidden;
+ travel->mRepeat = getRepeat();
ESM::AiSequence::AiPackageContainer package;
package.mType = ESM::AiSequence::Ai_Travel;
diff --git a/apps/openmw/mwmechanics/aitravel.hpp b/apps/openmw/mwmechanics/aitravel.hpp
index ee4ff9ad4d..303df7b105 100644
--- a/apps/openmw/mwmechanics/aitravel.hpp
+++ b/apps/openmw/mwmechanics/aitravel.hpp
@@ -19,11 +19,11 @@ namespace MWMechanics
class AiTravel : public TypedAiPackage<AiTravel>
{
public:
- AiTravel(float x, float y, float z, AiTravel* derived);
+ AiTravel(float x, float y, float z, bool repeat, AiTravel* derived);
AiTravel(float x, float y, float z, AiInternalTravel* derived);
- AiTravel(float x, float y, float z);
+ AiTravel(float x, float y, float z, bool repeat);
explicit AiTravel(const ESM::AiSequence::AiTravel* travel);
diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp
index 9c84555404..664ae105f3 100644
--- a/apps/openmw/mwmechanics/aiwander.cpp
+++ b/apps/openmw/mwmechanics/aiwander.cpp
@@ -5,7 +5,7 @@
#include <components/debug/debuglog.hpp>
#include <components/misc/rng.hpp>
#include <components/esm/aisequence.hpp>
-#include <components/detournavigator/navigator.hpp>
+#include <components/detournavigator/navigatorutils.hpp>
#include <components/misc/coordinateconverter.hpp>
#include "../mwbase/world.hpp"
@@ -105,7 +105,7 @@ namespace MWMechanics
}
AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat):
- TypedAiPackage<AiWander>(makeDefaultOptions().withRepeat(repeat)),
+ TypedAiPackage<AiWander>(repeat),
mDistance(std::max(0, distance)),
mDuration(std::max(0, duration)),
mRemainingDuration(duration), mTimeOfDay(timeOfDay),
@@ -337,7 +337,8 @@ namespace MWMechanics
if (!isWaterCreature && !isFlyingCreature)
{
// findRandomPointAroundCircle uses wanderDistance as limit for random and not as exact distance
- if (const auto destination = navigator->findRandomPointAroundCircle(halfExtents, mInitialActorPosition, wanderDistance, navigatorFlags))
+ if (const auto destination = DetourNavigator::findRandomPointAroundCircle(*navigator, halfExtents,
+ mInitialActorPosition, wanderDistance, navigatorFlags))
mDestination = *destination;
else
mDestination = getRandomPointAround(mInitialActorPosition, wanderRadius);
diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp
index 58339d0228..6c33f86c76 100644
--- a/apps/openmw/mwmechanics/character.cpp
+++ b/apps/openmw/mwmechanics/character.cpp
@@ -1944,7 +1944,7 @@ void CharacterController::update(float duration)
bool flying = world->isFlying(mPtr);
bool solid = world->isActorCollisionEnabled(mPtr);
// Can't run and sneak while flying (see speed formula in Npc/Creature::getSpeed)
- bool sneak = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Sneak) && !flying;
+ bool sneak = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Sneak) && !flying && !inwater;
bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && !flying;
CreatureStats &stats = cls.getCreatureStats(mPtr);
Movement& movementSettings = cls.getMovementSettings(mPtr);
@@ -2044,7 +2044,7 @@ void CharacterController::update(float duration)
mIsMovingBackward = vec.y() < 0;
float maxDelta = osg::PI * duration * (2.5f - cosDelta);
- delta = osg::clampBetween(delta, -maxDelta, maxDelta);
+ delta = std::clamp(delta, -maxDelta, maxDelta);
stats.setSideMovementAngle(stats.getSideMovementAngle() + delta);
effectiveRotation += delta;
}
@@ -2061,7 +2061,7 @@ void CharacterController::update(float duration)
vec.x() *= speed;
vec.y() *= speed;
- if(mHitState != CharState_None && mJumpState == JumpState_None)
+ if(mHitState != CharState_None && mHitState != CharState_Block && mJumpState == JumpState_None)
vec = osg::Vec3f();
CharacterState movestate = CharState_None;
@@ -2286,7 +2286,7 @@ void CharacterController::update(float duration)
float swimmingPitch = mAnimation->getBodyPitchRadians();
float targetSwimmingPitch = -mPtr.getRefData().getPosition().rot[0];
float maxSwimPitchDelta = 3.0f * duration;
- swimmingPitch += osg::clampBetween(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta);
+ swimmingPitch += std::clamp(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta);
mAnimation->setBodyPitchRadians(swimmingPitch);
}
else
@@ -2522,7 +2522,7 @@ void CharacterController::unpersistAnimationState()
{
float start = mAnimation->getTextKeyTime(anim.mGroup+": start");
float stop = mAnimation->getTextKeyTime(anim.mGroup+": stop");
- float time = std::max(start, std::min(stop, anim.mTime));
+ float time = std::clamp(anim.mTime, start, stop);
complete = (time - start) / (stop - start);
}
@@ -2746,7 +2746,7 @@ void CharacterController::setVisibility(float visibility)
float chameleon = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude();
if (chameleon)
{
- alpha *= std::min(0.75f, std::max(0.25f, (100.f - chameleon)/100.f));
+ alpha *= std::clamp(1.f - chameleon / 100.f, 0.25f, 0.75f);
}
visibility = std::min(visibility, alpha);
@@ -2965,8 +2965,8 @@ void CharacterController::updateHeadTracking(float duration)
const double xLimit = osg::DegreesToRadians(40.0);
const double zLimit = osg::DegreesToRadians(30.0);
double zLimitOffset = mAnimation->getUpperBodyYawRadians();
- xAngleRadians = osg::clampBetween(xAngleRadians, -xLimit, xLimit);
- zAngleRadians = osg::clampBetween(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset);
+ xAngleRadians = std::clamp(xAngleRadians, -xLimit, xLimit);
+ zAngleRadians = std::clamp(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset);
float factor = duration*5;
factor = std::min(factor, 1.f);
diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp
index c1d5f711fc..2962a25ede 100644
--- a/apps/openmw/mwmechanics/combat.cpp
+++ b/apps/openmw/mwmechanics/combat.cpp
@@ -113,10 +113,9 @@ namespace MWMechanics
+ 0.1f * attackerStats.getAttribute(ESM::Attribute::Luck).getModified();
attackerTerm *= attackerStats.getFatigueTerm();
- int x = int(blockerTerm - attackerTerm);
- int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger();
- int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger();
- x = std::min(iBlockMaxChance, std::max(iBlockMinChance, x));
+ const int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger();
+ const int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger();
+ int x = std::clamp<int>(blockerTerm - attackerTerm, iBlockMinChance, iBlockMaxChance);
if (Misc::Rng::roll0to99() < x)
{
diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp
index d832c47c87..570547e0d9 100644
--- a/apps/openmw/mwmechanics/creaturestats.cpp
+++ b/apps/openmw/mwmechanics/creaturestats.cpp
@@ -1,7 +1,6 @@
#include "creaturestats.hpp"
#include <algorithm>
-#include <climits>
#include <components/esm/creaturestats.hpp>
#include <components/esm/esmreader.hpp>
@@ -45,7 +44,7 @@ namespace MWMechanics
float max = getFatigue().getModified();
float current = getFatigue().getCurrent();
- float normalised = floor(max) == 0 ? 1 : std::max (0.0f, current / max);
+ float normalised = std::floor(max) == 0 ? 1 : std::max (0.0f, current / max);
const MWWorld::Store<ESM::GameSetting> &gmst =
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
@@ -558,12 +557,13 @@ namespace MWMechanics
state.mHasAiSettings = true;
for (int i=0; i<4; ++i)
mAiSettings[i].writeState (state.mAiSettings[i]);
+
+ state.mMissingACDT = false;
}
void CreatureStats::readState (const ESM::CreatureStats& state)
{
- // HACK: using mGoldPool as an indicator for lack of ACDT during .ess import
- if (state.mGoldPool != INT_MIN)
+ if (!state.mMissingACDT)
{
for (int i=0; i<ESM::Attribute::Length; ++i)
mAttributes[i].readState (state.mAttributes[i]);
diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp
index e5c4f6fa41..0543a31ba2 100644
--- a/apps/openmw/mwmechanics/creaturestats.hpp
+++ b/apps/openmw/mwmechanics/creaturestats.hpp
@@ -69,8 +69,6 @@ namespace MWMechanics
MWWorld::TimeStamp mLastRestock;
// The pool of merchant gold (not in inventory)
- // HACK: value of INT_MIN has a special meaning: indicates a converted .ess file
- // (this is a workaround to avoid changing the save file format)
int mGoldPool;
int mActorId;
diff --git a/apps/openmw/mwmechanics/difficultyscaling.cpp b/apps/openmw/mwmechanics/difficultyscaling.cpp
index 2376989745..e973e0ed52 100644
--- a/apps/openmw/mwmechanics/difficultyscaling.cpp
+++ b/apps/openmw/mwmechanics/difficultyscaling.cpp
@@ -13,9 +13,7 @@ float scaleDamage(float damage, const MWWorld::Ptr& attacker, const MWWorld::Ptr
const MWWorld::Ptr& player = MWMechanics::getPlayer();
// [-500, 500]
- int difficultySetting = Settings::Manager::getInt("difficulty", "Game");
- difficultySetting = std::min(difficultySetting, 500);
- difficultySetting = std::max(difficultySetting, -500);
+ const int difficultySetting = std::clamp(Settings::Manager::getInt("difficulty", "Game"), -500, 500);
static const float fDifficultyMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fDifficultyMult")->mValue.getFloat();
diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp
index 078cbc5f43..a1870cbdb0 100644
--- a/apps/openmw/mwmechanics/enchanting.cpp
+++ b/apps/openmw/mwmechanics/enchanting.cpp
@@ -356,10 +356,10 @@ namespace MWMechanics
ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass;
if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo)
{
- static const float multiplier = std::max(0.f, std::min(1.0f, Settings::Manager::getFloat("projectiles enchant multiplier", "Game")));
+ static const float multiplier = std::clamp(Settings::Manager::getFloat("projectiles enchant multiplier", "Game"), 0.f, 1.f);
MWWorld::Ptr player = getPlayer();
- int itemsInInventoryCount = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId());
- count = std::min(itemsInInventoryCount, std::max(1, int(getGemCharge() * multiplier / enchantPoints)));
+ count = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId());
+ count = std::clamp<int>(getGemCharge() * multiplier / enchantPoints, 1, count);
}
}
diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp
index b01c5446a7..e9e0be397e 100644
--- a/apps/openmw/mwmechanics/magiceffects.cpp
+++ b/apps/openmw/mwmechanics/magiceffects.cpp
@@ -11,7 +11,7 @@ namespace
// Round value to prevent precision issues
void truncate(float& value)
{
- value = std::roundf(value * 1024.f) / 1024.f;
+ value = static_cast<int>(value * 1024.f) / 1024.f;
}
}
@@ -133,28 +133,6 @@ namespace MWMechanics
}
}
- MagicEffects& MagicEffects::operator+= (const MagicEffects& effects)
- {
- if (this==&effects)
- {
- MagicEffects temp (effects);
- *this += temp;
- return *this;
- }
-
- for (Collection::const_iterator iter (effects.begin()); iter!=effects.end(); ++iter)
- {
- Collection::iterator result = mCollection.find (iter->first);
-
- if (result!=mCollection.end())
- result->second += iter->second;
- else
- mCollection.insert (*iter);
- }
-
- return *this;
- }
-
EffectParam MagicEffects::get (const EffectKey& key) const
{
Collection::const_iterator iter = mCollection.find (key);
diff --git a/apps/openmw/mwmechanics/magiceffects.hpp b/apps/openmw/mwmechanics/magiceffects.hpp
index 50f4dab050..e8175f6a78 100644
--- a/apps/openmw/mwmechanics/magiceffects.hpp
+++ b/apps/openmw/mwmechanics/magiceffects.hpp
@@ -97,8 +97,6 @@ namespace MWMechanics
/// Copy Modifier values from \a effects, but keep original mBase values.
void setModifiers(const MagicEffects& effects);
- MagicEffects& operator+= (const MagicEffects& effects);
-
EffectParam get (const EffectKey& key) const;
///< This function can safely be used for keys that are not present.
diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp
index e01ed3c425..e0d2da497b 100644
--- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp
+++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp
@@ -66,6 +66,35 @@ namespace
}
}
+ bool isOwned(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim)
+ {
+ const MWWorld::CellRef& cellref = target.getCellRef();
+
+ const std::string& owner = cellref.getOwner();
+ bool isOwned = !owner.empty() && owner != "player";
+
+ const std::string& faction = cellref.getFaction();
+ bool isFactionOwned = false;
+ if (!faction.empty() && ptr.getClass().isNpc())
+ {
+ const std::map<std::string, int>& factions = ptr.getClass().getNpcStats(ptr).getFactionRanks();
+ auto found = factions.find(Misc::StringUtils::lowerCase(faction));
+ if (found == factions.end() || found->second < cellref.getFactionRank())
+ isFactionOwned = true;
+ }
+
+ const std::string& globalVariable = cellref.getGlobalVariable();
+ if (!globalVariable.empty() && MWBase::Environment::get().getWorld()->getGlobalInt(globalVariable))
+ {
+ isOwned = false;
+ isFactionOwned = false;
+ }
+
+ if (!cellref.getOwner().empty())
+ victim = MWBase::Environment::get().getWorld()->searchPtr(cellref.getOwner(), true, false);
+
+ return isOwned || isFactionOwned;
+ }
}
namespace MWMechanics
@@ -289,13 +318,8 @@ namespace MWMechanics
MWWorld::Ptr ptr = getPlayer();
MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager();
- // Update the equipped weapon icon
MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr);
MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
- if (weapon == inv.end())
- winMgr->unsetSelectedWeapon();
- else
- winMgr->setSelectedWeapon(*weapon);
// Update the selected spell icon
MWWorld::ContainerStoreIterator enchantItem = inv.getSelectedEnchantItem();
@@ -310,6 +334,12 @@ namespace MWMechanics
winMgr->unsetSelectedSpell();
}
+ // Update the equipped weapon icon
+ if (weapon == inv.end())
+ winMgr->unsetSelectedWeapon();
+ else
+ winMgr->setSelectedWeapon(*weapon);
+
if (mUpdatePlayer)
{
mUpdatePlayer = false;
@@ -545,9 +575,9 @@ namespace MWMechanics
x += ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Charm).getMagnitude();
- if(clamp)
- return std::max(0,std::min(int(x),100));//, normally clamped to [0..100] when used
- return int(x);
+ if (clamp)
+ return std::clamp<int>(x, 0, 100);//, normally clamped to [0..100] when used
+ return static_cast<int>(x);
}
int MechanicsManager::getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying)
@@ -649,9 +679,9 @@ namespace MWMechanics
int flee = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Flee).getBase();
int fight = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Fight).getBase();
npcStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee,
- std::max(0, std::min(100, flee + int(std::max(iPerMinChange, s)))));
+ std::clamp(flee + int(std::max(iPerMinChange, s)), 0, 100));
npcStats.setAiSetting (MWMechanics::CreatureStats::AI_Fight,
- std::max(0, std::min(100, fight + int(std::min(-iPerMinChange, -s)))));
+ std::clamp(fight + int(std::min(-iPerMinChange, -s)), 0, 100));
}
float c = -std::abs(floor(r * fPerDieRollMult));
@@ -689,10 +719,10 @@ namespace MWMechanics
float s = c * fPerDieRollMult * fPerTempMult;
int flee = npcStats.getAiSetting (CreatureStats::AI_Flee).getBase();
int fight = npcStats.getAiSetting (CreatureStats::AI_Fight).getBase();
- npcStats.setAiSetting (CreatureStats::AI_Flee,
- std::max(0, std::min(100, flee + std::min(-int(iPerMinChange), int(-s)))));
- npcStats.setAiSetting (CreatureStats::AI_Fight,
- std::max(0, std::min(100, fight + std::max(int(iPerMinChange), int(s)))));
+ npcStats.setAiSetting(CreatureStats::AI_Flee,
+ std::clamp(flee + std::min(-int(iPerMinChange), int(-s)), 0, 100));
+ npcStats.setAiSetting(CreatureStats::AI_Fight,
+ std::clamp(fight + std::max(int(iPerMinChange), int(s)), 0, 100));
}
x = floor(-c * fPerDieRollMult);
@@ -717,7 +747,7 @@ namespace MWMechanics
if (currentDisposition + tempChange < 0)
{
cappedDispositionChange = -currentDisposition;
- tempChange = 0;
+ tempChange = cappedDispositionChange;
}
permChange = floor(cappedDispositionChange / fPerTempMult);
@@ -787,7 +817,7 @@ namespace MWMechanics
MWBase::World* world = MWBase::Environment::get().getWorld();
world->getNavigator()->setUpdatesEnabled(mAI);
if (mAI)
- world->getNavigator()->update(world->getPlayerPtr().getRefData().getPosition().asVec3());
+ world->getNavigator()->update(world->getPlayerPtr().getRefData().getPosition().asVec3());
return mAI;
}
@@ -880,35 +910,11 @@ namespace MWMechanics
return true;
}
- const std::string& owner = cellref.getOwner();
- bool isOwned = !owner.empty() && owner != "player";
-
- const std::string& faction = cellref.getFaction();
- bool isFactionOwned = false;
- if (!faction.empty() && ptr.getClass().isNpc())
- {
- const std::map<std::string, int>& factions = ptr.getClass().getNpcStats(ptr).getFactionRanks();
- std::map<std::string, int>::const_iterator found = factions.find(Misc::StringUtils::lowerCase(faction));
- if (found == factions.end()
- || found->second < cellref.getFactionRank())
- isFactionOwned = true;
- }
-
- const std::string& globalVariable = cellref.getGlobalVariable();
- if (!globalVariable.empty() && MWBase::Environment::get().getWorld()->getGlobalInt(Misc::StringUtils::lowerCase(globalVariable)) == 1)
- {
- isOwned = false;
- isFactionOwned = false;
- }
-
- if (!cellref.getOwner().empty())
- victim = MWBase::Environment::get().getWorld()->searchPtr(cellref.getOwner(), true, false);
-
- // A special case for evidence chest - we should not allow to take items even if it is technically permitted
- if (Misc::StringUtils::ciEqual(cellref.getRefId(), "stolen_goods"))
+ if (isOwned(ptr, target, victim))
return false;
- return (!isOwned && !isFactionOwned);
+ // A special case for evidence chest - we should not allow to take items even if it is technically permitted
+ return !Misc::StringUtils::ciEqual(cellref.getRefId(), "stolen_goods");
}
bool MechanicsManager::sleepInBed(const MWWorld::Ptr &ptr, const MWWorld::Ptr &bed)
@@ -940,9 +946,14 @@ namespace MWMechanics
void MechanicsManager::unlockAttempted(const MWWorld::Ptr &ptr, const MWWorld::Ptr &item)
{
MWWorld::Ptr victim;
- if (isAllowedToUse(ptr, item, victim))
- return;
- commitCrime(ptr, victim, OT_Trespassing, item.getCellRef().getFaction());
+ if (isOwned(ptr, item, victim))
+ {
+ // Note that attempting to unlock something that has ever been locked (even ESM::UnbreakableLock) is a crime even if it's already unlocked.
+ // Likewise, it's illegal to unlock something that has a trap but isn't otherwise locked.
+ const auto& cellref = item.getCellRef();
+ if(cellref.getLockLevel() || !cellref.getTrap().empty())
+ commitCrime(ptr, victim, OT_Trespassing, item.getCellRef().getFaction());
+ }
}
std::vector<std::pair<std::string, int> > MechanicsManager::getStolenItemOwners(const std::string& itemid)
@@ -1607,6 +1618,11 @@ namespace MWMechanics
MWBase::Environment::get().getDialogueManager()->say(ptr, "attack");
}
+ void MechanicsManager::stopCombat(const MWWorld::Ptr& actor)
+ {
+ mActors.stopCombat(actor);
+ }
+
void MechanicsManager::getObjectsInRange(const osg::Vec3f &position, float radius, std::vector<MWWorld::Ptr> &objects)
{
mActors.getObjectsInRange(position, radius, objects);
@@ -1849,8 +1865,8 @@ namespace MWMechanics
{
const MWWorld::Store<ESM::GameSetting>& gmst = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
MWMechanics::NpcStats &stats = actor.getClass().getNpcStats(actor);
-
- stats.getSkill(ESM::Skill::Acrobatics).setBase(gmst.find("fWerewolfAcrobatics")->mValue.getInteger());
+ auto& skill = stats.getSkill(ESM::Skill::Acrobatics);
+ skill.setModifier(gmst.find("fWerewolfAcrobatics")->mValue.getFloat() - skill.getModified());
}
void MechanicsManager::cleanupSummonedCreature(const MWWorld::Ptr &caster, int creatureActorId)
diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp
index 06da2fde51..0f4c2e606a 100644
--- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp
+++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp
@@ -102,6 +102,8 @@ namespace MWMechanics
/// Makes \a ptr fight \a target. Also shouts a combat taunt.
void startCombat (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) override;
+ void stopCombat(const MWWorld::Ptr& ptr) override;
+
/**
* @note victim may be empty
* @param arg Depends on \a type, e.g. for Theft, the value of the item that was stolen.
diff --git a/apps/openmw/mwmechanics/npcstats.cpp b/apps/openmw/mwmechanics/npcstats.cpp
index 5d19368bf6..1d1dfacce8 100644
--- a/apps/openmw/mwmechanics/npcstats.cpp
+++ b/apps/openmw/mwmechanics/npcstats.cpp
@@ -371,7 +371,7 @@ int MWMechanics::NpcStats::getReputation() const
void MWMechanics::NpcStats::setReputation(int reputation)
{
// Reputation is capped in original engine
- mReputation = std::min(255, std::max(0, reputation));
+ mReputation = std::clamp(reputation, 0, 255);
}
int MWMechanics::NpcStats::getCrimeId() const
diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp
index 4b06993a49..e727d8585d 100644
--- a/apps/openmw/mwmechanics/pathfinding.cpp
+++ b/apps/openmw/mwmechanics/pathfinding.cpp
@@ -3,8 +3,7 @@
#include <iterator>
#include <limits>
-#include <components/detournavigator/debug.hpp>
-#include <components/detournavigator/navigator.hpp>
+#include <components/detournavigator/navigatorutils.hpp>
#include <components/debug/debuglog.hpp>
#include <components/misc/coordinateconverter.hpp>
@@ -114,7 +113,7 @@ namespace
bool operator()(const osg::Vec3f& start, const osg::Vec3f& end) const
{
- const auto position = mNavigator->raycast(mHalfExtents, start, end, mFlags);
+ const auto position = DetourNavigator::raycast(*mNavigator, mHalfExtents, start, end, mFlags);
return position.has_value() && std::abs((position.value() - start).length2() - (end - start).length2()) <= 1;
}
};
@@ -422,8 +421,8 @@ namespace MWMechanics
const auto world = MWBase::Environment::get().getWorld();
const auto stepSize = getPathStepSize(actor);
const auto navigator = world->getNavigator();
- const auto status = navigator->findPath(halfExtents, stepSize, startPoint, endPoint, flags, areaCosts,
- endTolerance, out);
+ const auto status = DetourNavigator::findPath(*navigator, halfExtents, stepSize,
+ startPoint, endPoint, flags, areaCosts, endTolerance, out);
if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath)
return DetourNavigator::Status::Success;
@@ -455,8 +454,8 @@ namespace MWMechanics
std::deque<osg::Vec3f> prePath;
auto prePathInserter = std::back_inserter(prePath);
const float endTolerance = 0;
- const auto status = navigator->findPath(halfExtents, stepSize, startPoint, mPath.front(), flags, areaCosts,
- endTolerance, prePathInserter);
+ const auto status = DetourNavigator::findPath(*navigator, halfExtents, stepSize,
+ startPoint, mPath.front(), flags, areaCosts, endTolerance, prePathInserter);
if (status == DetourNavigator::Status::NavMeshNotFound)
return;
diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp
index 2edc437752..c99eee43d5 100644
--- a/apps/openmw/mwmechanics/spellcasting.cpp
+++ b/apps/openmw/mwmechanics/spellcasting.cpp
@@ -128,9 +128,6 @@ namespace MWMechanics
}
canCastAnEffect = true;
- if (!checkEffectTarget(effectIt->mEffectID, target, caster, castByPlayer))
- continue;
-
// caster needs to be an actor for linked effects (e.g. Absorb)
if (magicEffect->mData.mFlags & ESM::MagicEffect::CasterLinked
&& (caster.isEmpty() || !caster.getClass().isActor()))
diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp
index 7907642f5e..7e5f89b39b 100644
--- a/apps/openmw/mwmechanics/spelleffects.cpp
+++ b/apps/openmw/mwmechanics/spelleffects.cpp
@@ -46,7 +46,7 @@ namespace
{
auto& creatureStats = target.getClass().getCreatureStats(target);
auto stat = creatureStats.getAiSetting(setting);
- stat.setModifier(static_cast<int>(stat.getModifier() - magnitude));
+ stat.setModifier(static_cast<int>(stat.getModifier() + magnitude));
creatureStats.setAiSetting(setting, stat);
}
}
@@ -491,11 +491,11 @@ void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, co
break;
case ESM::MagicEffect::FrenzyCreature:
case ESM::MagicEffect::FrenzyHumanoid:
- modifyAiSetting(target, effect, ESM::MagicEffect::FrenzyCreature, CreatureStats::AI_Fight, -effect.mMagnitude, invalid);
+ modifyAiSetting(target, effect, ESM::MagicEffect::FrenzyCreature, CreatureStats::AI_Fight, effect.mMagnitude, invalid);
break;
case ESM::MagicEffect::CalmCreature:
case ESM::MagicEffect::CalmHumanoid:
- modifyAiSetting(target, effect, ESM::MagicEffect::CalmCreature, CreatureStats::AI_Fight, effect.mMagnitude, invalid);
+ modifyAiSetting(target, effect, ESM::MagicEffect::CalmCreature, CreatureStats::AI_Fight, -effect.mMagnitude, invalid);
if(!invalid && effect.mMagnitude > 0)
{
auto& creatureStats = target.getClass().getCreatureStats(target);
@@ -631,7 +631,7 @@ void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, co
if (!target.isInCell() || !target.getCell()->isExterior() || godmode)
break;
float time = world->getTimeStamp().getHour();
- float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13)));
+ float timeDiff = std::clamp(std::abs(time - 13.f), 0.f, 7.f);
float damageScale = 1.f - timeDiff / 7.f;
// When cloudy, the sun damage effect is halved
static float fMagicSunBlockedMult = world->getStore().get<ESM::GameSetting>().find("fMagicSunBlockedMult")->mValue.getFloat();
@@ -774,6 +774,45 @@ void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, co
}
}
+bool shouldRemoveEffect(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect)
+{
+ const auto world = MWBase::Environment::get().getWorld();
+ switch(effect.mEffectId)
+ {
+ case ESM::MagicEffect::Levitate:
+ {
+ if(!world->isLevitationEnabled())
+ {
+ if(target == getPlayer())
+ MWBase::Environment::get().getWindowManager()->messageBox("#{sLevitateDisabled}");
+ return true;
+ }
+ break;
+ }
+ case ESM::MagicEffect::Recall:
+ case ESM::MagicEffect::DivineIntervention:
+ case ESM::MagicEffect::AlmsiviIntervention:
+ {
+ return effect.mFlags & ESM::ActiveEffect::Flag_Applied;
+ }
+ case ESM::MagicEffect::WaterWalking:
+ {
+ if (target.getClass().isPureWaterCreature(target) && world->isSwimming(target))
+ return true;
+ if (effect.mFlags & ESM::ActiveEffect::Flag_Applied)
+ break;
+ if (!world->isWaterWalkingCastableOnTarget(target))
+ {
+ if(target == getPlayer())
+ MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidEffect}");
+ return true;
+ }
+ break;
+ }
+ }
+ return false;
+}
+
MagicApplicationResult applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt)
{
const auto world = MWBase::Environment::get().getWorld();
@@ -792,10 +831,9 @@ MagicApplicationResult applyMagicEffect(const MWWorld::Ptr& target, const MWWorl
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}");
return MagicApplicationResult::APPLIED;
}
- else if(effect.mEffectId == ESM::MagicEffect::Levitate && !world->isLevitationEnabled())
+ else if(shouldRemoveEffect(target, effect))
{
- if(target == getPlayer())
- MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}");
+ onMagicEffectRemoved(target, spellParams, effect);
return MagicApplicationResult::REMOVED;
}
const auto* magicEffect = world->getStore().get<ESM::MagicEffect>().find(effect.mEffectId);
@@ -874,8 +912,13 @@ MagicApplicationResult applyMagicEffect(const MWWorld::Ptr& target, const MWWorl
float oldMagnitude = 0.f;
if(effect.mFlags & ESM::ActiveEffect::Flag_Applied)
oldMagnitude = effect.mMagnitude;
- else if(spellParams.getType() == ESM::ActiveSpells::Type_Consumable || spellParams.getType() == ESM::ActiveSpells::Type_Temporary)
- playEffects(target, *magicEffect);
+ else
+ {
+ if(spellParams.getType() != ESM::ActiveSpells::Type_Enchantment)
+ playEffects(target, *magicEffect, spellParams.getType() == ESM::ActiveSpells::Type_Consumable || spellParams.getType() == ESM::ActiveSpells::Type_Temporary);
+ if(effect.mEffectId == ESM::MagicEffect::Soultrap && !target.getClass().isNpc() && target.getType() == ESM::Creature::sRecordId && target.get<ESM::Creature>()->mBase->mData.mSoul == 0 && caster == getPlayer())
+ MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidTarget}");
+ }
float magnitude = roll(effect);
//Note that there's an early out for Flag_Applied AppliedOnce effects so we don't have to exclude them here
effect.mMagnitude = magnitude;
@@ -955,11 +998,11 @@ void removeMagicEffect(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellPara
break;
case ESM::MagicEffect::FrenzyCreature:
case ESM::MagicEffect::FrenzyHumanoid:
- modifyAiSetting(target, effect, ESM::MagicEffect::FrenzyCreature, CreatureStats::AI_Fight, effect.mMagnitude, invalid);
+ modifyAiSetting(target, effect, ESM::MagicEffect::FrenzyCreature, CreatureStats::AI_Fight, -effect.mMagnitude, invalid);
break;
case ESM::MagicEffect::CalmCreature:
case ESM::MagicEffect::CalmHumanoid:
- modifyAiSetting(target, effect, ESM::MagicEffect::CalmCreature, CreatureStats::AI_Fight, -effect.mMagnitude, invalid);
+ modifyAiSetting(target, effect, ESM::MagicEffect::CalmCreature, CreatureStats::AI_Fight, effect.mMagnitude, invalid);
break;
case ESM::MagicEffect::DemoralizeCreature:
case ESM::MagicEffect::DemoralizeHumanoid:
diff --git a/apps/openmw/mwmechanics/spellpriority.cpp b/apps/openmw/mwmechanics/spellpriority.cpp
index eb276deb78..b79af49b05 100644
--- a/apps/openmw/mwmechanics/spellpriority.cpp
+++ b/apps/openmw/mwmechanics/spellpriority.cpp
@@ -75,6 +75,16 @@ namespace
}
return duration;
}
+
+ bool isSpellActive(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const std::string& id)
+ {
+ int actorId = caster.getClass().getCreatureStats(caster).getActorId();
+ const auto& active = target.getClass().getCreatureStats(target).getActiveSpells();
+ return std::find_if(active.begin(), active.end(), [&](const auto& spell)
+ {
+ return spell.getCasterActorId() == actorId && Misc::StringUtils::ciEqual(spell.getId(), id);
+ }) != active.end();
+ }
}
namespace MWMechanics
@@ -105,8 +115,6 @@ namespace MWMechanics
float rateSpell(const ESM::Spell *spell, const MWWorld::Ptr &actor, const MWWorld::Ptr& enemy)
{
- const CreatureStats& stats = actor.getClass().getCreatureStats(actor);
-
float successChance = MWMechanics::getSpellSuccessChance(spell, actor);
if (successChance == 0.f)
return 0.f;
@@ -125,9 +133,9 @@ namespace MWMechanics
// Spells don't stack, so early out if the spell is still active on the target
int types = getRangeTypes(spell->mEffects);
- if ((types & Self) && stats.getActiveSpells().isSpellActive(spell->mId))
+ if ((types & Self) && isSpellActive(actor, actor, spell->mId))
return 0.f;
- if ( ((types & Touch) || (types & Target)) && enemy.getClass().getCreatureStats(enemy).getActiveSpells().isSpellActive(spell->mId))
+ if ( ((types & Touch) || (types & Target)) && isSpellActive(actor, enemy, spell->mId))
return 0.f;
return rateEffects(spell->mEffects, actor, enemy) * (successChance / 100.f);
@@ -368,6 +376,24 @@ namespace MWMechanics
return 0.f;
break;
+ case ESM::MagicEffect::BoundShield:
+ if(!actor.getClass().hasInventoryStore(actor))
+ return 0.f;
+ else if(!actor.getClass().isNpc())
+ {
+ // If the actor is an NPC they can benefit from the armor rating, otherwise check if we've got a one-handed weapon to use with the shield
+ const auto& store = actor.getClass().getInventoryStore(actor);
+ auto oneHanded = std::find_if(store.cbegin(MWWorld::ContainerStore::Type_Weapon), store.cend(), [](const MWWorld::ConstPtr& weapon)
+ {
+ if(weapon.getClass().getItemHealth(weapon) <= 0.f)
+ return false;
+ short type = weapon.get<ESM::Weapon>()->mBase->mData.mType;
+ return !(MWMechanics::getWeaponType(type)->mFlags & ESM::WeaponType::TwoHanded);
+ });
+ if(oneHanded == store.cend())
+ return 0.f;
+ }
+ break;
// Creatures can not wear armor
case ESM::MagicEffect::BoundCuirass:
case ESM::MagicEffect::BoundGloves:
@@ -552,6 +578,13 @@ namespace MWMechanics
if (!creatureStats.getSummonedCreatureMap().empty())
return 0.f;
}
+ if(effect.mEffectID >= ESM::MagicEffect::BoundDagger && effect.mEffectID <= ESM::MagicEffect::BoundGloves)
+ {
+ // While rateSpell prevents actors from recasting the same spell, it doesn't prevent them from casting different spells with the same effect.
+ // Multiple instances of the same bound item don't stack so if the effect is already active, rate it as useless.
+ if(actor.getClass().getCreatureStats(actor).getMagicEffects().get(effect.mEffectID).getMagnitude() > 0.f)
+ return 0.f;
+ }
// Underwater casting not possible
if (effect.mRange == ESM::RT_Target)
diff --git a/apps/openmw/mwmechanics/spellutil.cpp b/apps/openmw/mwmechanics/spellutil.cpp
index b18ad288ad..3c4f094be4 100644
--- a/apps/openmw/mwmechanics/spellutil.cpp
+++ b/apps/openmw/mwmechanics/spellutil.cpp
@@ -162,7 +162,10 @@ namespace MWMechanics
float castChance = baseChance + castBonus;
castChance *= stats.getFatigueTerm();
- return std::max(0.f, cap ? std::min(100.f, castChance) : castChance);
+ if (cap)
+ return std::clamp(castChance, 0.f, 100.f);
+
+ return std::max(castChance, 0.f);
}
float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka)
@@ -196,48 +199,4 @@ namespace MWMechanics
const auto spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search(spellId);
return spell && spellIncreasesSkill(spell);
}
-
- bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, const MWWorld::Ptr& caster, bool castByPlayer)
- {
- switch (effectId)
- {
- case ESM::MagicEffect::Levitate:
- {
- if (!MWBase::Environment::get().getWorld()->isLevitationEnabled())
- {
- if (castByPlayer)
- MWBase::Environment::get().getWindowManager()->messageBox("#{sLevitateDisabled}");
- return false;
- }
- break;
- }
- case ESM::MagicEffect::Soultrap:
- {
- if (!target.getClass().isNpc() // no messagebox for NPCs
- && (target.getType() == ESM::Creature::sRecordId && target.get<ESM::Creature>()->mBase->mData.mSoul == 0))
- {
- if (castByPlayer)
- MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidTarget}");
- return true; // must still apply to get visual effect and have target regard it as attack
- }
- break;
- }
- case ESM::MagicEffect::WaterWalking:
- {
- if (target.getClass().isPureWaterCreature(target) && MWBase::Environment::get().getWorld()->isSwimming(target))
- return false;
-
- MWBase::World *world = MWBase::Environment::get().getWorld();
-
- if (!world->isWaterWalkingCastableOnTarget(target))
- {
- if (castByPlayer && caster == target)
- MWBase::Environment::get().getWindowManager()->messageBox ("#{sMagicInvalidEffect}");
- return false;
- }
- break;
- }
- }
- return true;
- }
}
diff --git a/apps/openmw/mwmechanics/spellutil.hpp b/apps/openmw/mwmechanics/spellutil.hpp
index 81f39b6dda..bcc531087c 100644
--- a/apps/openmw/mwmechanics/spellutil.hpp
+++ b/apps/openmw/mwmechanics/spellutil.hpp
@@ -48,9 +48,6 @@ namespace MWMechanics
/// Get whether or not the given spell contributes to skill progress.
bool spellIncreasesSkill(const ESM::Spell* spell);
bool spellIncreasesSkill(const std::string& spellId);
-
- /// Check if the given effect can be applied to the target. If \a castByPlayer, emits a message box on failure.
- bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, const MWWorld::Ptr& caster, bool castByPlayer);
}
#endif
diff --git a/apps/openmw/mwmechanics/stat.cpp b/apps/openmw/mwmechanics/stat.cpp
index c87de2ccbb..ee484f5afd 100644
--- a/apps/openmw/mwmechanics/stat.cpp
+++ b/apps/openmw/mwmechanics/stat.cpp
@@ -246,14 +246,25 @@ namespace MWMechanics
return mModifier;
}
- void AttributeValue::setBase(float base)
+ void AttributeValue::setBase(float base, bool clearModifier)
{
mBase = base;
+ if(clearModifier)
+ {
+ mModifier = 0.f;
+ mDamage = 0.f;
+ }
}
void AttributeValue::setModifier(float mod)
{
- mModifier = mod;
+ if(mod < 0)
+ {
+ mModifier = 0.f;
+ mDamage -= mod;
+ }
+ else
+ mModifier = mod;
}
void AttributeValue::damage(float damage)
diff --git a/apps/openmw/mwmechanics/stat.hpp b/apps/openmw/mwmechanics/stat.hpp
index fb9dca9221..c80c5b1b70 100644
--- a/apps/openmw/mwmechanics/stat.hpp
+++ b/apps/openmw/mwmechanics/stat.hpp
@@ -133,13 +133,14 @@ namespace MWMechanics
float getBase() const;
float getModifier() const;
- void setBase(float base);
+ void setBase(float base, bool clearModifier = false);
void setModifier(float mod);
// Maximum attribute damage is limited to the modified value.
- // Note: I think MW applies damage directly to mModified, since you can also
- // "restore" drained attributes. We need to rewrite the magic effect system to support this.
+ // Note: MW applies damage directly to mModified, however it does track how much
+ // a damaged attribute that has been fortified beyond its base can be restored.
+ // Getting rid of mDamage would require calculating its value by ignoring active effects when restoring
void damage(float damage);
void restore(float amount);
diff --git a/apps/openmw/mwmechanics/trading.cpp b/apps/openmw/mwmechanics/trading.cpp
index 750fd803d4..d9ec66bab2 100644
--- a/apps/openmw/mwmechanics/trading.cpp
+++ b/apps/openmw/mwmechanics/trading.cpp
@@ -71,10 +71,10 @@ namespace MWMechanics
int initialMerchantOffer = std::abs(merchantOffer);
if ( !buying && (finalPrice > initialMerchantOffer) ) {
- skillGain = floor(100.f * (finalPrice - initialMerchantOffer) / finalPrice);
+ skillGain = std::floor(100.f * (finalPrice - initialMerchantOffer) / finalPrice);
}
else if ( buying && (finalPrice < initialMerchantOffer) ) {
- skillGain = floor(100.f * (initialMerchantOffer - finalPrice) / initialMerchantOffer);
+ skillGain = std::floor(100.f * (initialMerchantOffer - finalPrice) / initialMerchantOffer);
}
player.getClass().skillUsageSucceeded(player, ESM::Skill::Mercantile, 0, skillGain);
diff --git a/apps/openmw/mwmechanics/typedaipackage.hpp b/apps/openmw/mwmechanics/typedaipackage.hpp
index d2d424326c..0ea276999f 100644
--- a/apps/openmw/mwmechanics/typedaipackage.hpp
+++ b/apps/openmw/mwmechanics/typedaipackage.hpp
@@ -11,6 +11,9 @@ namespace MWMechanics
TypedAiPackage() :
AiPackage(T::getTypeId(), T::makeDefaultOptions()) {}
+ TypedAiPackage(bool repeat) :
+ AiPackage(T::getTypeId(), T::makeDefaultOptions().withRepeat(repeat)) {}
+
TypedAiPackage(const Options& options) :
AiPackage(T::getTypeId(), options) {}
diff --git a/apps/openmw/mwmechanics/weaponpriority.cpp b/apps/openmw/mwmechanics/weaponpriority.cpp
index 570e89a17d..1a17cc87e6 100644
--- a/apps/openmw/mwmechanics/weaponpriority.cpp
+++ b/apps/openmw/mwmechanics/weaponpriority.cpp
@@ -128,8 +128,7 @@ namespace MWMechanics
}
// Take hit chance in account, but do not allow rating become negative.
- float chance = getHitChance(actor, enemy, value) / 100.f;
- rating *= std::min(1.f, std::max(0.01f, chance));
+ rating *= std::clamp(getHitChance(actor, enemy, value) / 100.f, 0.01f, 1.f);
if (weapclass != ESM::WeaponType::Ammo)
rating *= weapon->mData.mSpeed;
diff --git a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp
index fc8725f5f4..a01ab96301 100644
--- a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp
+++ b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp
@@ -15,9 +15,9 @@ namespace MWPhysics
const btVector3& position, const btScalar radius)
{
const btVector3 nearest(
- std::max(aabbMin.x(), std::min(aabbMax.x(), position.x())),
- std::max(aabbMin.y(), std::min(aabbMax.y(), position.y())),
- std::max(aabbMin.z(), std::min(aabbMax.z(), position.z()))
+ std::clamp(position.x(), aabbMin.x(), aabbMax.x()),
+ std::clamp(position.y(), aabbMin.y(), aabbMax.y()),
+ std::clamp(position.z(), aabbMin.z(), aabbMax.z())
);
return nearest.distance(position) < radius;
}
diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp
index 5c49dee297..2f7e3b5995 100644
--- a/apps/openmw/mwphysics/mtphysics.cpp
+++ b/apps/openmw/mwphysics/mtphysics.cpp
@@ -267,7 +267,7 @@ namespace MWPhysics
, mNumJobs(0)
, mRemainingSteps(0)
, mLOSCacheExpiry(Settings::Manager::getInt("lineofsight keep inactive cache", "Physics"))
- , mNewFrame(false)
+ , mFrameCounter(0)
, mAdvanceSimulation(false)
, mQuit(false)
, mNextJob(0)
@@ -302,13 +302,14 @@ namespace MWPhysics
PhysicsTaskScheduler::~PhysicsTaskScheduler()
{
+ waitForWorkers();
{
MaybeExclusiveLock lock(mSimulationMutex, mNumThreads);
mQuit = true;
mNumJobs = 0;
mRemainingSteps = 0;
+ mHasJob.notify_all();
}
- mHasJob.notify_all();
for (auto& thread : mThreads)
thread.join();
}
@@ -361,6 +362,8 @@ namespace MWPhysics
void PhysicsTaskScheduler::applyQueuedMovements(float & timeAccum, std::vector<Simulation>&& simulations, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats)
{
+ waitForWorkers();
+
// This function run in the main thread.
// While the mSimulationMutex is held, background physics threads can't run.
@@ -393,7 +396,7 @@ namespace MWPhysics
mPhysicsDt = newDelta;
mSimulations = std::move(simulations);
mAdvanceSimulation = (mRemainingSteps != 0);
- mNewFrame = true;
+ ++mFrameCounter;
mNumJobs = mSimulations.size();
mNextLOS.store(0, std::memory_order_relaxed);
mNextJob.store(0, std::memory_order_release);
@@ -421,6 +424,7 @@ namespace MWPhysics
void PhysicsTaskScheduler::resetSimulation(const ActorMap& actors)
{
+ waitForWorkers();
MaybeExclusiveLock lock(mSimulationMutex, mNumThreads);
mBudget.reset(mDefaultPhysicsDt);
mAsyncBudget.reset(0.0f);
@@ -577,11 +581,15 @@ namespace MWPhysics
void PhysicsTaskScheduler::worker()
{
+ std::size_t lastFrame = 0;
std::shared_lock lock(mSimulationMutex);
while (!mQuit)
{
- if (!mNewFrame)
- mHasJob.wait(lock, [&]() { return mQuit || mNewFrame; });
+ if (lastFrame == mFrameCounter)
+ {
+ mHasJob.wait(lock, [&] { return mQuit || lastFrame != mFrameCounter; });
+ lastFrame = mFrameCounter;
+ }
doSimulation();
}
@@ -663,6 +671,7 @@ namespace MWPhysics
void PhysicsTaskScheduler::releaseSharedStates()
{
+ waitForWorkers();
std::scoped_lock lock(mSimulationMutex, mUpdateAabbMutex);
mSimulations.clear();
mUpdateAabb.clear();
@@ -693,7 +702,6 @@ namespace MWPhysics
void PhysicsTaskScheduler::afterPostSim()
{
- mNewFrame = false;
{
MaybeExclusiveLock lock(mLOSCacheMutex, mNumThreads);
mLOSCache.erase(
@@ -702,6 +710,10 @@ namespace MWPhysics
mLOSCache.end());
}
mTimeEnd = mTimer->tick();
+
+ std::unique_lock lock(mWorkersDoneMutex);
+ ++mWorkersFrameCounter;
+ mWorkersDone.notify_all();
}
void PhysicsTaskScheduler::syncWithMainThread()
@@ -710,4 +722,19 @@ namespace MWPhysics
for (auto& sim : mSimulations)
std::visit(vis, sim);
}
+
+ // Attempt to acquire unique lock on mSimulationMutex while not all worker
+ // threads are holding shared lock but will have to may lead to a deadlock because
+ // C++ standard does not guarantee priority for exclusive and shared locks
+ // for std::shared_mutex. For example microsoft STL implementation points out
+ // for the absence of such priority:
+ // https://docs.microsoft.com/en-us/windows/win32/sync/slim-reader-writer--srw--locks
+ void PhysicsTaskScheduler::waitForWorkers()
+ {
+ if (mNumThreads == 0)
+ return;
+ std::unique_lock lock(mWorkersDoneMutex);
+ if (mFrameCounter != mWorkersFrameCounter)
+ mWorkersDone.wait(lock);
+ }
}
diff --git a/apps/openmw/mwphysics/mtphysics.hpp b/apps/openmw/mwphysics/mtphysics.hpp
index 44330b2cc6..9ded1262d6 100644
--- a/apps/openmw/mwphysics/mtphysics.hpp
+++ b/apps/openmw/mwphysics/mtphysics.hpp
@@ -74,6 +74,7 @@ namespace MWPhysics
void afterPostStep();
void afterPostSim();
void syncWithMainThread();
+ void waitForWorkers();
std::unique_ptr<WorldFrameData> mWorldFrameData;
std::vector<Simulation> mSimulations;
@@ -95,13 +96,17 @@ namespace MWPhysics
int mNumJobs;
int mRemainingSteps;
int mLOSCacheExpiry;
- bool mNewFrame;
+ std::size_t mFrameCounter;
bool mAdvanceSimulation;
bool mQuit;
std::atomic<int> mNextJob;
std::atomic<int> mNextLOS;
std::vector<std::thread> mThreads;
+ std::size_t mWorkersFrameCounter = 0;
+ std::condition_variable mWorkersDone;
+ std::mutex mWorkersDoneMutex;
+
mutable std::shared_mutex mSimulationMutex;
mutable std::shared_mutex mCollisionWorldMutex;
mutable std::shared_mutex mLOSCacheMutex;
diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp
index ea595e6abb..98e3bcf737 100644
--- a/apps/openmw/mwphysics/physicssystem.cpp
+++ b/apps/openmw/mwphysics/physicssystem.cpp
@@ -718,7 +718,7 @@ namespace MWPhysics
physicActor->setCanWaterWalk(waterCollision);
// Slow fall reduces fall speed by a factor of (effect magnitude / 200)
- const float slowFall = 1.f - std::max(0.f, std::min(1.f, effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f));
+ const float slowFall = 1.f - std::clamp(effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f, 0.f, 1.f);
const bool godmode = ptr == world->getPlayerConstPtr() && world->getGodModeState();
const bool inert = stats.isDead() || (!godmode && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0);
diff --git a/apps/openmw/mwphysics/ptrholder.hpp b/apps/openmw/mwphysics/ptrholder.hpp
index fcd6ce203a..7366049cba 100644
--- a/apps/openmw/mwphysics/ptrholder.hpp
+++ b/apps/openmw/mwphysics/ptrholder.hpp
@@ -4,6 +4,8 @@
#include <mutex>
#include <memory>
+#include <osg/Vec3d>
+
#include <BulletCollision/CollisionDispatch/btCollisionObject.h>
#include "../mwworld/ptr.hpp"
@@ -20,7 +22,7 @@ namespace MWPhysics
mPtr = updated;
}
- MWWorld::Ptr getPtr()
+ MWWorld::Ptr getPtr() const
{
return mPtr;
}
@@ -56,12 +58,12 @@ namespace MWPhysics
mPosition = position;
}
- osg::Vec3f getPosition() const
+ osg::Vec3d getPosition() const
{
return mPosition;
}
- osg::Vec3f getPreviousPosition() const
+ osg::Vec3d getPreviousPosition() const
{
return mPreviousPosition;
}
@@ -71,8 +73,8 @@ namespace MWPhysics
std::unique_ptr<btCollisionObject> mCollisionObject;
osg::Vec3f mVelocity;
osg::Vec3f mSimulationPosition;
- osg::Vec3f mPosition;
- osg::Vec3f mPreviousPosition;
+ osg::Vec3d mPosition;
+ osg::Vec3d mPreviousPosition;
};
}
diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp
index 7706f7d7f1..25a9904e26 100644
--- a/apps/openmw/mwrender/actoranimation.cpp
+++ b/apps/openmw/mwrender/actoranimation.cpp
@@ -261,9 +261,6 @@ void ActorAnimation::updateHolsteredShield(bool showCarriedLeft)
if (isEnchanted)
SceneUtil::addEnchantedGlow(shieldNode, mResourceSystem, glowColor);
}
-
- if (mAlpha != 1.f)
- mResourceSystem->getSceneManager()->recreateShaders(mHolsteredShield->getNode());
}
bool ActorAnimation::useShieldAnimations() const
@@ -335,7 +332,7 @@ void ActorAnimation::resetControllers(osg::Node* node)
std::shared_ptr<SceneUtil::ControllerSource> src;
src.reset(new NullAnimationTime);
- SceneUtil::AssignControllerSourcesVisitor removeVisitor(src);
+ SceneUtil::ForceControllerSourcesVisitor removeVisitor(src);
node->accept(removeVisitor);
}
diff --git a/apps/openmw/mwrender/actorspaths.cpp b/apps/openmw/mwrender/actorspaths.cpp
index 4e3bfd79a6..c8c5f56d8f 100644
--- a/apps/openmw/mwrender/actorspaths.cpp
+++ b/apps/openmw/mwrender/actorspaths.cpp
@@ -4,11 +4,15 @@
#include <components/sceneutil/agentpath.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
+#include <components/detournavigator/settings.hpp>
#include <osg/PositionAttitudeTransform>
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
+
+#include <algorithm>
+
namespace MWRender
{
ActorsPaths::ActorsPaths(const osg::ref_ptr<osg::Group>& root, bool enabled)
@@ -44,7 +48,7 @@ namespace MWRender
if (group != mGroups.end())
mRootNode->removeChild(group->second);
- const auto newGroup = SceneUtil::createAgentPathGroup(path, halfExtents, start, end, settings);
+ const auto newGroup = SceneUtil::createAgentPathGroup(path, halfExtents, start, end, settings.mRecast);
if (newGroup)
{
MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(newGroup, "debug");
diff --git a/apps/openmw/mwrender/actorspaths.hpp b/apps/openmw/mwrender/actorspaths.hpp
index 1f61834d46..12f102093b 100644
--- a/apps/openmw/mwrender/actorspaths.hpp
+++ b/apps/openmw/mwrender/actorspaths.hpp
@@ -3,18 +3,22 @@
#include <apps/openmw/mwworld/ptr.hpp>
-#include <components/detournavigator/navigator.hpp>
-
#include <osg/ref_ptr>
#include <unordered_map>
#include <deque>
+#include <map>
namespace osg
{
class Group;
}
+namespace DetourNavigator
+{
+ struct Settings;
+}
+
namespace MWRender
{
class ActorsPaths
diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp
index ecfe65c575..b1c7bc89c4 100644
--- a/apps/openmw/mwrender/animation.cpp
+++ b/apps/openmw/mwrender/animation.cpp
@@ -7,6 +7,7 @@
#include <osg/BlendFunc>
#include <osg/Material>
#include <osg/Switch>
+#include <osg/LightModel>
#include <osgParticle/ParticleSystem>
#include <osgParticle/ParticleProcessor>
@@ -372,6 +373,19 @@ namespace
private:
int mEffectId;
};
+
+ osg::ref_ptr<osg::LightModel> getVFXLightModelInstance()
+ {
+ static osg::ref_ptr<osg::LightModel> lightModel = nullptr;
+
+ if (!lightModel)
+ {
+ lightModel = new osg::LightModel;
+ lightModel->setAmbientIntensity({1,1,1,1});
+ }
+
+ return lightModel;
+ }
}
namespace MWRender
@@ -624,9 +638,8 @@ namespace MWRender
return;
const NodeMap& nodeMap = getNodeMap();
-
- for (SceneUtil::KeyframeHolder::KeyframeControllerMap::const_iterator it = animsrc->mKeyframes->mKeyframeControllers.begin();
- it != animsrc->mKeyframes->mKeyframeControllers.end(); ++it)
+ const auto& controllerMap = animsrc->mKeyframes->mKeyframeControllers;
+ for (SceneUtil::KeyframeHolder::KeyframeControllerMap::const_iterator it = controllerMap.begin(); it != controllerMap.end(); ++it)
{
std::string bonename = Misc::StringUtils::lowerCase(it->first);
NodeMap::const_iterator found = nodeMap.find(bonename);
@@ -652,14 +665,32 @@ namespace MWRender
SceneUtil::AssignControllerSourcesVisitor assignVisitor(mAnimationTimePtr[0]);
mObjectRoot->accept(assignVisitor);
+ // Determine the movement accumulation bone if necessary
if (!mAccumRoot)
{
- NodeMap::const_iterator found = nodeMap.find("bip01");
- if (found == nodeMap.end())
- found = nodeMap.find("root bone");
-
- if (found != nodeMap.end())
- mAccumRoot = found->second;
+ // Priority matters! bip01 is preferred.
+ static const std::array<std::string, 2> accumRootNames =
+ {
+ "bip01",
+ "root bone"
+ };
+ NodeMap::const_iterator found = nodeMap.end();
+ for (const std::string& name : accumRootNames)
+ {
+ found = nodeMap.find(name);
+ if (found == nodeMap.end())
+ continue;
+ for (SceneUtil::KeyframeHolder::KeyframeControllerMap::const_iterator it = controllerMap.begin(); it != controllerMap.end(); ++it)
+ {
+ if (Misc::StringUtils::lowerCase(it->first) == name)
+ {
+ mAccumRoot = found->second;
+ break;
+ }
+ }
+ if (mAccumRoot)
+ break;
+ }
}
}
@@ -1530,7 +1561,9 @@ namespace MWRender
parentNode->addChild(trans);
osg::ref_ptr<osg::Node> node = mResourceSystem->getSceneManager()->getInstance(model, trans);
- node->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
+
+ // Morrowind has a white ambient light attached to the root VFX node of the scenegraph
+ node->getOrCreateStateSet()->setAttributeAndModes(getVFXLightModelInstance(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor;
node->accept(findMaxLengthVisitor);
@@ -1798,7 +1831,7 @@ namespace MWRender
visitor.remove();
}
- if (SceneUtil::hasUserDescription(mObjectRoot, Constants::NightDayLabel))
+ if (Settings::Manager::getBool("day night switches", "Game") && SceneUtil::hasUserDescription(mObjectRoot, Constants::NightDayLabel))
{
AddSwitchCallbacksVisitor visitor;
mObjectRoot->accept(visitor);
diff --git a/apps/openmw/mwrender/bulletdebugdraw.cpp b/apps/openmw/mwrender/bulletdebugdraw.cpp
index 9155132871..b169251465 100644
--- a/apps/openmw/mwrender/bulletdebugdraw.cpp
+++ b/apps/openmw/mwrender/bulletdebugdraw.cpp
@@ -8,7 +8,7 @@
#include <components/debug/debuglog.hpp>
#include <components/misc/convert.hpp>
-#include <components/sceneutil/util.hpp>
+#include <components/sceneutil/depth.hpp>
#include <osg/PolygonMode>
#include <osg/PolygonOffset>
#include <osg/ShapeDrawable>
@@ -66,7 +66,7 @@ void DebugDrawer::createGeometry()
auto* stateSet = new osg::StateSet;
stateSet->setAttributeAndModes(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE), osg::StateAttribute::ON);
- stateSet->setAttributeAndModes(new osg::PolygonOffset(SceneUtil::getReverseZ() ? 1.0 : -1.0, SceneUtil::getReverseZ() ? 1.0 : -1.0));
+ stateSet->setAttributeAndModes(new osg::PolygonOffset(SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0, SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0));
osg::ref_ptr<osg::Material> material = new osg::Material;
material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE);
stateSet->setAttribute(material);
diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp
index e750fcad46..5ca102bc39 100644
--- a/apps/openmw/mwrender/camera.cpp
+++ b/apps/openmw/mwrender/camera.cpp
@@ -56,41 +56,25 @@ namespace MWRender
mCamera(camera),
mAnimation(nullptr),
mFirstPersonView(true),
- mMode(Mode::Normal),
+ mMode(Mode::FirstPerson),
mVanityAllowed(true),
- mStandingPreviewAllowed(Settings::Manager::getBool("preview if stand still", "Camera")),
- mDeferredRotationAllowed(Settings::Manager::getBool("deferred preview rotation", "Camera")),
- mNearest(30.f),
- mFurthest(800.f),
- mIsNearest(false),
+ mDeferredRotationAllowed(true),
+ mProcessViewChange(false),
mHeight(124.f),
- mBaseCameraDistance(Settings::Manager::getFloat("third person camera distance", "Camera")),
mPitch(0.f),
mYaw(0.f),
mRoll(0.f),
- mVanityToggleQueued(false),
- mVanityToggleQueuedValue(false),
- mViewModeToggleQueued(false),
mCameraDistance(0.f),
- mMaxNextCameraDistance(800.f),
+ mPreferredCameraDistance(0.f),
mFocalPointCurrentOffset(osg::Vec2d()),
mFocalPointTargetOffset(osg::Vec2d()),
mFocalPointTransitionSpeedCoef(1.f),
mSkipFocalPointTransition(true),
mPreviousTransitionInfluence(0.f),
- mSmoothedSpeed(0.f),
- mZoomOutWhenMoveCoef(Settings::Manager::getFloat("zoom out when move coef", "Camera")),
- mDynamicCameraDistanceEnabled(false),
- mShowCrosshairInThirdPersonMode(false),
- mHeadBobbingEnabled(Settings::Manager::getBool("head bobbing", "Camera")),
- mHeadBobbingOffset(0.f),
- mHeadBobbingWeight(0.f),
- mTotalMovement(0.f),
+ mShowCrosshair(false),
mDeferredRotation(osg::Vec3f()),
mDeferredRotationDisabled(false)
{
- mCameraDistance = mBaseCameraDistance;
-
mUpdateCallback = new UpdateRenderCameraCallback(this);
mCamera->addUpdateCallback(mUpdateCallback);
}
@@ -100,7 +84,7 @@ namespace MWRender
mCamera->removeUpdateCallback(mUpdateCallback);
}
- osg::Vec3d Camera::getFocalPoint() const
+ osg::Vec3d Camera::calculateTrackedPosition() const
{
if (!mTrackingNode)
return osg::Vec3d();
@@ -108,155 +92,95 @@ namespace MWRender
if (nodepaths.empty())
return osg::Vec3d();
osg::Matrix worldMat = osg::computeLocalToWorld(nodepaths[0]);
-
- osg::Vec3d position = worldMat.getTrans();
- if (isFirstPerson())
- position.z() += mHeadBobbingOffset;
- else
- {
- position.z() += mHeight * mHeightScale;
-
- // We subtract 10.f here and add it within focalPointOffset in order to avoid camera clipping through ceiling.
- // Needed because character's head can be a bit higher than collision area.
- position.z() -= 10.f;
-
- position += getFocalPointOffset() + mFocalPointAdjustment;
- }
- return position;
+ osg::Vec3d res = worldMat.getTrans();
+ if (mMode != Mode::FirstPerson)
+ res.z() += mHeight * mHeightScale;
+ return res;
}
osg::Vec3d Camera::getFocalPointOffset() const
{
- osg::Vec3d offset(0, 0, 10.f);
- offset.x() += mFocalPointCurrentOffset.x() * cos(getYaw());
- offset.y() += mFocalPointCurrentOffset.x() * sin(getYaw());
- offset.z() += mFocalPointCurrentOffset.y();
+ osg::Vec3d offset;
+ offset.x() = mFocalPointCurrentOffset.x() * cos(mYaw);
+ offset.y() = mFocalPointCurrentOffset.x() * sin(mYaw);
+ offset.z() = mFocalPointCurrentOffset.y();
return offset;
}
- void Camera::getPosition(osg::Vec3d &focal, osg::Vec3d &camera) const
- {
- focal = getFocalPoint();
- osg::Vec3d offset(0,0,0);
- if (!isFirstPerson())
- {
- osg::Quat orient = osg::Quat(getPitch(), osg::Vec3d(1,0,0)) * osg::Quat(getYaw(), osg::Vec3d(0,0,1));
- offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f);
- }
- camera = focal + offset;
- }
-
void Camera::updateCamera(osg::Camera *cam)
{
- osg::Vec3d focal, position;
- getPosition(focal, position);
-
- osg::Quat orient = osg::Quat(mRoll, osg::Vec3d(0, 1, 0)) * osg::Quat(mPitch, osg::Vec3d(1, 0, 0)) * osg::Quat(mYaw, osg::Vec3d(0, 0, 1));
+ osg::Quat orient = osg::Quat(mRoll, osg::Vec3d(0, 1, 0)) *
+ osg::Quat(mPitch + mExtraPitch, osg::Vec3d(1, 0, 0)) *
+ osg::Quat(mYaw + mExtraYaw, osg::Vec3d(0, 0, 1));
osg::Vec3d forward = orient * osg::Vec3d(0,1,0);
osg::Vec3d up = orient * osg::Vec3d(0,0,1);
- cam->setViewMatrixAsLookAt(position, position + forward, up);
- }
-
- void Camera::updateHeadBobbing(float duration) {
- static const float doubleStepLength = Settings::Manager::getFloat("head bobbing step", "Camera") * 2;
- static const float stepHeight = Settings::Manager::getFloat("head bobbing height", "Camera");
- static const float maxRoll = osg::DegreesToRadians(Settings::Manager::getFloat("head bobbing roll", "Camera"));
-
- if (MWBase::Environment::get().getWorld()->isOnGround(mTrackingPtr))
- mHeadBobbingWeight = std::min(mHeadBobbingWeight + duration * 5, 1.f);
- else
- mHeadBobbingWeight = std::max(mHeadBobbingWeight - duration * 5, 0.f);
-
- float doubleStepState = mTotalMovement / doubleStepLength - std::floor(mTotalMovement / doubleStepLength); // from 0 to 1 during 2 steps
- float stepState = std::abs(doubleStepState * 4 - 2) - 1; // from -1 to 1 on even steps and from 1 to -1 on odd steps
- float effect = (1 - std::cos(stepState * osg::DegreesToRadians(30.f))) * 7.5f; // range from 0 to 1
- float coef = std::min(mSmoothedSpeed / 300.f, 1.f) * mHeadBobbingWeight;
- mHeadBobbingOffset = (0.5f - effect) * coef * stepHeight; // range from -stepHeight/2 to stepHeight/2
- mRoll = osg::sign(stepState) * effect * coef * maxRoll; // range from -maxRoll to maxRoll
- }
-
- void Camera::reset()
- {
- togglePreviewMode(false);
- toggleVanityMode(false);
- if (!mFirstPersonView)
- toggleViewMode();
- }
-
- void Camera::rotateCamera(float pitch, float yaw, bool adjust)
- {
- if (adjust)
+ osg::Vec3d pos = mPosition;
+ if (mMode == Mode::FirstPerson)
{
- pitch += getPitch();
- yaw += getYaw();
+ // It is a hack. Camera position depends on neck animation.
+ // Animations are updated in OSG cull traversal and in order to avoid 1 frame delay we
+ // recalculate the position here. Note that it becomes different from mPosition that
+ // is used in other parts of the code.
+ // TODO: detach camera from OSG animation and get rid of this hack.
+ osg::Vec3d recalculatedTrackedPosition = calculateTrackedPosition();
+ pos = calculateFirstPersonPosition(recalculatedTrackedPosition);
}
- setYaw(yaw);
- setPitch(pitch);
+ cam->setViewMatrixAsLookAt(pos, pos + forward, up);
}
void Camera::update(float duration, bool paused)
{
- if (mAnimation->upperBodyReady())
- {
- // Now process the view changes we queued earlier
- if (mVanityToggleQueued)
- {
- toggleVanityMode(mVanityToggleQueuedValue);
- mVanityToggleQueued = false;
- }
- if (mViewModeToggleQueued)
- {
- togglePreviewMode(false);
- toggleViewMode();
- mViewModeToggleQueued = false;
- }
- }
+ mLockPitch = mLockYaw = false;
+ if (mQueuedMode && mAnimation->upperBodyReady())
+ setMode(*mQueuedMode);
+ if (mProcessViewChange)
+ processViewChange();
if (paused)
return;
// only show the crosshair in game mode
MWBase::WindowManager *wm = MWBase::Environment::get().getWindowManager();
- wm->showCrosshair(!wm->isGuiMode() && mMode != Mode::Preview && mMode != Mode::Vanity
- && (mFirstPersonView || mShowCrosshairInThirdPersonMode));
-
- if(mMode == Mode::Vanity)
- rotateCamera(0.f, osg::DegreesToRadians(3.f * duration), true);
-
- if (isFirstPerson() && mHeadBobbingEnabled)
- updateHeadBobbing(duration);
- else
- mRoll = mHeadBobbingOffset = 0;
+ wm->showCrosshair(!wm->isGuiMode() && mShowCrosshair);
updateFocalPointOffset(duration);
updatePosition();
+ }
- float speed = mTrackingPtr.getClass().getCurrentSpeed(mTrackingPtr);
- mTotalMovement += speed * duration;
- speed /= (1.f + speed / 500.f);
- float maxDelta = 300.f * duration;
- mSmoothedSpeed += osg::clampBetween(speed - mSmoothedSpeed, -maxDelta, maxDelta);
-
- mMaxNextCameraDistance = mCameraDistance + duration * (100.f + mBaseCameraDistance);
- updateStandingPreviewMode();
+ osg::Vec3d Camera::calculateFirstPersonPosition(const osg::Vec3d& trackedPosition) const
+ {
+ osg::Vec3d res = trackedPosition;
+ osg::Vec2f horizontalOffset = Misc::rotateVec2f(osg::Vec2f(mFirstPersonOffset.x(), mFirstPersonOffset.y()), mYaw);
+ res.x() += horizontalOffset.x();
+ res.y() += horizontalOffset.y();
+ res.z() += mFirstPersonOffset.z();
+ return res;
}
void Camera::updatePosition()
{
- mFocalPointAdjustment = osg::Vec3d();
- if (isFirstPerson())
+ mTrackedPosition = calculateTrackedPosition();
+ if (mMode == Mode::Static)
return;
+ if (mMode == Mode::FirstPerson)
+ {
+ mPosition = calculateFirstPersonPosition(mTrackedPosition);
+ mCameraDistance = 0;
+ return;
+ }
- const float cameraObstacleLimit = 5.0f;
- const float focalObstacleLimit = 10.f;
- const int collisionType = (MWPhysics::CollisionType::CollisionType_Default & ~MWPhysics::CollisionType::CollisionType_Actor);
+ constexpr float cameraObstacleLimit = 5.0f;
+ constexpr float focalObstacleLimit = 10.f;
const auto* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting();
+ constexpr int collisionType = (MWPhysics::CollisionType::CollisionType_Default & ~MWPhysics::CollisionType::CollisionType_Actor);
// Adjust focal point to prevent clipping.
- osg::Vec3d focal = getFocalPoint();
osg::Vec3d focalOffset = getFocalPointOffset();
+ osg::Vec3d focal = mTrackedPosition + focalOffset;
+ focalOffset.z() += 10.f; // Needed to avoid camera clipping through the ceiling because
+ // character's head can be a bit higher than the collision area.
float offsetLen = focalOffset.length();
if (offsetLen > 0)
{
@@ -264,39 +188,51 @@ namespace MWRender
if (result.mHit)
{
double adjustmentCoef = -(result.mHitPos + result.mHitNormal * focalObstacleLimit - focal).length() / offsetLen;
- mFocalPointAdjustment = focalOffset * std::max(-1.0, adjustmentCoef);
+ focal += focalOffset * std::max(-1.0, adjustmentCoef);
}
}
- // Calculate camera distance.
- mCameraDistance = mBaseCameraDistance + getCameraDistanceCorrection();
- if (mDynamicCameraDistanceEnabled)
- mCameraDistance = std::min(mCameraDistance, mMaxNextCameraDistance);
- osg::Vec3d cameraPos;
- getPosition(focal, cameraPos);
- MWPhysics::RayCastingResult result = rayCasting->castSphere(focal, cameraPos, cameraObstacleLimit, collisionType);
+ // Adjust camera distance.
+ mCameraDistance = mPreferredCameraDistance;
+ osg::Quat orient = osg::Quat(mPitch + mExtraPitch, osg::Vec3d(1,0,0)) * osg::Quat(mYaw + mExtraYaw, osg::Vec3d(0,0,1));
+ osg::Vec3d offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f);
+ MWPhysics::RayCastingResult result = rayCasting->castSphere(focal, focal + offset, cameraObstacleLimit, collisionType);
if (result.mHit)
+ {
mCameraDistance = (result.mHitPos + result.mHitNormal * cameraObstacleLimit - focal).length();
+ offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f);
+ }
+
+ mPosition = focal + offset;
}
- void Camera::updateStandingPreviewMode()
+ void Camera::setMode(Mode newMode, bool force)
{
- if (!mStandingPreviewAllowed)
+ if (mMode == newMode)
return;
- float speed = mTrackingPtr.getClass().getCurrentSpeed(mTrackingPtr);
- bool combat = mTrackingPtr.getClass().isActor() &&
- mTrackingPtr.getClass().getCreatureStats(mTrackingPtr).getDrawState() != MWMechanics::DrawState_Nothing;
- bool standingStill = speed == 0 && !combat && !mFirstPersonView;
- if (!standingStill && mMode == Mode::StandingPreview)
+ Mode oldMode = mMode;
+ if (!force && (newMode == Mode::FirstPerson || oldMode == Mode::FirstPerson) && !mAnimation->upperBodyReady())
{
- mMode = Mode::Normal;
- calculateDeferredRotation();
+ // Changing the view will stop all playing animations, so if we are playing
+ // anything important, queue the view change for later
+ mQueuedMode = newMode;
+ return;
+ }
+ mMode = newMode;
+ mQueuedMode = std::nullopt;
+ if (newMode == Mode::FirstPerson)
+ mFirstPersonView = true;
+ else if (newMode == Mode::ThirdPerson)
+ mFirstPersonView = false;
+ calculateDeferredRotation();
+ if (oldMode == Mode::FirstPerson || newMode == Mode::FirstPerson)
+ {
+ instantTransition();
+ mProcessViewChange = true;
}
- else if (standingStill && mMode == Mode::Normal)
- mMode = Mode::StandingPreview;
}
- void Camera::setFocalPointTargetOffset(osg::Vec2d v)
+ void Camera::setFocalPointTargetOffset(const osg::Vec2d& v)
{
mFocalPointTargetOffset = v;
mPreviousTransitionSpeed = mFocalPointTransitionSpeed;
@@ -346,78 +282,16 @@ namespace MWRender
void Camera::toggleViewMode(bool force)
{
- // Changing the view will stop all playing animations, so if we are playing
- // anything important, queue the view change for later
- if (!mAnimation->upperBodyReady() && !force)
- {
- mViewModeToggleQueued = true;
- return;
- }
- else
- mViewModeToggleQueued = false;
-
- mFirstPersonView = !mFirstPersonView;
- updateStandingPreviewMode();
- instantTransition();
- processViewChange();
- }
-
- void Camera::allowVanityMode(bool allow)
- {
- if (!allow && mMode == Mode::Vanity)
- {
- disableDeferredPreviewRotation();
- toggleVanityMode(false);
- }
- mVanityAllowed = allow;
+ setMode(mFirstPersonView ? Mode::ThirdPerson : Mode::FirstPerson, force);
}
bool Camera::toggleVanityMode(bool enable)
{
- // Changing the view will stop all playing animations, so if we are playing
- // anything important, queue the view change for later
- if (mFirstPersonView && !mAnimation->upperBodyReady())
- {
- mVanityToggleQueued = true;
- mVanityToggleQueuedValue = enable;
- return false;
- }
-
- if (!mVanityAllowed && enable)
- return false;
-
- if ((mMode == Mode::Vanity) == enable)
- return true;
- mMode = enable ? Mode::Vanity : Mode::Normal;
- if (!mDeferredRotationAllowed)
- disableDeferredPreviewRotation();
if (!enable)
- calculateDeferredRotation();
-
- processViewChange();
- return true;
- }
-
- void Camera::togglePreviewMode(bool enable)
- {
- if (mFirstPersonView && !mAnimation->upperBodyReady())
- return;
-
- if((mMode == Mode::Preview) == enable)
- return;
-
- mMode = enable ? Mode::Preview : Mode::Normal;
- if (mMode == Mode::Normal)
- updateStandingPreviewMode();
- else if (mFirstPersonView)
- instantTransition();
- if (mMode == Mode::Normal)
- {
- if (!mDeferredRotationAllowed)
- disableDeferredPreviewRotation();
- calculateDeferredRotation();
- }
- processViewChange();
+ setMode(mFirstPersonView ? Mode::FirstPerson : Mode::ThirdPerson, false);
+ else if (mVanityAllowed)
+ setMode(Mode::Vanity, false);
+ return (mMode == Mode::Vanity) == enable;
}
void Camera::setSneakOffset(float offset)
@@ -425,67 +299,42 @@ namespace MWRender
mAnimation->setFirstPersonOffset(osg::Vec3f(0,0,-offset));
}
- void Camera::setYaw(float angle)
+ void Camera::setYaw(float angle, bool force)
{
- mYaw = Misc::normalizeAngle(angle);
+ if (!mLockYaw || force)
+ mYaw = Misc::normalizeAngle(angle);
+ if (force)
+ mLockYaw = true;
}
- void Camera::setPitch(float angle)
+ void Camera::setPitch(float angle, bool force)
{
const float epsilon = 0.000001f;
float limit = static_cast<float>(osg::PI_2) - epsilon;
- mPitch = osg::clampBetween(angle, -limit, limit);
- }
-
- float Camera::getCameraDistance() const
- {
- if (isFirstPerson())
- return 0.f;
- return mCameraDistance;
+ if (!mLockPitch || force)
+ mPitch = std::clamp(angle, -limit, limit);
+ if (force)
+ mLockPitch = true;
}
- void Camera::adjustCameraDistance(float delta)
+ void Camera::setStaticPosition(const osg::Vec3d& pos)
{
- if (!isFirstPerson())
- {
- if(isNearest() && delta < 0.f && getMode() != Mode::Preview && getMode() != Mode::Vanity)
- toggleViewMode();
- else
- mBaseCameraDistance = std::min(mCameraDistance - getCameraDistanceCorrection(), mBaseCameraDistance) + delta;
- }
- else if (delta > 0.f)
- {
- toggleViewMode();
- mBaseCameraDistance = 0;
- }
-
- mIsNearest = mBaseCameraDistance <= mNearest;
- mBaseCameraDistance = osg::clampBetween(mBaseCameraDistance, mNearest, mFurthest);
- Settings::Manager::setFloat("third person camera distance", "Camera", mBaseCameraDistance);
- }
-
- float Camera::getCameraDistanceCorrection() const
- {
- if (!mDynamicCameraDistanceEnabled)
- return 0;
-
- float pitchCorrection = std::max(-getPitch(), 0.f) * 50.f;
-
- float smoothedSpeedSqr = mSmoothedSpeed * mSmoothedSpeed;
- float speedCorrection = smoothedSpeedSqr / (smoothedSpeedSqr + 300.f*300.f) * mZoomOutWhenMoveCoef;
-
- return pitchCorrection + speedCorrection;
+ if (mMode != Mode::Static)
+ throw std::runtime_error("setStaticPosition can be used only if camera is in Static mode");
+ mPosition = pos;
}
void Camera::setAnimation(NpcAnimation *anim)
{
mAnimation = anim;
- processViewChange();
+ mProcessViewChange = true;
}
void Camera::processViewChange()
{
- if(isFirstPerson())
+ if (mTrackingPtr.isEmpty())
+ return;
+ if (mMode == Mode::FirstPerson)
{
mAnimation->setViewMode(NpcAnimation::VM_FirstPerson);
mTrackingNode = mAnimation->getNode("Camera");
@@ -503,12 +352,12 @@ namespace MWRender
else
mHeightScale = 1.f;
}
- rotateCamera(getPitch(), getYaw(), false);
+ mProcessViewChange = false;
}
void Camera::applyDeferredPreviewRotationToPlayer(float dt)
{
- if (isVanityOrPreviewModeEnabled() || mTrackingPtr.isEmpty())
+ if (mMode != Mode::ThirdPerson || mTrackingPtr.isEmpty())
return;
osg::Vec3f rot = mDeferredRotation;
@@ -541,6 +390,8 @@ namespace MWRender
void Camera::rotateCameraToTrackingPtr()
{
+ if (mMode == Mode::Static || mTrackingPtr.isEmpty())
+ return;
setPitch(-mTrackingPtr.getRefData().getPosition().rot[0] - mDeferredRotation.x());
setYaw(-mTrackingPtr.getRefData().getPosition().rot[2] - mDeferredRotation.z());
}
@@ -555,8 +406,13 @@ namespace MWRender
void Camera::calculateDeferredRotation()
{
+ if (mMode == Mode::Static)
+ {
+ mDeferredRotation = osg::Vec3f();
+ return;
+ }
MWWorld::Ptr ptr = mTrackingPtr;
- if (isVanityOrPreviewModeEnabled() || ptr.isEmpty())
+ if (mMode == Mode::Preview || mMode == Mode::Vanity || ptr.isEmpty())
return;
if (mFirstPersonView)
{
@@ -566,6 +422,8 @@ namespace MWRender
mDeferredRotation.x() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[0] - mPitch);
mDeferredRotation.z() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[2] - mYaw);
+ if (!mDeferredRotationAllowed)
+ mDeferredRotationDisabled = true;
}
}
diff --git a/apps/openmw/mwrender/camera.hpp b/apps/openmw/mwrender/camera.hpp
index 1a5477e89c..280d309256 100644
--- a/apps/openmw/mwrender/camera.hpp
+++ b/apps/openmw/mwrender/camera.hpp
@@ -1,6 +1,7 @@
#ifndef GAME_MWRENDER_CAMERA_H
#define GAME_MWRENDER_CAMERA_H
+#include <optional>
#include <string>
#include <osg/ref_ptr>
@@ -24,73 +25,8 @@ namespace MWRender
class Camera
{
public:
- enum class Mode { Normal, Vanity, Preview, StandingPreview };
+ enum class Mode : int {Static = 0, FirstPerson = 1, ThirdPerson = 2, Vanity = 3, Preview = 4};
- private:
- MWWorld::Ptr mTrackingPtr;
- osg::ref_ptr<const osg::Node> mTrackingNode;
- float mHeightScale;
-
- osg::ref_ptr<osg::Camera> mCamera;
-
- NpcAnimation *mAnimation;
-
- bool mFirstPersonView;
- Mode mMode;
- bool mVanityAllowed;
- bool mStandingPreviewAllowed;
- bool mDeferredRotationAllowed;
-
- float mNearest;
- float mFurthest;
- bool mIsNearest;
-
- float mHeight, mBaseCameraDistance;
- float mPitch, mYaw, mRoll;
-
- bool mVanityToggleQueued;
- bool mVanityToggleQueuedValue;
- bool mViewModeToggleQueued;
-
- float mCameraDistance;
- float mMaxNextCameraDistance;
-
- osg::Vec3d mFocalPointAdjustment;
- osg::Vec2d mFocalPointCurrentOffset;
- osg::Vec2d mFocalPointTargetOffset;
- float mFocalPointTransitionSpeedCoef;
- bool mSkipFocalPointTransition;
-
- // This fields are used to make focal point transition smooth if previous transition was not finished.
- float mPreviousTransitionInfluence;
- osg::Vec2d mFocalPointTransitionSpeed;
- osg::Vec2d mPreviousTransitionSpeed;
- osg::Vec2d mPreviousExtraOffset;
-
- float mSmoothedSpeed;
- float mZoomOutWhenMoveCoef;
- bool mDynamicCameraDistanceEnabled;
- bool mShowCrosshairInThirdPersonMode;
-
- bool mHeadBobbingEnabled;
- float mHeadBobbingOffset;
- float mHeadBobbingWeight; // Value from 0 to 1 for smooth enabling/disabling.
- float mTotalMovement; // Needed for head bobbing.
- void updateHeadBobbing(float duration);
-
- void updateFocalPointOffset(float duration);
- void updatePosition();
- float getCameraDistanceCorrection() const;
-
- osg::ref_ptr<osg::Callback> mUpdateCallback;
-
- // Used to rotate player to the direction of view after exiting preview or vanity mode.
- osg::Vec3f mDeferredRotation;
- bool mDeferredRotationDisabled;
- void calculateDeferredRotation();
- void updateStandingPreviewMode();
-
- public:
Camera(osg::Camera* camera);
~Camera();
@@ -99,36 +35,36 @@ namespace MWRender
MWWorld::Ptr getTrackingPtr() const { return mTrackingPtr; }
void setFocalPointTransitionSpeed(float v) { mFocalPointTransitionSpeedCoef = v; }
- void setFocalPointTargetOffset(osg::Vec2d v);
+ float getFocalPointTransitionSpeed() const { return mFocalPointTransitionSpeedCoef; }
+ void setFocalPointTargetOffset(const osg::Vec2d& v);
+ osg::Vec2d getFocalPointTargetOffset() const { return mFocalPointTargetOffset; }
void instantTransition();
- void enableDynamicCameraDistance(bool v) { mDynamicCameraDistanceEnabled = v; }
- void enableCrosshairInThirdPersonMode(bool v) { mShowCrosshairInThirdPersonMode = v; }
+ void showCrosshair(bool v) { mShowCrosshair = v; }
/// Update the view matrix of \a cam
void updateCamera(osg::Camera* cam);
/// Reset to defaults
- void reset();
+ void reset() { setMode(Mode::FirstPerson); }
- /// Set where the camera is looking at. Uses Morrowind (euler) angles
- /// \param rot Rotation angles in radians
- void rotateCamera(float pitch, float yaw, bool adjust);
void rotateCameraToTrackingPtr();
+ float getPitch() const { return mPitch; }
float getYaw() const { return mYaw; }
- void setYaw(float angle);
+ float getRoll() const { return mRoll; }
- float getPitch() const { return mPitch; }
- void setPitch(float angle);
+ void setPitch(float angle, bool force = false);
+ void setYaw(float angle, bool force = false);
+ void setRoll(float angle) { mRoll = angle; }
+
+ float getExtraPitch() const { return mExtraPitch; }
+ float getExtraYaw() const { return mExtraYaw; }
+ void setExtraPitch(float angle) { mExtraPitch = angle; }
+ void setExtraYaw(float angle) { mExtraYaw = angle; }
/// @param Force view mode switch, even if currently not allowed by the animation.
void toggleViewMode(bool force=false);
-
bool toggleVanityMode(bool enable);
- void allowVanityMode(bool allow);
-
- /// @note this may be ignored if an important animation is currently playing
- void togglePreviewMode(bool enable);
void applyDeferredPreviewRotationToPlayer(float dt);
void disableDeferredPreviewRotation() { mDeferredRotationDisabled = true; }
@@ -136,29 +72,84 @@ namespace MWRender
/// \brief Lowers the camera for sneak.
void setSneakOffset(float offset);
- bool isFirstPerson() const { return mFirstPersonView && mMode == Mode::Normal; }
-
void processViewChange();
void update(float duration, bool paused=false);
- /// Adds distDelta to the camera distance. Switches 3rd/1st person view if distance is less than limit.
- void adjustCameraDistance(float distDelta);
-
- float getCameraDistance() const;
+ float getCameraDistance() const { return mCameraDistance; }
+ void setPreferredCameraDistance(float v) { mPreferredCameraDistance = v; }
void setAnimation(NpcAnimation *anim);
- osg::Vec3d getFocalPoint() const;
- osg::Vec3d getFocalPointOffset() const;
-
- /// Stores focal and camera world positions in passed arguments
- void getPosition(osg::Vec3d &focal, osg::Vec3d &camera) const;
+ osg::Vec3d getTrackedPosition() const { return mTrackedPosition; }
+ const osg::Vec3d& getPosition() const { return mPosition; }
+ void setStaticPosition(const osg::Vec3d& pos);
- bool isVanityOrPreviewModeEnabled() const { return mMode != Mode::Normal; }
+ bool isVanityOrPreviewModeEnabled() const { return mMode == Mode::Vanity || mMode == Mode::Preview; }
Mode getMode() const { return mMode; }
+ std::optional<Mode> getQueuedMode() const { return mQueuedMode; }
+ void setMode(Mode mode, bool force = true);
+
+ void allowCharacterDeferredRotation(bool v) { mDeferredRotationAllowed = v; }
+ void calculateDeferredRotation();
+ void setFirstPersonOffset(const osg::Vec3f& v) { mFirstPersonOffset = v; }
+ osg::Vec3f getFirstPersonOffset() const { return mFirstPersonOffset; }
+
+ private:
+ MWWorld::Ptr mTrackingPtr;
+ osg::ref_ptr<const osg::Node> mTrackingNode;
+ osg::Vec3d mTrackedPosition;
+ float mHeightScale;
+
+ osg::ref_ptr<osg::Camera> mCamera;
+
+ NpcAnimation *mAnimation;
+
+ // Always 'true' if mMode == `FirstPerson`. Also it is 'true' in `Vanity` or `Preview` modes if
+ // the camera should return to `FirstPerson` view after it.
+ bool mFirstPersonView;
+
+ Mode mMode;
+ std::optional<Mode> mQueuedMode;
+ bool mVanityAllowed;
+ bool mDeferredRotationAllowed;
+
+ bool mProcessViewChange;
+
+ float mHeight;
+ float mPitch, mYaw, mRoll;
+ float mExtraPitch = 0, mExtraYaw = 0;
+ bool mLockPitch = false, mLockYaw = false;
+ osg::Vec3d mPosition;
- bool isNearest() const { return mIsNearest; }
+ float mCameraDistance, mPreferredCameraDistance;
+
+ osg::Vec3f mFirstPersonOffset{0, 0, 0};
+
+ osg::Vec2d mFocalPointCurrentOffset;
+ osg::Vec2d mFocalPointTargetOffset;
+ float mFocalPointTransitionSpeedCoef;
+ bool mSkipFocalPointTransition;
+
+ // This fields are used to make focal point transition smooth if previous transition was not finished.
+ float mPreviousTransitionInfluence;
+ osg::Vec2d mFocalPointTransitionSpeed;
+ osg::Vec2d mPreviousTransitionSpeed;
+ osg::Vec2d mPreviousExtraOffset;
+
+ bool mShowCrosshair;
+
+ osg::Vec3d calculateTrackedPosition() const;
+ osg::Vec3d calculateFirstPersonPosition(const osg::Vec3d& trackedPosition) const;
+ osg::Vec3d getFocalPointOffset() const;
+ void updateFocalPointOffset(float duration);
+ void updatePosition();
+
+ osg::ref_ptr<osg::Callback> mUpdateCallback;
+
+ // Used to rotate player to the direction of view after exiting preview or vanity mode.
+ osg::Vec3f mDeferredRotation;
+ bool mDeferredRotationDisabled;
};
}
diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp
index c717806e35..7cca787580 100644
--- a/apps/openmw/mwrender/characterpreview.cpp
+++ b/apps/openmw/mwrender/characterpreview.cpp
@@ -23,6 +23,7 @@
#include <components/sceneutil/shadow.hpp>
#include <components/settings/settings.hpp>
#include <components/sceneutil/nodecallback.hpp>
+#include <components/sceneutil/depth.hpp>
#include "../mwbase/world.hpp"
#include "../mwworld/class.hpp"
@@ -132,43 +133,6 @@ namespace MWRender
newStateSet->setTextureMode(7, GL_TEXTURE_2D, osg::StateAttribute::OFF);
newStateSet->setDefine("FORCE_OPAQUE", "0", osg::StateAttribute::ON);
}
- if (SceneUtil::getReverseZ() && stateset->getAttribute(osg::StateAttribute::DEPTH))
- {
- bool depthModified = false;
- osg::Depth* depth = static_cast<osg::Depth*>(stateset->getAttribute(osg::StateAttribute::DEPTH));
- depth->getUserValue("depthModified", depthModified);
-
- if (!depthModified)
- {
- if (!newStateSet)
- {
- newStateSet = new osg::StateSet(*stateset, osg::CopyOp::SHALLOW_COPY);
- node.setStateSet(newStateSet);
- }
- // Setup standard depth ranges
- osg::ref_ptr<osg::Depth> newDepth = new osg::Depth(*depth);
-
- switch (newDepth->getFunction())
- {
- case osg::Depth::LESS:
- newDepth->setFunction(osg::Depth::GREATER);
- break;
- case osg::Depth::LEQUAL:
- newDepth->setFunction(osg::Depth::GEQUAL);
- break;
- case osg::Depth::GREATER:
- newDepth->setFunction(osg::Depth::LESS);
- break;
- case osg::Depth::GEQUAL:
- newDepth->setFunction(osg::Depth::LEQUAL);
- break;
- default:
- break;
- }
- newStateSet->setAttribute(newDepth, osg::StateAttribute::ON);
- newDepth->setUserValue("depthModified", true);
- }
- }
}
traverse(node);
}
@@ -200,8 +164,6 @@ namespace MWRender
mCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT);
mCamera->setClearColor(osg::Vec4(0.f, 0.f, 0.f, 0.f));
mCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
- const float fovYDegrees = 12.3f;
- mCamera->setProjectionMatrixAsPerspective(fovYDegrees, sizeX/static_cast<float>(sizeY), 0.1f, 10000.f); // zNear and zFar are autocomputed
mCamera->setViewport(0, 0, sizeX, sizeY);
mCamera->setRenderOrder(osg::Camera::PRE_RENDER);
mCamera->attach(osg::Camera::COLOR_BUFFER, mTexture, 0, 0, false, Settings::Manager::getInt("antialiasing", "Video"));
@@ -211,6 +173,8 @@ namespace MWRender
mCamera->setNodeMask(Mask_RenderToTexture);
+ SceneUtil::setCameraClearDepth(mCamera);
+
bool ffp = mResourceSystem->getSceneManager()->getLightingMethod() == SceneUtil::LightingMethod::FFP;
osg::ref_ptr<SceneUtil::LightManager> lightManager = new SceneUtil::LightManager(ffp);
@@ -226,9 +190,14 @@ namespace MWRender
defaultMat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1));
defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f));
stateset->setAttribute(defaultMat);
- stateset->addUniform(new osg::Uniform("projectionMatrix", static_cast<osg::Matrixf>(mCamera->getProjectionMatrix())));
- stateset->setAttributeAndModes(new osg::Depth, osg::StateAttribute::ON);
+ const float fovYDegrees = 12.3f;
+ const float aspectRatio = static_cast<float>(sizeX) / static_cast<float>(sizeY);
+ const float znear = 0.1f;
+ const float zfar = 10000.f;
+ mCamera->setProjectionMatrixAsPerspective(fovYDegrees, aspectRatio, znear, zfar);
+ osg::Matrixf projectionMatrix = SceneUtil::AutoDepth::isReversed() ? static_cast<osg::Matrixf>(SceneUtil::getReversedZProjectionMatrixAsPerspective(fovYDegrees, aspectRatio, znear, zfar)) : static_cast<osg::Matrixf>(mCamera->getProjectionMatrix());
+ stateset->addUniform(new osg::Uniform("projectionMatrix", projectionMatrix));
SceneUtil::ShadowManager::disableShadowsForStateSet(stateset);
diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp
index b248efad4e..ba8749d81c 100644
--- a/apps/openmw/mwrender/globalmap.cpp
+++ b/apps/openmw/mwrender/globalmap.cpp
@@ -4,7 +4,6 @@
#include <osg/Texture2D>
#include <osg/Group>
#include <osg/Geometry>
-#include <osg/Depth>
#include <osg/TexEnvCombine>
#include <osgDB/WriteFile>
@@ -15,8 +14,8 @@
#include <components/debug/debuglog.hpp>
#include <components/sceneutil/workqueue.hpp>
-#include <components/sceneutil/util.hpp>
#include <components/sceneutil/nodecallback.hpp>
+#include <components/sceneutil/depth.hpp>
#include <components/esm/globalmap.hpp>
@@ -326,8 +325,8 @@ namespace MWRender
if (texture)
{
osg::ref_ptr<osg::Geometry> geom = createTexturedQuad(srcLeft, srcTop, srcRight, srcBottom);
- auto depth = SceneUtil::createDepth();
- depth->setWriteMask(0);
+ osg::ref_ptr<osg::Depth> depth = new SceneUtil::AutoDepth;
+ depth->setWriteMask(false);
osg::StateSet* stateset = geom->getOrCreateStateSet();
stateset->setAttribute(depth);
stateset->setTextureAttributeAndModes(0, texture, osg::StateAttribute::ON);
diff --git a/apps/openmw/mwrender/groundcover.cpp b/apps/openmw/mwrender/groundcover.cpp
index 77c4f0fab5..a39e31bb4d 100644
--- a/apps/openmw/mwrender/groundcover.cpp
+++ b/apps/openmw/mwrender/groundcover.cpp
@@ -1,33 +1,24 @@
#include "groundcover.hpp"
+#include <osg/ComputeBoundsVisitor>
#include <osg/AlphaFunc>
#include <osg/BlendFunc>
#include <osg/Geometry>
#include <osg/VertexAttribDivisor>
+#include <osg/Program>
#include <components/esm/esmreader.hpp>
#include <components/sceneutil/lightmanager.hpp>
+#include <components/sceneutil/nodecallback.hpp>
+#include <components/terrain/quadtreenode.hpp>
#include <components/shader/shadermanager.hpp>
-#include "apps/openmw/mwworld/esmstore.hpp"
-#include "apps/openmw/mwbase/environment.hpp"
-#include "apps/openmw/mwbase/world.hpp"
+#include "../mwworld/groundcoverstore.hpp"
#include "vismask.hpp"
namespace MWRender
{
- std::string getGroundcoverModel(int type, const std::string& id, const MWWorld::ESMStore& store)
- {
- switch (type)
- {
- case ESM::REC_STAT:
- return store.get<ESM::Static>().searchStatic(id)->mModel;
- default:
- return std::string();
- }
- }
-
class InstancingVisitor : public osg::NodeVisitor
{
public:
@@ -106,6 +97,20 @@ namespace MWRender
float mDensity = 0.f;
};
+ class ViewDistanceCallback : public SceneUtil::NodeCallback<ViewDistanceCallback>
+ {
+ public:
+ ViewDistanceCallback(float dist, const osg::BoundingBox& box) : mViewDistance(dist), mBox(box) {}
+ void operator()(osg::Node* node, osg::NodeVisitor* nv)
+ {
+ if (Terrain::distance(mBox, nv->getEyePoint()) <= mViewDistance)
+ traverse(node, nv);
+ }
+ private:
+ float mViewDistance;
+ osg::BoundingBox mBox;
+ };
+
inline bool isInChunkBorders(ESM::CellRef& ref, osg::Vec2f& minBound, osg::Vec2f& maxBound)
{
osg::Vec2f size = maxBound - minBound;
@@ -122,8 +127,9 @@ namespace MWRender
osg::ref_ptr<osg::Node> Groundcover::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile)
{
+ if (lod > getMaxLodLevel())
+ return nullptr;
GroundcoverChunkId id = std::make_tuple(center, size);
-
osg::ref_ptr<osg::Object> obj = mCache->getRefFromObjectCache(id);
if (obj)
return static_cast<osg::Node*>(obj.get());
@@ -137,12 +143,14 @@ namespace MWRender
}
}
- Groundcover::Groundcover(Resource::SceneManager* sceneManager, float density)
+ Groundcover::Groundcover(Resource::SceneManager* sceneManager, float density, float viewDistance, const MWWorld::GroundcoverStore& store)
: GenericResourceManager<GroundcoverChunkId>(nullptr)
, mSceneManager(sceneManager)
, mDensity(density)
, mStateset(new osg::StateSet)
+ , mGroundcoverStore(store)
{
+ setViewDistance(viewDistance);
// MGE uses default alpha settings for groundcover, so we can not rely on alpha properties
// Force a unified alpha handling instead of data from meshes
osg::ref_ptr<osg::AlphaFunc> alpha = new osg::AlphaFunc(osg::AlphaFunc::GEQUAL, 128.f / 255.f);
@@ -152,14 +160,19 @@ namespace MWRender
mStateset->setAttribute(new osg::VertexAttribDivisor(6, 1));
mStateset->setAttribute(new osg::VertexAttribDivisor(7, 1));
- mProgramTemplate = mSceneManager->getShaderManager().getProgramTemplate() ? static_cast<osg::Program*>(mSceneManager->getShaderManager().getProgramTemplate()->clone(osg::CopyOp::SHALLOW_COPY)) : new osg::Program;
+ mProgramTemplate = mSceneManager->getShaderManager().getProgramTemplate() ? Shader::ShaderManager::cloneProgram(mSceneManager->getShaderManager().getProgramTemplate()) : osg::ref_ptr<osg::Program>(new osg::Program);
mProgramTemplate->addBindAttribLocation("aOffset", 6);
mProgramTemplate->addBindAttribLocation("aRotation", 7);
}
+ Groundcover::~Groundcover()
+ {
+ }
+
void Groundcover::collectInstances(InstanceMap& instances, float size, const osg::Vec2f& center)
{
- const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
+ if (mDensity <=0.f) return;
+
osg::Vec2f minBound = (center - osg::Vec2f(size/2.f, size/2.f));
osg::Vec2f maxBound = (center + osg::Vec2f(size/2.f, size/2.f));
DensityCalculator calculator(mDensity);
@@ -169,35 +182,37 @@ namespace MWRender
{
for (int cellY = startCell.y(); cellY < startCell.y() + size; ++cellY)
{
- const ESM::Cell* cell = store.get<ESM::Cell>().searchStatic(cellX, cellY);
- if (!cell) continue;
+ ESM::Cell cell;
+ mGroundcoverStore.initCell(cell, cellX, cellY);
+ if (cell.mContextList.empty()) continue;
calculator.reset();
- for (size_t i=0; i<cell->mContextList.size(); ++i)
+ std::map<ESM::RefNum, ESM::CellRef> refs;
+ for (size_t i=0; i<cell.mContextList.size(); ++i)
{
- unsigned int index = cell->mContextList[i].index;
+ unsigned int index = cell.mContextList[i].index;
if (esm.size() <= index)
esm.resize(index+1);
- cell->restore(esm[index], i);
+ cell.restore(esm[index], i);
ESM::CellRef ref;
ref.mRefNum.unset();
bool deleted = false;
- while(cell->getNextRef(esm[index], ref, deleted))
+ while(cell.getNextRef(esm[index], ref, deleted))
{
- if (deleted) continue;
- if (!ref.mRefNum.fromGroundcoverFile()) continue;
-
- if (!calculator.isInstanceEnabled()) continue;
- if (!isInChunkBorders(ref, minBound, maxBound)) continue;
+ if (!deleted && refs.find(ref.mRefNum) == refs.end() && !calculator.isInstanceEnabled()) deleted = true;
+ if (!deleted && !isInChunkBorders(ref, minBound, maxBound)) deleted = true;
- Misc::StringUtils::lowerCaseInPlace(ref.mRefID);
- int type = store.findStatic(ref.mRefID);
- std::string model = getGroundcoverModel(type, ref.mRefID, store);
- if (model.empty()) continue;
- model = "meshes/" + model;
+ if (deleted) { refs.erase(ref.mRefNum); continue; }
+ refs[ref.mRefNum] = std::move(ref);
+ }
+ }
+ for (auto& pair : refs)
+ {
+ ESM::CellRef& ref = pair.second;
+ const std::string& model = mGroundcoverStore.getGroundcoverModel(ref.mRefID);
+ if (!model.empty())
instances[model].emplace_back(std::move(ref));
- }
}
}
}
@@ -220,10 +235,15 @@ namespace MWRender
group->addChild(node);
}
+ osg::ComputeBoundsVisitor cbv;
+ group->accept(cbv);
+ osg::BoundingBox box = cbv.getBoundingBox();
+ group->addCullCallback(new ViewDistanceCallback(getViewDistance(), box));
+
group->setStateSet(mStateset);
group->setNodeMask(Mask_Groundcover);
if (mSceneManager->getLightingMethod() != SceneUtil::LightingMethod::FFP)
- group->setCullCallback(new SceneUtil::LightListCallback);
+ group->addCullCallback(new SceneUtil::LightListCallback);
mSceneManager->recreateShaders(group, "groundcover", true, mProgramTemplate);
mSceneManager->shareState(group);
group->getBound();
diff --git a/apps/openmw/mwrender/groundcover.hpp b/apps/openmw/mwrender/groundcover.hpp
index ed88f7fe24..26ed8530aa 100644
--- a/apps/openmw/mwrender/groundcover.hpp
+++ b/apps/openmw/mwrender/groundcover.hpp
@@ -4,7 +4,16 @@
#include <components/terrain/quadtreeworld.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/esm/loadcell.hpp>
-#include <osg/Program>
+
+namespace MWWorld
+{
+ class ESMStore;
+ class GroundcoverStore;
+}
+namespace osg
+{
+ class Program;
+}
namespace MWRender
{
@@ -12,8 +21,8 @@ namespace MWRender
class Groundcover : public Resource::GenericResourceManager<GroundcoverChunkId>, public Terrain::QuadTreeWorld::ChunkManager
{
public:
- Groundcover(Resource::SceneManager* sceneManager, float density);
- ~Groundcover() = default;
+ Groundcover(Resource::SceneManager* sceneManager, float density, float viewDistance, const MWWorld::GroundcoverStore& store);
+ ~Groundcover();
osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override;
@@ -35,6 +44,7 @@ namespace MWRender
float mDensity;
osg::ref_ptr<osg::StateSet> mStateset;
osg::ref_ptr<osg::Program> mProgramTemplate;
+ const MWWorld::GroundcoverStore& mGroundcoverStore;
typedef std::map<std::string, std::vector<GroundcoverEntry>> InstanceMap;
osg::ref_ptr<osg::Node> createChunk(InstanceMap& instances, const osg::Vec2f& center);
diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp
index 70f0ff02bb..d9982d35c3 100644
--- a/apps/openmw/mwrender/localmap.cpp
+++ b/apps/openmw/mwrender/localmap.cpp
@@ -18,7 +18,7 @@
#include <components/settings/settings.hpp>
#include <components/sceneutil/visitor.hpp>
#include <components/sceneutil/shadow.hpp>
-#include <components/sceneutil/util.hpp>
+#include <components/sceneutil/depth.hpp>
#include <components/sceneutil/lightmanager.hpp>
#include <components/sceneutil/nodecallback.hpp>
#include <components/files/memorystream.hpp>
@@ -178,7 +178,7 @@ osg::ref_ptr<osg::Camera> LocalMap::createOrthographicCamera(float x, float y, f
{
osg::ref_ptr<osg::Camera> camera (new osg::Camera);
- if (SceneUtil::getReverseZ())
+ if (SceneUtil::AutoDepth::isReversed())
camera->setProjectionMatrix(SceneUtil::getReversedZProjectionMatrixAsOrtho(-width/2, width/2, -height/2, height/2, 5, (zmax-zmin) + 10));
else
camera->setProjectionMatrixAsOrtho(-width/2, width/2, -height/2, height/2, 5, (zmax-zmin) + 10);
@@ -198,14 +198,10 @@ osg::ref_ptr<osg::Camera> LocalMap::createOrthographicCamera(float x, float y, f
osg::Camera::CullingMode cullingMode = (osg::Camera::DEFAULT_CULLING|osg::Camera::FAR_PLANE_CULLING) & ~(osg::Camera::SMALL_FEATURE_CULLING);
camera->setCullingMode(cullingMode);
- osg::ref_ptr<osg::StateSet> stateset = new osg::StateSet;
- stateset->setAttribute(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::FILL), osg::StateAttribute::OVERRIDE);
-
- if (SceneUtil::getReverseZ())
- stateset->setAttributeAndModes(SceneUtil::createDepth(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
-
SceneUtil::setCameraClearDepth(camera);
+ osg::ref_ptr<osg::StateSet> stateset = new osg::StateSet;
+ stateset->setAttribute(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::FILL), osg::StateAttribute::OVERRIDE);
stateset->addUniform(new osg::Uniform("projectionMatrix", static_cast<osg::Matrixf>(camera->getProjectionMatrix())), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
// assign large value to effectively turn off fog
@@ -562,8 +558,8 @@ bool LocalMap::isPositionExplored (float nX, float nY, int x, int y)
if (!segment.mFogOfWarImage)
return false;
- nX = std::max(0.f, std::min(1.f, nX));
- nY = std::max(0.f, std::min(1.f, nY));
+ nX = std::clamp(nX, 0.f, 1.f);
+ nY = std::clamp(nY, 0.f, 1.f);
int texU = static_cast<int>((sFogOfWarResolution - 1) * nX);
int texV = static_cast<int>((sFogOfWarResolution - 1) * nY);
@@ -647,8 +643,7 @@ void LocalMap::updatePlayer (const osg::Vec3f& position, const osg::Quat& orient
uint32_t clr = *(uint32_t*)data;
uint8_t alpha = (clr >> 24);
-
- alpha = std::min( alpha, (uint8_t) (std::max(0.f, std::min(1.f, (sqrDist/sqrExploreRadius)))*255) );
+ alpha = std::min(alpha, (uint8_t)(std::clamp(sqrDist/sqrExploreRadius, 0.f, 1.f) * 255));
uint32_t val = (uint32_t) (alpha << 24);
if ( *data != val)
{
diff --git a/apps/openmw/mwrender/navmesh.cpp b/apps/openmw/mwrender/navmesh.cpp
index 523f7531af..a3c26aeb59 100644
--- a/apps/openmw/mwrender/navmesh.cpp
+++ b/apps/openmw/mwrender/navmesh.cpp
@@ -4,19 +4,25 @@
#include <components/sceneutil/navmesh.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
+#include <components/detournavigator/navmeshcacheitem.hpp>
+#include <components/sceneutil/detourdebugdraw.hpp>
#include <osg/PositionAttitudeTransform>
+#include <osg/StateSet>
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
+#include <limits>
+
namespace MWRender
{
NavMesh::NavMesh(const osg::ref_ptr<osg::Group>& root, bool enabled)
: mRootNode(root)
+ , mGroupStateSet(SceneUtil::makeNavMeshTileStateSet())
+ , mDebugDrawStateSet(SceneUtil::DebugDraw::makeStateSet())
, mEnabled(enabled)
- , mGeneration(0)
- , mRevision(0)
+ , mId(std::numeric_limits<std::size_t>::max())
{
}
@@ -36,46 +42,71 @@ namespace MWRender
return mEnabled;
}
- void NavMesh::update(const dtNavMesh& navMesh, const std::size_t id,
- const std::size_t generation, const std::size_t revision, const DetourNavigator::Settings& settings)
+ void NavMesh::update(const DetourNavigator::NavMeshCacheItem& navMesh, std::size_t id,
+ const DetourNavigator::Settings& settings)
{
- if (!mEnabled || (mGroup && mId == id && mGeneration == generation && mRevision == revision))
+ using DetourNavigator::TilePosition;
+ using DetourNavigator::Version;
+
+ if (!mEnabled || (!mTiles.empty() && mId == id && mVersion == navMesh.getVersion()))
return;
- mId = id;
- mGeneration = generation;
- mRevision = revision;
- if (mGroup)
- mRootNode->removeChild(mGroup);
- mGroup = SceneUtil::createNavMeshGroup(navMesh, settings);
- if (mGroup)
+ if (mId != id)
{
- MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(mGroup, "debug");
- mGroup->setNodeMask(Mask_Debug);
- mRootNode->addChild(mGroup);
+ reset();
+ mId = id;
+ }
+
+ mVersion = navMesh.getVersion();
+
+ std::vector<TilePosition> updated;
+ navMesh.forEachUsedTile([&] (const TilePosition& position, const Version& version, const dtMeshTile& meshTile)
+ {
+ updated.push_back(position);
+ Tile& tile = mTiles[position];
+ if (tile.mGroup != nullptr && tile.mVersion == version)
+ return;
+ if (tile.mGroup != nullptr)
+ mRootNode->removeChild(tile.mGroup);
+ tile.mGroup = SceneUtil::createNavMeshTileGroup(navMesh.getImpl(), meshTile, settings,
+ mGroupStateSet, mDebugDrawStateSet);
+ if (tile.mGroup == nullptr)
+ return;
+ MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(tile.mGroup, "debug");
+ tile.mGroup->setNodeMask(Mask_Debug);
+ mRootNode->addChild(tile.mGroup);
+ });
+ std::sort(updated.begin(), updated.end());
+ for (auto it = mTiles.begin(); it != mTiles.end();)
+ {
+ if (!std::binary_search(updated.begin(), updated.end(), it->first))
+ {
+ mRootNode->removeChild(it->second.mGroup);
+ it = mTiles.erase(it);
+ }
+ else
+ ++it;
}
}
void NavMesh::reset()
{
- if (mGroup)
- {
- mRootNode->removeChild(mGroup);
- mGroup = nullptr;
- }
+ for (auto& [position, tile] : mTiles)
+ mRootNode->removeChild(tile.mGroup);
+ mTiles.clear();
}
void NavMesh::enable()
{
- if (mGroup)
- mRootNode->addChild(mGroup);
+ for (const auto& [position, tile] : mTiles)
+ mRootNode->addChild(tile.mGroup);
mEnabled = true;
}
void NavMesh::disable()
{
- if (mGroup)
- mRootNode->removeChild(mGroup);
+ for (const auto& [position, tile] : mTiles)
+ mRootNode->removeChild(tile.mGroup);
mEnabled = false;
}
}
diff --git a/apps/openmw/mwrender/navmesh.hpp b/apps/openmw/mwrender/navmesh.hpp
index d329b895d7..fd69a3e487 100644
--- a/apps/openmw/mwrender/navmesh.hpp
+++ b/apps/openmw/mwrender/navmesh.hpp
@@ -1,14 +1,27 @@
#ifndef OPENMW_MWRENDER_NAVMESH_H
#define OPENMW_MWRENDER_NAVMESH_H
-#include <components/detournavigator/navigator.hpp>
+#include <components/detournavigator/version.hpp>
+#include <components/detournavigator/tileposition.hpp>
#include <osg/ref_ptr>
+#include <cstddef>
+#include <map>
+
+class dtNavMesh;
+
namespace osg
{
class Group;
class Geometry;
+ class StateSet;
+}
+
+namespace DetourNavigator
+{
+ class NavMeshCacheItem;
+ struct Settings;
}
namespace MWRender
@@ -21,8 +34,8 @@ namespace MWRender
bool toggle();
- void update(const dtNavMesh& navMesh, const std::size_t number, const std::size_t generation,
- const std::size_t revision, const DetourNavigator::Settings& settings);
+ void update(const DetourNavigator::NavMeshCacheItem& navMesh, std::size_t id,
+ const DetourNavigator::Settings& settings);
void reset();
@@ -36,12 +49,19 @@ namespace MWRender
}
private:
+ struct Tile
+ {
+ DetourNavigator::Version mVersion;
+ osg::ref_ptr<osg::Group> mGroup;
+ };
+
osg::ref_ptr<osg::Group> mRootNode;
+ osg::ref_ptr<osg::StateSet> mGroupStateSet;
+ osg::ref_ptr<osg::StateSet> mDebugDrawStateSet;
bool mEnabled;
- std::size_t mId = std::numeric_limits<std::size_t>::max();
- std::size_t mGeneration;
- std::size_t mRevision;
- osg::ref_ptr<osg::Group> mGroup;
+ std::size_t mId;
+ DetourNavigator::Version mVersion;
+ std::map<DetourNavigator::TilePosition, Tile> mTiles;
};
}
diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp
index 0d6c21c308..da361aec25 100644
--- a/apps/openmw/mwrender/npcanimation.cpp
+++ b/apps/openmw/mwrender/npcanimation.cpp
@@ -20,6 +20,7 @@
#include <components/sceneutil/visitor.hpp>
#include <components/sceneutil/skeleton.hpp>
#include <components/sceneutil/keyframe.hpp>
+#include <components/sceneutil/depth.hpp>
#include <components/settings/settings.hpp>
@@ -310,7 +311,7 @@ class DepthClearCallback : public osgUtil::RenderBin::DrawCallback
public:
DepthClearCallback()
{
- mDepth = SceneUtil::createDepth();
+ mDepth = new SceneUtil::AutoDepth;
mDepth->setWriteMask(true);
mStateSet = new osg::StateSet;
@@ -837,14 +838,18 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g
}
}
}
+ SceneUtil::ForceControllerSourcesVisitor assignVisitor(src);
+ node->accept(assignVisitor);
}
- else if (type == ESM::PRT_Weapon)
- src = mWeaponAnimationTime;
else
- src.reset(new NullAnimationTime);
-
- SceneUtil::AssignControllerSourcesVisitor assignVisitor(src);
- node->accept(assignVisitor);
+ {
+ if (type == ESM::PRT_Weapon)
+ src = mWeaponAnimationTime;
+ else
+ src.reset(new NullAnimationTime);
+ SceneUtil::AssignControllerSourcesVisitor assignVisitor(src);
+ node->accept(assignVisitor);
+ }
}
return true;
diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp
index b6f12dea37..756769bc7d 100644
--- a/apps/openmw/mwrender/objectpaging.cpp
+++ b/apps/openmw/mwrender/objectpaging.cpp
@@ -359,6 +359,7 @@ namespace MWRender
stateset->setAttribute(m);
stateset->addUniform(new osg::Uniform("colorMode", 0));
stateset->addUniform(new osg::Uniform("emissiveMult", 1.f));
+ stateset->addUniform(new osg::Uniform("specStrength", 1.f));
node.setStateSet(stateset);
}
};
@@ -431,7 +432,6 @@ namespace MWRender
int type = store.findStatic(ref.mRefID);
if (!typeFilter(type,size>=2)) continue;
if (deleted) { refs.erase(ref.mRefNum); continue; }
- if (ref.mRefNum.fromGroundcoverFile()) continue;
refs[ref.mRefNum] = std::move(ref);
}
}
@@ -733,12 +733,8 @@ namespace MWRender
}
void clampToCell(osg::Vec3f& cellPos)
{
- osg::Vec2i min (mCell.x(), mCell.y());
- osg::Vec2i max (mCell.x()+1, mCell.y()+1);
- if (cellPos.x() < min.x()) cellPos.x() = min.x();
- if (cellPos.x() > max.x()) cellPos.x() = max.x();
- if (cellPos.y() < min.y()) cellPos.y() = min.y();
- if (cellPos.y() > max.y()) cellPos.y() = max.y();
+ cellPos.x() = std::clamp<float>(cellPos.x(), mCell.x(), mCell.x() + 1);
+ cellPos.y() = std::clamp<float>(cellPos.y(), mCell.y(), mCell.y() + 1);
}
osg::Vec3f mPosition;
osg::Vec2i mCell;
diff --git a/apps/openmw/mwrender/objects.cpp b/apps/openmw/mwrender/objects.cpp
index f51008fffa..e208d7191e 100644
--- a/apps/openmw/mwrender/objects.cpp
+++ b/apps/openmw/mwrender/objects.cpp
@@ -141,11 +141,11 @@ void Objects::removeCell(const MWWorld::CellStore* store)
MWWorld::Ptr ptr = iter->second->getPtr();
if(ptr.getCell() == store)
{
- if (ptr.getClass().isNpc() && ptr.getRefData().getCustomData())
+ if (ptr.getClass().isActor() && ptr.getRefData().getCustomData())
{
- MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr);
- invStore.setInvListener(nullptr, ptr);
- invStore.setContListener(nullptr);
+ if (ptr.getClass().hasInventoryStore(ptr))
+ ptr.getClass().getInventoryStore(ptr).setInvListener(nullptr, ptr);
+ ptr.getClass().getContainerStore(ptr).setContListener(nullptr);
}
mObjects.erase(iter++);
diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp
index 1ee5745cb4..6db01444ad 100644
--- a/apps/openmw/mwrender/postprocessor.cpp
+++ b/apps/openmw/mwrender/postprocessor.cpp
@@ -9,12 +9,11 @@
#include <osgViewer/Viewer>
#include <components/settings/settings.hpp>
-#include <components/sceneutil/util.hpp>
+#include <components/sceneutil/depth.hpp>
#include <components/sceneutil/nodecallback.hpp>
#include <components/debug/debuglog.hpp>
#include "vismask.hpp"
-#include "renderingmanager.hpp"
namespace
{
@@ -93,17 +92,74 @@ namespace
MWRender::PostProcessor* mPostProcessor;
};
+
+ // Copies the currently bound depth attachment to a new texture so drawables in transparent renderbin can safely sample from depth.
+ class OpaqueDepthCopyCallback : public osgUtil::RenderBin::DrawCallback
+ {
+ public:
+ OpaqueDepthCopyCallback(osg::ref_ptr<osg::Texture2D> opaqueDepthTex, osg::ref_ptr<osg::FrameBufferObject> sourceFbo)
+ : mOpaqueDepthFbo(new osg::FrameBufferObject)
+ , mSourceFbo(sourceFbo)
+ , mOpaqueDepthTex(opaqueDepthTex)
+ , mColorAttached(false)
+ {
+ mOpaqueDepthFbo->setAttachment(osg::FrameBufferObject::BufferComponent::DEPTH_BUFFER, osg::FrameBufferAttachment(opaqueDepthTex));
+
+#ifdef __APPLE__
+ // Mac OS drivers complain that a FBO is incomplete if it has no color attachment
+ mOpaqueDepthFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER, osg::FrameBufferAttachment(new osg::RenderBuffer(mOpaqueDepthTex->getTextureWidth(), mOpaqueDepthTex->getTextureHeight(), GL_RGB)));
+ mColorAttached = true;
+#endif
+ }
+
+ void drawImplementation(osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override
+ {
+ if (bin->getStage()->getFrameBufferObject() == mSourceFbo)
+ {
+ osg::State& state = *renderInfo.getState();
+ osg::GLExtensions* ext = state.get<osg::GLExtensions>();
+
+ mSourceFbo->apply(state, osg::FrameBufferObject::READ_FRAMEBUFFER);
+ postBindOperation(state);
+
+ mOpaqueDepthFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER);
+ postBindOperation(state);
+
+ ext->glBlitFramebuffer(0, 0, mOpaqueDepthTex->getTextureWidth(), mOpaqueDepthTex->getTextureHeight(), 0, 0, mOpaqueDepthTex->getTextureWidth(), mOpaqueDepthTex->getTextureHeight(), GL_DEPTH_BUFFER_BIT, GL_NEAREST);
+
+ mSourceFbo->apply(state);
+ }
+
+ bin->drawImplementation(renderInfo, previous);
+ }
+ private:
+ void postBindOperation(osg::State& state)
+ {
+ if (mColorAttached)
+ return;
+ #if !defined(OSG_GLES1_AVAILABLE) && !defined(OSG_GLES2_AVAILABLE) && !defined(OSG_GLES3_AVAILABLE)
+ state.glDrawBuffer(GL_NONE);
+ state.glReadBuffer(GL_NONE);
+ #endif
+ }
+
+ osg::ref_ptr<osg::FrameBufferObject> mOpaqueDepthFbo;
+ osg::ref_ptr<osg::FrameBufferObject> mSourceFbo;
+ osg::ref_ptr<osg::Texture2D> mOpaqueDepthTex;
+ bool mColorAttached;
+ };
}
namespace MWRender
{
- PostProcessor::PostProcessor(RenderingManager& rendering, osgViewer::Viewer* viewer, osg::Group* rootNode)
+ PostProcessor::PostProcessor(osgViewer::Viewer* viewer, osg::Group* rootNode)
: mViewer(viewer)
, mRootNode(new osg::Group)
, mDepthFormat(GL_DEPTH_COMPONENT24)
- , mRendering(rendering)
{
- if (!SceneUtil::getReverseZ())
+ bool softParticles = Settings::Manager::getBool("soft particles", "Shaders");
+
+ if (!SceneUtil::AutoDepth::isReversed() && !softParticles)
return;
osg::GraphicsContext* gc = viewer->getCamera()->getGraphicsContext();
@@ -124,17 +180,22 @@ namespace MWRender
return;
}
- if (osg::isGLExtensionSupported(contextID, "GL_ARB_depth_buffer_float"))
- mDepthFormat = GL_DEPTH_COMPONENT32F;
- else if (osg::isGLExtensionSupported(contextID, "GL_NV_depth_buffer_float"))
- mDepthFormat = GL_DEPTH_COMPONENT32F_NV;
- else
+ if (SceneUtil::AutoDepth::isReversed())
{
- // TODO: Once we have post-processing implemented we want to skip this return and continue with setup.
- // Rendering to a FBO to fullscreen geometry has overhead (especially when MSAA is enabled) and there are no
- // benefits if no floating point depth formats are supported.
- Log(Debug::Warning) << errPreamble << "'GL_ARB_depth_buffer_float' and 'GL_NV_depth_buffer_float' unsupported.";
- return;
+ if (osg::isGLExtensionSupported(contextID, "GL_ARB_depth_buffer_float"))
+ mDepthFormat = GL_DEPTH_COMPONENT32F;
+ else if (osg::isGLExtensionSupported(contextID, "GL_NV_depth_buffer_float"))
+ mDepthFormat = GL_DEPTH_COMPONENT32F_NV;
+ else
+ {
+ // TODO: Once we have post-processing implemented we want to skip this return and continue with setup.
+ // Rendering to a FBO to fullscreen geometry has overhead (especially when MSAA is enabled) and there are no
+ // benefits if no floating point depth formats are supported.
+ Log(Debug::Warning) << errPreamble << "'GL_ARB_depth_buffer_float' and 'GL_NV_depth_buffer_float' unsupported.";
+
+ if (!softParticles)
+ return;
+ }
}
int width = viewer->getCamera()->getViewport()->width();
@@ -165,6 +226,12 @@ namespace MWRender
mDepthTex->dirtyTextureObject();
mSceneTex->dirtyTextureObject();
+ if (mOpaqueDepthTex)
+ {
+ mOpaqueDepthTex->setTextureSize(width, height);
+ mOpaqueDepthTex->dirtyTextureObject();
+ }
+
int samples = Settings::Manager::getInt("antialiasing", "Video");
mFbo = new osg::FrameBufferObject;
@@ -186,9 +253,11 @@ namespace MWRender
if (const auto depthProxy = std::getenv("OPENMW_ENABLE_DEPTH_CLEAR_PROXY"))
mFirstPersonDepthRBProxy = new osg::RenderBuffer(width, height, mDepthTex->getInternalFormat(), samples);
+ if (Settings::Manager::getBool("soft particles", "Shaders"))
+ osgUtil::RenderBin::getRenderBinPrototype("DepthSortedBin")->setDrawCallback(new OpaqueDepthCopyCallback(mOpaqueDepthTex, mMsaaFbo ? mMsaaFbo : mFbo));
+
mViewer->getCamera()->resize(width, height);
mHUDCamera->resize(width, height);
- mRendering.updateProjectionMatrix();
}
void PostProcessor::createTexturesAndCamera(int width, int height)
@@ -204,6 +273,12 @@ namespace MWRender
mDepthTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE);
mDepthTex->setResizeNonPowerOfTwoHint(false);
+ if (Settings::Manager::getBool("soft particles", "Shaders"))
+ {
+ mOpaqueDepthTex = new osg::Texture2D(*mDepthTex);
+ mOpaqueDepthTex->setName("opaqueTexMap");
+ }
+
mSceneTex = new osg::Texture2D;
mSceneTex->setTextureSize(width, height);
mSceneTex->setSourceFormat(GL_RGB);
diff --git a/apps/openmw/mwrender/postprocessor.hpp b/apps/openmw/mwrender/postprocessor.hpp
index 0d03d4b500..f2ef238737 100644
--- a/apps/openmw/mwrender/postprocessor.hpp
+++ b/apps/openmw/mwrender/postprocessor.hpp
@@ -14,18 +14,17 @@ namespace osgViewer
namespace MWRender
{
- class RenderingManager;
-
class PostProcessor : public osg::Referenced
{
public:
- PostProcessor(RenderingManager& rendering, osgViewer::Viewer* viewer, osg::Group* rootNode);
+ PostProcessor(osgViewer::Viewer* viewer, osg::Group* rootNode);
auto getMsaaFbo() { return mMsaaFbo; }
auto getFbo() { return mFbo; }
auto getFirstPersonRBProxy() { return mFirstPersonDepthRBProxy; }
int getDepthFormat() { return mDepthFormat; }
+ osg::ref_ptr<osg::Texture2D> getOpaqueDepthTex() { return mOpaqueDepthTex; }
void resize(int width, int height);
@@ -42,10 +41,9 @@ namespace MWRender
osg::ref_ptr<osg::Texture2D> mSceneTex;
osg::ref_ptr<osg::Texture2D> mDepthTex;
+ osg::ref_ptr<osg::Texture2D> mOpaqueDepthTex;
int mDepthFormat;
-
- RenderingManager& mRendering;
};
}
diff --git a/apps/openmw/mwrender/recastmesh.cpp b/apps/openmw/mwrender/recastmesh.cpp
index 91035907e9..5f202720b2 100644
--- a/apps/openmw/mwrender/recastmesh.cpp
+++ b/apps/openmw/mwrender/recastmesh.cpp
@@ -3,6 +3,8 @@
#include <components/sceneutil/recastmesh.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
+#include <components/detournavigator/settings.hpp>
+#include <components/detournavigator/recastmesh.hpp>
#include <osg/PositionAttitudeTransform>
@@ -52,7 +54,7 @@ namespace MWRender
if (it->second.mGeneration != tile->second->getGeneration()
|| it->second.mRevision != tile->second->getRevision())
{
- const auto group = SceneUtil::createRecastMeshGroup(*tile->second, settings);
+ const auto group = SceneUtil::createRecastMeshGroup(*tile->second, settings.mRecast);
MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug");
group->setNodeMask(Mask_Debug);
mRootNode->removeChild(it->second.mValue);
@@ -69,7 +71,7 @@ namespace MWRender
{
if (mGroups.count(tile.first))
continue;
- const auto group = SceneUtil::createRecastMeshGroup(*tile.second, settings);
+ const auto group = SceneUtil::createRecastMeshGroup(*tile.second, settings.mRecast);
MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug");
group->setNodeMask(Mask_Debug);
mGroups.emplace(tile.first, Group {tile.second->getGeneration(), tile.second->getRevision(), group});
diff --git a/apps/openmw/mwrender/recastmesh.hpp b/apps/openmw/mwrender/recastmesh.hpp
index 729438dbe5..194ec04a62 100644
--- a/apps/openmw/mwrender/recastmesh.hpp
+++ b/apps/openmw/mwrender/recastmesh.hpp
@@ -1,7 +1,7 @@
#ifndef OPENMW_MWRENDER_RECASTMESH_H
#define OPENMW_MWRENDER_RECASTMESH_H
-#include <components/detournavigator/navigator.hpp>
+#include <components/detournavigator/recastmeshtiles.hpp>
#include <osg/ref_ptr>
@@ -13,6 +13,11 @@ namespace osg
class Geometry;
}
+namespace DetourNavigator
+{
+ struct Settings;
+}
+
namespace MWRender
{
class RecastMesh
diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp
index c4ef1d9d9b..1925494ccd 100644
--- a/apps/openmw/mwrender/renderingmanager.cpp
+++ b/apps/openmw/mwrender/renderingmanager.cpp
@@ -32,7 +32,7 @@
#include <components/settings/settings.hpp>
-#include <components/sceneutil/util.hpp>
+#include <components/sceneutil/depth.hpp>
#include <components/sceneutil/lightmanager.hpp>
#include <components/sceneutil/statesetupdater.hpp>
#include <components/sceneutil/positionattitudetransform.hpp>
@@ -49,6 +49,7 @@
#include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp"
+#include "../mwworld/groundcoverstore.hpp"
#include "../mwgui/loadingscreen.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwmechanics/actorutil.hpp"
@@ -59,7 +60,6 @@
#include "vismask.hpp"
#include "pathgrid.hpp"
#include "camera.hpp"
-#include "viewovershoulder.hpp"
#include "water.hpp"
#include "terrainstorage.hpp"
#include "navmesh.hpp"
@@ -91,6 +91,7 @@ namespace MWRender
stateset->addUniform(new osg::Uniform("linearFac", 0.f));
stateset->addUniform(new osg::Uniform("near", 0.f));
stateset->addUniform(new osg::Uniform("far", 0.f));
+ stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{}));
if (mUsePlayerUniforms)
{
stateset->addUniform(new osg::Uniform("windSpeed", 0.0f));
@@ -116,6 +117,10 @@ namespace MWRender
if (uFar)
uFar->set(mFar);
+ auto* uScreenRes = stateset->getUniform("screenRes");
+ if (uScreenRes)
+ uScreenRes->set(mScreenRes);
+
if (mUsePlayerUniforms)
{
auto* windSpeed = stateset->getUniform("windSpeed");
@@ -148,6 +153,11 @@ namespace MWRender
mFar = far;
}
+ void setScreenRes(float width, float height)
+ {
+ mScreenRes = osg::Vec2f(width, height);
+ }
+
void setWindSpeed(float windSpeed)
{
mWindSpeed = windSpeed;
@@ -167,6 +177,7 @@ namespace MWRender
bool mUsePlayerUniforms;
float mWindSpeed;
osg::Vec3f mPlayerPos;
+ osg::Vec2f mScreenRes;
};
class StateUpdater : public SceneUtil::StateSetUpdater
@@ -283,7 +294,7 @@ namespace MWRender
RenderingManager::RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr<osg::Group> rootNode,
Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue,
- const std::string& resourcePath, DetourNavigator::Navigator& navigator)
+ const std::string& resourcePath, DetourNavigator::Navigator& navigator, const MWWorld::GroundcoverStore& groundcoverStore)
: mViewer(viewer)
, mRootNode(rootNode)
, mResourceSystem(resourceSystem)
@@ -297,20 +308,16 @@ namespace MWRender
, mViewDistance(Settings::Manager::getFloat("viewing distance", "Camera"))
, mFieldOfViewOverridden(false)
, mFieldOfViewOverride(0.f)
- , mFieldOfView(std::min(std::max(1.f, Settings::Manager::getFloat("field of view", "Camera")), 179.f))
+ , mFieldOfView(std::clamp(Settings::Manager::getFloat("field of view", "Camera"), 1.f, 179.f))
+ , mFirstPersonFieldOfView(std::clamp(Settings::Manager::getFloat("first person field of view", "Camera"), 1.f, 179.f))
{
- bool reverseZ = SceneUtil::getReverseZ();
-
- if (reverseZ)
- Log(Debug::Info) << "Using reverse-z depth buffer";
- else
- Log(Debug::Info) << "Using standard depth buffer";
-
+ bool reverseZ = SceneUtil::AutoDepth::isReversed();
auto lightingMethod = SceneUtil::LightManager::getLightingMethodFromString(Settings::Manager::getString("lighting method", "Shaders"));
resourceSystem->getSceneManager()->setParticleSystemMask(MWRender::Mask_ParticleSystem);
// Shadows and radial fog have problems with fixed-function mode
bool forceShaders = Settings::Manager::getBool("radial fog", "Shaders")
+ || Settings::Manager::getBool("soft particles", "Shaders")
|| Settings::Manager::getBool("force shaders", "Shaders")
|| Settings::Manager::getBool("enable shadows", "Shadows")
|| lightingMethod != SceneUtil::LightingMethod::FFP
@@ -438,11 +445,9 @@ namespace MWRender
float density = Settings::Manager::getFloat("density", "Groundcover");
density = std::clamp(density, 0.f, 1.f);
- mGroundcover.reset(new Groundcover(mResourceSystem->getSceneManager(), density));
+ mGroundcover.reset(new Groundcover(mResourceSystem->getSceneManager(), density, groundcoverDistance, groundcoverStore));
static_cast<Terrain::QuadTreeWorld*>(mTerrain.get())->addChunkManager(mGroundcover.get());
mResourceSystem->addResourceManager(mGroundcover.get());
-
- mGroundcover->setViewDistance(groundcoverDistance);
}
mStateUpdater = new StateUpdater;
@@ -451,8 +456,9 @@ namespace MWRender
mSharedUniformStateUpdater = new SharedUniformStateUpdater(groundcover);
rootNode->addUpdateCallback(mSharedUniformStateUpdater);
- mPostProcessor = new PostProcessor(*this, viewer, mRootNode);
+ mPostProcessor = new PostProcessor(viewer, mRootNode);
resourceSystem->getSceneManager()->setDepthFormat(mPostProcessor->getDepthFormat());
+ resourceSystem->getSceneManager()->setOpaqueDepthTex(mPostProcessor->getOpaqueDepthTex());
if (reverseZ && !SceneUtil::isFloatingPointDepthFormat(mPostProcessor->getDepthFormat()))
Log(Debug::Warning) << "Floating point depth format not in use but reverse-z buffer is enabled, consider disabling it.";
@@ -461,8 +467,6 @@ namespace MWRender
mWater.reset(new Water(sceneRoot->getParent(0), sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), resourcePath));
mCamera.reset(new Camera(mViewer->getCamera()));
- if (Settings::Manager::getBool("view over shoulder", "Camera"))
- mViewOverShoulderController.reset(new ViewOverShoulderController(mCamera.get()));
mScreenshotManager.reset(new ScreenshotManager(viewer, mRootNode, sceneRoot, mResourceSystem, mWater.get()));
@@ -489,6 +493,7 @@ namespace MWRender
defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f));
sceneRoot->getOrCreateStateSet()->setAttribute(defaultMat);
sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("emissiveMult", 1.f));
+ sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("specStrength", 1.f));
mFog.reset(new FogManager());
@@ -517,8 +522,6 @@ namespace MWRender
NifOsg::Loader::setIntersectionDisabledNodeMask(Mask_Effect);
Nif::NIFFile::setLoadUnsupportedFiles(Settings::Manager::getBool("load unsupported nif files", "Models"));
- float firstPersonFov = Settings::Manager::getFloat("first person field of view", "Camera");
- mFirstPersonFieldOfView = std::min(std::max(1.f, firstPersonFov), 179.f);
mStateUpdater->setFogEnd(mViewDistance);
mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("simpleWater", false));
@@ -531,7 +534,7 @@ namespace MWRender
if (reverseZ)
{
osg::ref_ptr<osg::ClipControl> clipcontrol = new osg::ClipControl(osg::ClipControl::LOWER_LEFT, osg::ClipControl::ZERO_TO_ONE);
- mRootNode->getOrCreateStateSet()->setAttributeAndModes(SceneUtil::createDepth(), osg::StateAttribute::ON);
+ mRootNode->getOrCreateStateSet()->setAttributeAndModes(new SceneUtil::AutoDepth, osg::StateAttribute::ON);
mRootNode->getOrCreateStateSet()->setAttributeAndModes(clipcontrol, osg::StateAttribute::ON);
}
@@ -816,15 +819,9 @@ namespace MWRender
updateNavMesh();
updateRecastMesh();
- if (mViewOverShoulderController)
- mViewOverShoulderController->update();
mCamera->update(dt, paused);
- osg::Vec3d focal, cameraPos;
- mCamera->getPosition(focal, cameraPos);
- mCurrentCameraPos = cameraPos;
-
- bool isUnderwater = mWater->isUnderwater(cameraPos);
+ bool isUnderwater = mWater->isUnderwater(mCamera->getPosition());
mStateUpdater->setFogStart(mFog->getFogStart(isUnderwater));
mStateUpdater->setFogEnd(mFog->getFogEnd(isUnderwater));
setFogColor(mFog->getFogColor(isUnderwater));
@@ -1142,14 +1139,17 @@ namespace MWRender
void RenderingManager::updateProjectionMatrix()
{
- double aspect = mViewer->getCamera()->getViewport()->aspectRatio();
+ double width = Settings::Manager::getInt("resolution x", "Video");
+ double height = Settings::Manager::getInt("resolution y", "Video");
+
+ double aspect = (height == 0.0) ? 1.0 : width / height;
float fov = mFieldOfView;
if (mFieldOfViewOverridden)
fov = mFieldOfViewOverride;
mViewer->getCamera()->setProjectionMatrixAsPerspective(fov, aspect, mNearClip, mViewDistance);
- if (SceneUtil::getReverseZ())
+ if (SceneUtil::AutoDepth::isReversed())
{
mSharedUniformStateUpdater->setLinearFac(-mNearClip / (mViewDistance - mNearClip) - 1.f);
mSharedUniformStateUpdater->setProjectionMatrix(SceneUtil::getReversedZProjectionMatrixAsPerspective(fov, aspect, mNearClip, mViewDistance));
@@ -1159,6 +1159,7 @@ namespace MWRender
mSharedUniformStateUpdater->setNear(mNearClip);
mSharedUniformStateUpdater->setFar(mViewDistance);
+ mSharedUniformStateUpdater->setScreenRes(width, height);
// Since our fog is not radial yet, we should take FOV in account, otherwise terrain near viewing distance may disappear.
// Limit FOV here just for sure, otherwise viewing distance can be too high.
@@ -1212,19 +1213,26 @@ namespace MWRender
void RenderingManager::processChangedSettings(const Settings::CategorySettingVector &changed)
{
+ // Only perform a projection matrix update once if a relevant setting is changed.
+ bool updateProjection = false;
+
for (Settings::CategorySettingVector::const_iterator it = changed.begin(); it != changed.end(); ++it)
{
if (it->first == "Camera" && it->second == "field of view")
{
mFieldOfView = Settings::Manager::getFloat("field of view", "Camera");
- updateProjectionMatrix();
+ updateProjection = true;
+ }
+ else if (it->first == "Video" && (it->second == "resolution x" || it->second == "resolution y"))
+ {
+ updateProjection = true;
}
else if (it->first == "Camera" && it->second == "viewing distance")
{
mViewDistance = Settings::Manager::getFloat("viewing distance", "Camera");
if(!Settings::Manager::getBool("use distant fog", "Fog"))
mStateUpdater->setFogEnd(mViewDistance);
- updateProjectionMatrix();
+ updateProjection = true;
}
else if (it->first == "General" && (it->second == "texture filter" ||
it->second == "texture mipmap" ||
@@ -1267,6 +1275,11 @@ namespace MWRender
}
}
}
+
+ if (updateProjection)
+ {
+ updateProjectionMatrix();
+ }
}
float RenderingManager::getNearClipDistance() const
@@ -1369,9 +1382,7 @@ namespace MWRender
{
try
{
- const auto locked = it->second->lockConst();
- mNavMesh->update(locked->getImpl(), mNavMeshNumber, locked->getGeneration(),
- locked->getNavMeshRevision(), mNavigator.getSettings());
+ mNavMesh->update(*it->second->lockConst(), mNavMeshNumber, mNavigator.getSettings());
}
catch (const std::exception& e)
{
diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp
index b8d5d955c8..119c85f82f 100644
--- a/apps/openmw/mwrender/renderingmanager.hpp
+++ b/apps/openmw/mwrender/renderingmanager.hpp
@@ -67,6 +67,11 @@ namespace DetourNavigator
struct Settings;
}
+namespace MWWorld
+{
+ class GroundcoverStore;
+}
+
namespace MWRender
{
class StateUpdater;
@@ -79,7 +84,6 @@ namespace MWRender
class NpcAnimation;
class Pathgrid;
class Camera;
- class ViewOverShoulderController;
class Water;
class TerrainStorage;
class LandManager;
@@ -95,7 +99,7 @@ namespace MWRender
public:
RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr<osg::Group> rootNode,
Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue,
- const std::string& resourcePath, DetourNavigator::Navigator& navigator);
+ const std::string& resourcePath, DetourNavigator::Navigator& navigator, const MWWorld::GroundcoverStore& groundcoverStore);
~RenderingManager();
osgUtil::IncrementalCompileOperation* getIncrementalCompileOperation();
@@ -207,7 +211,6 @@ namespace MWRender
// camera stuff
Camera* getCamera() { return mCamera.get(); }
- const osg::Vec3f& getCameraPosition() const { return mCurrentCameraPos; }
/// temporarily override the field of view with given value.
void overrideFieldOfView(float val);
@@ -284,8 +287,6 @@ namespace MWRender
osg::ref_ptr<NpcAnimation> mPlayerAnimation;
osg::ref_ptr<SceneUtil::PositionAttitudeTransform> mPlayerNode;
std::unique_ptr<Camera> mCamera;
- std::unique_ptr<ViewOverShoulderController> mViewOverShoulderController;
- osg::Vec3f mCurrentCameraPos;
osg::ref_ptr<StateUpdater> mStateUpdater;
osg::ref_ptr<SharedUniformStateUpdater> mSharedUniformStateUpdater;
diff --git a/apps/openmw/mwrender/ripplesimulation.cpp b/apps/openmw/mwrender/ripplesimulation.cpp
index fdfc3db63d..282362ea56 100644
--- a/apps/openmw/mwrender/ripplesimulation.cpp
+++ b/apps/openmw/mwrender/ripplesimulation.cpp
@@ -16,7 +16,7 @@
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/fallback/fallback.hpp>
-#include <components/sceneutil/util.hpp>
+#include <components/sceneutil/depth.hpp>
#include "vismask.hpp"
@@ -56,13 +56,13 @@ namespace
stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
stateset->setTextureAttributeAndModes(0, textures[0], osg::StateAttribute::ON);
- auto depth = SceneUtil::createDepth();
+ osg::ref_ptr<osg::Depth> depth = new SceneUtil::AutoDepth;
depth->setWriteMask(false);
stateset->setAttributeAndModes(depth, osg::StateAttribute::ON);
osg::ref_ptr<osg::PolygonOffset> polygonOffset (new osg::PolygonOffset);
- polygonOffset->setUnits(SceneUtil::getReverseZ() ? 1 : -1);
- polygonOffset->setFactor(SceneUtil::getReverseZ() ? 1 : -1);
+ polygonOffset->setUnits(SceneUtil::AutoDepth::isReversed() ? 1 : -1);
+ polygonOffset->setFactor(SceneUtil::AutoDepth::isReversed() ? 1 : -1);
stateset->setAttributeAndModes(polygonOffset, osg::StateAttribute::ON);
stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
diff --git a/apps/openmw/mwrender/screenshotmanager.cpp b/apps/openmw/mwrender/screenshotmanager.cpp
index ab7d0d93f0..5c3d925c9b 100644
--- a/apps/openmw/mwrender/screenshotmanager.cpp
+++ b/apps/openmw/mwrender/screenshotmanager.cpp
@@ -12,7 +12,7 @@
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/shader/shadermanager.hpp>
-#include <components/sceneutil/util.hpp>
+#include <components/sceneutil/depth.hpp>
#include <components/settings/settings.hpp>
@@ -91,15 +91,18 @@ namespace MWRender
int width = screenW - leftPadding*2;
int height = screenH - topPadding*2;
- // Ensure we are reading from the resolved framebuffer and not the multisampled render buffer when in use.
- // glReadPixel() cannot read from multisampled targets.
+ // Ensure we are reading from the resolved framebuffer and not the multisampled render buffer. Also ensure that the readbuffer is set correctly with rendeirng to FBO.
+ // glReadPixel() cannot read from multisampled targets
PostProcessor* postProcessor = dynamic_cast<PostProcessor*>(renderInfo.getCurrentCamera()->getUserData());
- if (postProcessor && postProcessor->getFbo() && postProcessor->getMsaaFbo())
+ if (postProcessor && postProcessor->getFbo())
{
osg::GLExtensions* ext = osg::GLExtensions::Get(renderInfo.getContextID(), false);
if (ext)
+ {
ext->glBindFramebuffer(GL_FRAMEBUFFER_EXT, postProcessor->getFbo()->getHandle(renderInfo.getContextID()));
+ renderInfo.getState()->glReadBuffer(GL_COLOR_ATTACHMENT0_EXT);
+ }
}
mImage->readPixels(leftPadding, topPadding, width, height, GL_RGB, GL_UNSIGNED_BYTE);
@@ -331,7 +334,7 @@ namespace MWRender
float nearClip = Settings::Manager::getFloat("near clip", "Camera");
float viewDistance = Settings::Manager::getFloat("viewing distance", "Camera");
// each cubemap side sees 90 degrees
- if (SceneUtil::getReverseZ())
+ if (SceneUtil::AutoDepth::isReversed())
rttCamera->setProjectionMatrix(SceneUtil::getReversedZProjectionMatrixAsPerspectiveInf(90.0, w/float(h), nearClip));
else
rttCamera->setProjectionMatrixAsPerspective(90.0, w/float(h), nearClip, viewDistance);
diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp
index eb7c4a3882..8e4353f2ef 100644
--- a/apps/openmw/mwrender/sky.cpp
+++ b/apps/openmw/mwrender/sky.cpp
@@ -14,7 +14,7 @@
#include <components/sceneutil/controller.hpp>
#include <components/sceneutil/shadow.hpp>
#include <components/sceneutil/visitor.hpp>
-#include <components/sceneutil/util.hpp>
+#include <components/sceneutil/depth.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/resource/imagemanager.hpp>
@@ -248,6 +248,7 @@ namespace MWRender
, mEnabled(true)
, mSunEnabled(true)
, mPrecipitationAlpha(0.f)
+ , mDirtyParticlesEffect(false)
{
osg::ref_ptr<CameraRelativeTransform> skyroot = new CameraRelativeTransform;
skyroot->setName("Sky Root");
@@ -300,7 +301,7 @@ namespace MWRender
mAtmosphereNightUpdater = new AtmosphereNightUpdater(mSceneManager->getImageManager(), forceShaders);
atmosphereNight->addUpdateCallback(mAtmosphereNightUpdater);
- mSun.reset(new Sun(mEarlyRenderBinRoot, *mSceneManager->getImageManager()));
+ mSun.reset(new Sun(mEarlyRenderBinRoot, *mSceneManager));
mMasser.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager, Fallback::Map::getFloat("Moons_Masser_Size")/125, Moon::Type_Masser));
mSecunda.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager, Fallback::Map::getFloat("Moons_Secunda_Size")/125, Moon::Type_Secunda));
@@ -338,7 +339,7 @@ namespace MWRender
mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(program, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
}
- auto depth = SceneUtil::createDepth();
+ osg::ref_ptr<osg::Depth> depth = new SceneUtil::AutoDepth;
depth->setWriteMask(false);
mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth);
mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON);
@@ -535,7 +536,10 @@ namespace MWRender
mRootNode->setNodeMask(enabled ? Mask_Sky : 0u);
if (!enabled && mParticleNode && mParticleEffect)
- mCurrentParticleEffect = {};
+ {
+ mCurrentParticleEffect.clear();
+ mDirtyParticlesEffect = true;
+ }
mEnabled = enabled;
}
@@ -606,8 +610,9 @@ namespace MWRender
if (mIsStorm)
mStormDirection = weather.mStormDirection;
- if (mCurrentParticleEffect != weather.mParticleEffect)
+ if (mDirtyParticlesEffect || (mCurrentParticleEffect != weather.mParticleEffect))
{
+ mDirtyParticlesEffect = false;
mCurrentParticleEffect = weather.mParticleEffect;
// cleanup old particles
@@ -632,7 +637,6 @@ namespace MWRender
mParticleNode = new osg::PositionAttitudeTransform;
mParticleNode->addCullCallback(mUnderwaterSwitch);
mParticleNode->setNodeMask(Mask_WeatherParticles);
- mParticleNode->getOrCreateStateSet();
mRootNode->addChild(mParticleNode);
}
@@ -664,7 +668,6 @@ namespace MWRender
ps->getParticle(particleIndex)->update(0, true);
}
- ps->getOrCreateStateSet();
ps->setUserValue("simpleLighting", true);
}
diff --git a/apps/openmw/mwrender/sky.hpp b/apps/openmw/mwrender/sky.hpp
index 1a30633886..e2ceae45f4 100644
--- a/apps/openmw/mwrender/sky.hpp
+++ b/apps/openmw/mwrender/sky.hpp
@@ -186,6 +186,7 @@ namespace MWRender
bool mSunEnabled;
float mPrecipitationAlpha;
+ bool mDirtyParticlesEffect;
osg::Vec4f mMoonScriptColor;
};
diff --git a/apps/openmw/mwrender/skyutil.cpp b/apps/openmw/mwrender/skyutil.cpp
index 0e2955333f..ead9d9af70 100644
--- a/apps/openmw/mwrender/skyutil.cpp
+++ b/apps/openmw/mwrender/skyutil.cpp
@@ -25,7 +25,7 @@
#include <components/resource/scenemanager.hpp>
#include <components/resource/imagemanager.hpp>
-#include <components/sceneutil/util.hpp>
+#include <components/sceneutil/depth.hpp>
#include <components/fallback/fallback.hpp>
@@ -694,12 +694,14 @@ namespace MWRender
mTransform->setNodeMask(visible ? mVisibleMask : 0);
}
- Sun::Sun(osg::Group* parentNode, Resource::ImageManager& imageManager)
+ Sun::Sun(osg::Group* parentNode, Resource::SceneManager& sceneManager)
: CelestialBody(parentNode, 1.0f, 1, Mask_Sun)
, mUpdater(new SunUpdater)
{
mTransform->addUpdateCallback(mUpdater);
+ Resource::ImageManager& imageManager = *sceneManager.getImageManager();
+
osg::ref_ptr<osg::Texture2D> sunTex = new osg::Texture2D(imageManager.getImage("textures/tx_sun_05.dds"));
sunTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE);
sunTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE);
@@ -714,9 +716,12 @@ namespace MWRender
stateset->setRenderBinDetails(RenderBin_OcclusionQuery, "RenderBin");
stateset->setNestRenderBins(false);
// Set up alpha testing on the occlusion testing subgraph, that way we can get the occlusion tested fragments to match the circular shape of the sun
- osg::ref_ptr<osg::AlphaFunc> alphaFunc = new osg::AlphaFunc;
- alphaFunc->setFunction(osg::AlphaFunc::GREATER, 0.8);
- stateset->setAttributeAndModes(alphaFunc);
+ if (!sceneManager.getForceShaders())
+ {
+ osg::ref_ptr<osg::AlphaFunc> alphaFunc = new osg::AlphaFunc;
+ alphaFunc->setFunction(osg::AlphaFunc::GREATER, 0.8);
+ stateset->setAttributeAndModes(alphaFunc);
+ }
stateset->setTextureAttributeAndModes(0, sunTex);
stateset->setAttributeAndModes(createUnlitMaterial());
stateset->addUniform(new osg::Uniform("pass", static_cast<int>(Pass::Sunflash_Query)));
@@ -811,11 +816,12 @@ namespace MWRender
osg::StateSet* queryStateSet = new osg::StateSet;
if (queryVisible)
{
- auto depth = SceneUtil::createDepth();
+ osg::ref_ptr<osg::Depth> depth = new SceneUtil::AutoDepth(osg::Depth::LEQUAL);
// This is a trick to make fragments written by the query always use the maximum depth value,
// without having to retrieve the current far clipping distance.
// We want the sun glare to be "infinitely" far away.
- double far = SceneUtil::getReverseZ() ? 0.0 : 1.0;
+ double far = SceneUtil::AutoDepth::isReversed() ? 0.0 : 1.0;
+ depth->setFunction(osg::Depth::LEQUAL);
depth->setZNear(far);
depth->setZFar(far);
depth->setWriteMask(false);
@@ -879,6 +885,8 @@ namespace MWRender
camera->setClearMask(0);
camera->setRenderOrder(osg::Camera::NESTED_RENDER);
camera->setAllowEventFocus(false);
+ camera->getOrCreateStateSet()->addUniform(new osg::Uniform("projectionMatrix", static_cast<osg::Matrixf>(camera->getProjectionMatrix())));
+ SceneUtil::setCameraClearDepth(camera);
osg::ref_ptr<osg::Geometry> geom = osg::createTexturedQuadGeometry(osg::Vec3f(-1,-1,0), osg::Vec3f(2,0,0), osg::Vec3f(0,2,0));
camera->addChild(geom);
diff --git a/apps/openmw/mwrender/skyutil.hpp b/apps/openmw/mwrender/skyutil.hpp
index c2272143a0..a0d9ed72b4 100644
--- a/apps/openmw/mwrender/skyutil.hpp
+++ b/apps/openmw/mwrender/skyutil.hpp
@@ -239,7 +239,7 @@ namespace MWRender
class Sun : public CelestialBody
{
public:
- Sun(osg::Group* parentNode, Resource::ImageManager& imageManager);
+ Sun(osg::Group* parentNode, Resource::SceneManager& sceneManager);
~Sun();
diff --git a/apps/openmw/mwrender/viewovershoulder.cpp b/apps/openmw/mwrender/viewovershoulder.cpp
deleted file mode 100644
index 799e34c992..0000000000
--- a/apps/openmw/mwrender/viewovershoulder.cpp
+++ /dev/null
@@ -1,110 +0,0 @@
-#include "viewovershoulder.hpp"
-
-#include <osg/Quat>
-
-#include <components/settings/settings.hpp>
-
-#include "../mwbase/environment.hpp"
-#include "../mwbase/world.hpp"
-
-#include "../mwworld/class.hpp"
-#include "../mwworld/ptr.hpp"
-#include "../mwworld/refdata.hpp"
-
-#include "../mwmechanics/drawstate.hpp"
-
-namespace MWRender
-{
-
- ViewOverShoulderController::ViewOverShoulderController(Camera* camera) :
- mCamera(camera), mMode(Mode::RightShoulder),
- mAutoSwitchShoulder(Settings::Manager::getBool("auto switch shoulder", "Camera")),
- mOverShoulderHorizontalOffset(30.f), mOverShoulderVerticalOffset(-10.f)
- {
- osg::Vec2f offset = Settings::Manager::getVector2("view over shoulder offset", "Camera");
- mOverShoulderHorizontalOffset = std::abs(offset.x());
- mOverShoulderVerticalOffset = offset.y();
- mDefaultShoulderIsRight = offset.x() >= 0;
-
- mCamera->enableDynamicCameraDistance(true);
- mCamera->enableCrosshairInThirdPersonMode(true);
- mCamera->setFocalPointTargetOffset(offset);
- }
-
- void ViewOverShoulderController::update()
- {
- if (mCamera->isFirstPerson())
- return;
-
- Mode oldMode = mMode;
- auto ptr = mCamera->getTrackingPtr();
- bool combat = ptr.getClass().isActor() && ptr.getClass().getCreatureStats(ptr).getDrawState() != MWMechanics::DrawState_Nothing;
- if (combat && !mCamera->isVanityOrPreviewModeEnabled())
- mMode = Mode::Combat;
- else if (MWBase::Environment::get().getWorld()->isSwimming(ptr))
- mMode = Mode::Swimming;
- else if (oldMode == Mode::Combat || oldMode == Mode::Swimming)
- mMode = mDefaultShoulderIsRight ? Mode::RightShoulder : Mode::LeftShoulder;
- if (mAutoSwitchShoulder && (mMode == Mode::LeftShoulder || mMode == Mode::RightShoulder))
- trySwitchShoulder();
-
- if (oldMode == mMode)
- return;
-
- if (mCamera->getMode() == Camera::Mode::Vanity)
- // Player doesn't touch controls for a long time. Transition should be very slow.
- mCamera->setFocalPointTransitionSpeed(0.2f);
- else if ((oldMode == Mode::Combat || mMode == Mode::Combat) && mCamera->getMode() == Camera::Mode::Normal)
- // Transition to/from combat mode and we are not it preview mode. Should be fast.
- mCamera->setFocalPointTransitionSpeed(5.f);
- else
- mCamera->setFocalPointTransitionSpeed(1.f); // Default transition speed.
-
- switch (mMode)
- {
- case Mode::RightShoulder:
- mCamera->setFocalPointTargetOffset({mOverShoulderHorizontalOffset, mOverShoulderVerticalOffset});
- break;
- case Mode::LeftShoulder:
- mCamera->setFocalPointTargetOffset({-mOverShoulderHorizontalOffset, mOverShoulderVerticalOffset});
- break;
- case Mode::Combat:
- case Mode::Swimming:
- default:
- mCamera->setFocalPointTargetOffset({0, 15});
- }
- }
-
- void ViewOverShoulderController::trySwitchShoulder()
- {
- if (mCamera->getMode() != Camera::Mode::Normal)
- return;
-
- const float limitToSwitch = 120; // switch to other shoulder if wall is closer than this limit
- const float limitToSwitchBack = 300; // switch back to default shoulder if there is no walls at this distance
-
- auto orient = osg::Quat(mCamera->getYaw(), osg::Vec3d(0,0,1));
- osg::Vec3d playerPos = mCamera->getFocalPoint() - mCamera->getFocalPointOffset();
-
- MWBase::World* world = MWBase::Environment::get().getWorld();
- osg::Vec3d sideOffset = orient * osg::Vec3d(world->getHalfExtents(mCamera->getTrackingPtr()).x() - 1, 0, 0);
- float rayRight = world->getDistToNearestRayHit(
- playerPos + sideOffset, orient * osg::Vec3d(1, 0, 0), limitToSwitchBack + 1);
- float rayLeft = world->getDistToNearestRayHit(
- playerPos - sideOffset, orient * osg::Vec3d(-1, 0, 0), limitToSwitchBack + 1);
- float rayRightForward = world->getDistToNearestRayHit(
- playerPos + sideOffset, orient * osg::Vec3d(1, 3, 0), limitToSwitchBack + 1);
- float rayLeftForward = world->getDistToNearestRayHit(
- playerPos - sideOffset, orient * osg::Vec3d(-1, 3, 0), limitToSwitchBack + 1);
- float distRight = std::min(rayRight, rayRightForward);
- float distLeft = std::min(rayLeft, rayLeftForward);
-
- if (distLeft < limitToSwitch && distRight > limitToSwitchBack)
- mMode = Mode::RightShoulder;
- else if (distRight < limitToSwitch && distLeft > limitToSwitchBack)
- mMode = Mode::LeftShoulder;
- else if (distRight > limitToSwitchBack && distLeft > limitToSwitchBack)
- mMode = mDefaultShoulderIsRight ? Mode::RightShoulder : Mode::LeftShoulder;
- }
-
-}
diff --git a/apps/openmw/mwrender/viewovershoulder.hpp b/apps/openmw/mwrender/viewovershoulder.hpp
deleted file mode 100644
index 80ac308656..0000000000
--- a/apps/openmw/mwrender/viewovershoulder.hpp
+++ /dev/null
@@ -1,30 +0,0 @@
-#ifndef VIEWOVERSHOULDER_H
-#define VIEWOVERSHOULDER_H
-
-#include "camera.hpp"
-
-namespace MWRender
-{
-
- class ViewOverShoulderController
- {
- public:
- ViewOverShoulderController(Camera* camera);
-
- void update();
-
- private:
- void trySwitchShoulder();
- enum class Mode { RightShoulder, LeftShoulder, Combat, Swimming };
-
- Camera* mCamera;
- Mode mMode;
- bool mAutoSwitchShoulder;
- float mOverShoulderHorizontalOffset;
- float mOverShoulderVerticalOffset;
- bool mDefaultShoulderIsRight;
- };
-
-}
-
-#endif // VIEWOVERSHOULDER_H
diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp
index d8f92d1d1f..fd5cbe0f79 100644
--- a/apps/openmw/mwrender/water.cpp
+++ b/apps/openmw/mwrender/water.cpp
@@ -27,11 +27,12 @@
#include <components/sceneutil/rtt.hpp>
#include <components/sceneutil/shadow.hpp>
-#include <components/sceneutil/util.hpp>
+#include <components/sceneutil/depth.hpp>
#include <components/sceneutil/waterutil.hpp>
#include <components/sceneutil/lightmanager.hpp>
#include <components/misc/constants.hpp>
+#include <components/misc/stringops.hpp>
#include <components/nifosg/controller.hpp>
@@ -267,7 +268,6 @@ public:
void setDefaults(osg::Camera* camera) override
{
- SceneUtil::setCameraClearDepth(camera);
camera->setReferenceFrame(osg::Camera::RELATIVE_RF);
camera->setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water"));
camera->setName("RefractionCamera");
@@ -305,8 +305,7 @@ public:
void setWaterLevel(float waterLevel)
{
- const float refractionScale = std::min(1.0f, std::max(0.0f,
- Settings::Manager::getFloat("refraction scale", "Water")));
+ const float refractionScale = std::clamp(Settings::Manager::getFloat("refraction scale", "Water"), 0.f, 1.f);
mViewMatrix = osg::Matrix::scale(1, 1, refractionScale) *
osg::Matrix::translate(0, 0, (1.0 - refractionScale) * waterLevel);
@@ -344,7 +343,6 @@ public:
void setDefaults(osg::Camera* camera) override
{
- SceneUtil::setCameraClearDepth(camera);
camera->setReferenceFrame(osg::Camera::RELATIVE_RF);
camera->setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water"));
camera->setName("ReflectionCamera");
@@ -400,7 +398,7 @@ private:
unsigned int calcNodeMask()
{
int reflectionDetail = Settings::Manager::getInt("reflection detail", "Water");
- reflectionDetail = std::min(5, std::max(mInterior ? 2 : 0, reflectionDetail));
+ reflectionDetail = std::clamp(reflectionDetail, mInterior ? 2 : 0, 5);
unsigned int extraMask = 0;
if(reflectionDetail >= 1) extraMask |= Mask_Terrain;
if(reflectionDetail >= 2) extraMask |= Mask_Static;
@@ -583,7 +581,7 @@ void Water::createSimpleWaterStateSet(osg::Node* node, float alpha)
// Add animated textures
std::vector<osg::ref_ptr<osg::Texture2D> > textures;
- int frameCount = std::max(0, std::min(Fallback::Map::getInt("Water_SurfaceFrameCount"), 320));
+ const int frameCount = std::clamp(Fallback::Map::getInt("Water_SurfaceFrameCount"), 0, 320);
const std::string& texture = Fallback::Map::getString("Water_SurfaceTexture");
for (int i=0; i<frameCount; ++i)
{
@@ -645,7 +643,7 @@ public:
{
stateset->setMode(GL_BLEND, osg::StateAttribute::ON);
stateset->setRenderBinDetails(MWRender::RenderBin_Water, "RenderBin");
- osg::ref_ptr<osg::Depth> depth = SceneUtil::createDepth();
+ osg::ref_ptr<osg::Depth> depth = new SceneUtil::AutoDepth;
depth->setWriteMask(false);
stateset->setAttributeAndModes(depth, osg::StateAttribute::ON);
}
@@ -677,7 +675,10 @@ void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, R
{
// use a define map to conditionally compile the shader
std::map<std::string, std::string> defineMap;
- defineMap.insert(std::make_pair(std::string("refraction_enabled"), std::string(mRefraction ? "1" : "0")));
+ defineMap["refraction_enabled"] = std::string(mRefraction ? "1" : "0");
+ const auto rippleDetail = std::clamp(Settings::Manager::getInt("rain ripple detail", "Water"), 0, 2);
+ defineMap["rain_ripple_detail"] = std::to_string(rippleDetail);
+
Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager();
osg::ref_ptr<osg::Shader> vertexShader(shaderMgr.getShader("water_vertex.glsl", defineMap, osg::Shader::VERTEX));
@@ -724,7 +725,7 @@ Water::~Water()
void Water::listAssetsToPreload(std::vector<std::string> &textures)
{
- int frameCount = std::max(0, std::min(Fallback::Map::getInt("Water_SurfaceFrameCount"), 320));
+ const int frameCount = std::clamp(Fallback::Map::getInt("Water_SurfaceFrameCount"), 0, 320);
const std::string& texture = Fallback::Map::getString("Water_SurfaceTexture");
for (int i=0; i<frameCount; ++i)
{
diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp
index c5a4bb6dfc..b7b6de9463 100644
--- a/apps/openmw/mwscript/aiextensions.cpp
+++ b/apps/openmw/mwscript/aiextensions.cpp
@@ -14,6 +14,7 @@
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
+#include "../mwmechanics/actorutil.hpp"
#include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/aiactivate.hpp"
#include "../mwmechanics/aiescort.hpp"
@@ -47,10 +48,14 @@ namespace MWScript
std::string objectID = runtime.getStringLiteral (runtime[0].mInteger);
runtime.pop();
- // discard additional arguments (reset), because we have no idea what they mean.
+ // The value of the reset argument doesn't actually matter
+ bool repeat = arg0;
for (unsigned int i=0; i<arg0; ++i) runtime.pop();
- MWMechanics::AiActivate activatePackage(objectID);
+ if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer())
+ return;
+
+ MWMechanics::AiActivate activatePackage(objectID, repeat);
ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(activatePackage, ptr);
Log(Debug::Info) << "AiActivate";
}
@@ -74,10 +79,14 @@ namespace MWScript
Interpreter::Type_Float z = runtime[0].mFloat;
runtime.pop();
- // discard additional arguments (reset), because we have no idea what they mean.
+ // The value of the reset argument doesn't actually matter
+ bool repeat = arg0;
for (unsigned int i=0; i<arg0; ++i) runtime.pop();
- MWMechanics::AiTravel travelPackage(x, y, z);
+ if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer())
+ return;
+
+ MWMechanics::AiTravel travelPackage(x, y, z, repeat);
ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(travelPackage, ptr);
Log(Debug::Info) << "AiTravel: " << x << ", " << y << ", " << z;
@@ -108,10 +117,14 @@ namespace MWScript
Interpreter::Type_Float z = runtime[0].mFloat;
runtime.pop();
- // discard additional arguments (reset), because we have no idea what they mean.
+ // The value of the reset argument doesn't actually matter
+ bool repeat = arg0;
for (unsigned int i=0; i<arg0; ++i) runtime.pop();
- MWMechanics::AiEscort escortPackage(actorID, static_cast<int>(duration), x, y, z);
+ if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer())
+ return;
+
+ MWMechanics::AiEscort escortPackage(actorID, static_cast<int>(duration), x, y, z, repeat);
ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(escortPackage, ptr);
Log(Debug::Info) << "AiEscort: " << x << ", " << y << ", " << z << ", " << duration;
@@ -145,15 +158,20 @@ namespace MWScript
Interpreter::Type_Float z = runtime[0].mFloat;
runtime.pop();
- // discard additional arguments (reset), because we have no idea what they mean.
+ // The value of the reset argument doesn't actually matter
+ bool repeat = arg0;
for (unsigned int i=0; i<arg0; ++i) runtime.pop();
+ if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer())
+ return;
+
if (cellID.empty())
- throw std::runtime_error("AiEscortCell: no cell ID given");
+ return;
- MWBase::Environment::get().getWorld()->getStore().get<ESM::Cell>().find(cellID);
+ if (!MWBase::Environment::get().getWorld()->getStore().get<ESM::Cell>().search(cellID))
+ return;
- MWMechanics::AiEscort escortPackage(actorID, cellID, static_cast<int>(duration), x, y, z);
+ MWMechanics::AiEscort escortPackage(actorID, cellID, static_cast<int>(duration), x, y, z, repeat);
ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(escortPackage, ptr);
Log(Debug::Info) << "AiEscort: " << x << ", " << y << ", " << z << ", " << duration;
@@ -169,9 +187,11 @@ namespace MWScript
{
MWWorld::Ptr ptr = R()(runtime);
- Interpreter::Type_Integer value = ptr.getClass().getCreatureStats (ptr).getAiSequence().isPackageDone();
+ bool done = false;
+ if (ptr.getClass().isActor())
+ done = ptr.getClass().getCreatureStats(ptr).getAiSequence().isPackageDone();
- runtime.push (value);
+ runtime.push(done);
}
};
@@ -208,8 +228,7 @@ namespace MWScript
{
if(!repeat)
repeat = true;
- Interpreter::Type_Integer idleValue = runtime[0].mInteger;
- idleValue = std::min(255, std::max(0, idleValue));
+ Interpreter::Type_Integer idleValue = std::clamp(runtime[0].mInteger, 0, 255);
idleList.push_back(idleValue);
runtime.pop();
--arg0;
@@ -222,9 +241,12 @@ namespace MWScript
--arg0;
}
- // discard additional arguments (reset), because we have no idea what they mean.
+ // discard additional arguments, because we have no idea what they mean.
for (unsigned int i=0; i<arg0; ++i) runtime.pop();
+ if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer())
+ return;
+
MWMechanics::AiWander wanderPackage(range, duration, time, idleList, repeat);
ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(wanderPackage, ptr);
}
@@ -241,7 +263,10 @@ namespace MWScript
{
MWWorld::Ptr ptr = R()(runtime);
- runtime.push(ptr.getClass().getCreatureStats (ptr).getAiSetting (mIndex).getModified(false));
+ Interpreter::Type_Integer value = 0;
+ if (ptr.getClass().isActor())
+ value = ptr.getClass().getCreatureStats (ptr).getAiSetting(mIndex).getModified(false);
+ runtime.push(value);
}
};
template<class R>
@@ -257,6 +282,9 @@ namespace MWScript
Interpreter::Type_Integer value = runtime[0].mInteger;
runtime.pop();
+ if (!ptr.getClass().isActor())
+ return;
+
int modified = ptr.getClass().getCreatureStats (ptr).getAiSetting (mIndex).getBase() + value;
ptr.getClass().getCreatureStats (ptr).setAiSetting (mIndex, modified);
@@ -307,10 +335,14 @@ namespace MWScript
Interpreter::Type_Float z = runtime[0].mFloat;
runtime.pop();
- // discard additional arguments (reset), because we have no idea what they mean.
+ // The value of the reset argument doesn't actually matter
+ bool repeat = arg0;
for (unsigned int i=0; i<arg0; ++i) runtime.pop();
- MWMechanics::AiFollow followPackage(actorID, duration, x, y ,z);
+ if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer())
+ return;
+
+ MWMechanics::AiFollow followPackage(actorID, duration, x, y, z, repeat);
ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(followPackage, ptr);
Log(Debug::Info) << "AiFollow: " << actorID << ", " << x << ", " << y << ", " << z << ", " << duration;
@@ -344,10 +376,14 @@ namespace MWScript
Interpreter::Type_Float z = runtime[0].mFloat;
runtime.pop();
- // discard additional arguments (reset), because we have no idea what they mean.
+ // The value of the reset argument doesn't actually matter
+ bool repeat = arg0;
for (unsigned int i=0; i<arg0; ++i) runtime.pop();
- MWMechanics::AiFollow followPackage(actorID, cellID, duration, x, y ,z);
+ if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer())
+ return;
+
+ MWMechanics::AiFollow followPackage(actorID, cellID, duration, x, y, z, repeat);
ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(followPackage, ptr);
Log(Debug::Info) << "AiFollow: " << actorID << ", " << x << ", " << y << ", " << z << ", " << duration;
}
@@ -432,23 +468,25 @@ namespace MWScript
std::string testedTargetId = runtime.getStringLiteral (runtime[0].mInteger);
runtime.pop();
- const MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor);
-
bool targetsAreEqual = false;
- MWWorld::Ptr targetPtr;
- if (creatureStats.getAiSequence().getCombatTarget (targetPtr))
+ if (actor.getClass().isActor())
{
- if (!targetPtr.isEmpty() && targetPtr.getCellRef().getRefId() == testedTargetId)
- targetsAreEqual = true;
- }
- else if (testedTargetId == "player") // Currently the player ID is hardcoded
- {
- MWBase::MechanicsManager* mechMgr = MWBase::Environment::get().getMechanicsManager();
- bool greeting = mechMgr->getGreetingState(actor) == MWMechanics::Greet_InProgress;
- bool sayActive = MWBase::Environment::get().getSoundManager()->sayActive(actor);
- targetsAreEqual = (greeting && sayActive) || mechMgr->isTurningToPlayer(actor);
+ const MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor);
+ MWWorld::Ptr targetPtr;
+ if (creatureStats.getAiSequence().getCombatTarget(targetPtr))
+ {
+ if (!targetPtr.isEmpty() && targetPtr.getCellRef().getRefId() == testedTargetId)
+ targetsAreEqual = true;
+ }
+ else if (testedTargetId == "player") // Currently the player ID is hardcoded
+ {
+ MWBase::MechanicsManager* mechMgr = MWBase::Environment::get().getMechanicsManager();
+ bool greeting = mechMgr->getGreetingState(actor) == MWMechanics::Greet_InProgress;
+ bool sayActive = MWBase::Environment::get().getSoundManager()->sayActive(actor);
+ targetsAreEqual = (greeting && sayActive) || mechMgr->isTurningToPlayer(actor);
+ }
}
- runtime.push(int(targetsAreEqual));
+ runtime.push(targetsAreEqual);
}
};
@@ -475,8 +513,9 @@ namespace MWScript
void execute (Interpreter::Runtime& runtime) override
{
MWWorld::Ptr actor = R()(runtime);
- MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor);
- creatureStats.getAiSequence().stopCombat();
+ if (!actor.getClass().isActor())
+ return;
+ MWBase::Environment::get().getMechanicsManager()->stopCombat(actor);
}
};
@@ -506,6 +545,9 @@ namespace MWScript
Interpreter::Type_Float y = runtime[0].mFloat;
runtime.pop();
+ if (!actor.getClass().isActor() || actor == MWMechanics::getPlayer())
+ return;
+
MWMechanics::AiFace facePackage(x, y);
actor.getClass().getCreatureStats(actor).getAiSequence().stack(facePackage, actor);
}
diff --git a/apps/openmw/mwscript/skyextensions.cpp b/apps/openmw/mwscript/skyextensions.cpp
index 2b6bf826f9..81984ad7b7 100644
--- a/apps/openmw/mwscript/skyextensions.cpp
+++ b/apps/openmw/mwscript/skyextensions.cpp
@@ -11,6 +11,8 @@
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
+#include "../mwworld/esmstore.hpp"
+
#include "interpretercontext.hpp"
namespace MWScript
@@ -91,7 +93,11 @@ namespace MWScript
Interpreter::Type_Integer id = runtime[0].mInteger;
runtime.pop();
- MWBase::Environment::get().getWorld()->changeWeather(region, id);
+ const ESM::Region* reg = MWBase::Environment::get().getWorld()->getStore().get<ESM::Region>().search(region);
+ if (reg)
+ MWBase::Environment::get().getWorld()->changeWeather(region, id);
+ else
+ runtime.getContext().report("Warning: Region \"" + region + "\" was not found");
}
};
@@ -108,7 +114,7 @@ namespace MWScript
chances.reserve(10);
while(arg0 > 0)
{
- chances.push_back(std::max(0, std::min(127, runtime[0].mInteger)));
+ chances.push_back(std::clamp(runtime[0].mInteger, 0, 127));
runtime.pop();
arg0--;
}
diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp
index ccad186a0d..186d1edf26 100644
--- a/apps/openmw/mwscript/statsextensions.cpp
+++ b/apps/openmw/mwscript/statsextensions.cpp
@@ -40,6 +40,22 @@ namespace
return factionId;
}
+
+ void modStat(MWMechanics::AttributeValue& stat, float amount)
+ {
+ float base = stat.getBase();
+ float modifier = stat.getModifier() - stat.getDamage();
+ float modified = base + modifier;
+ if(modified <= 0.f && amount < 0.f)
+ amount = 0.f;
+ else if(amount < 0.f && modified + amount < 0.f)
+ amount = -modified;
+ else if((modifier <= 0.f || base >= 100.f) && amount > 0.f)
+ amount = std::clamp(100.f - modified, 0.f, amount);
+ stat.setBase(std::min(base + amount, 100.f), true);
+ modifier += base - stat.getBase() + amount;
+ stat.setModifier(modifier);
+ }
}
namespace MWScript
@@ -122,7 +138,7 @@ namespace MWScript
runtime.pop();
MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex);
- attribute.setBase (value);
+ attribute.setBase(value, true);
ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute);
}
};
@@ -146,19 +162,7 @@ namespace MWScript
MWMechanics::AttributeValue attribute = ptr.getClass()
.getCreatureStats(ptr)
.getAttribute(mIndex);
-
- if (value == 0)
- return;
-
- if (((attribute.getBase() <= 0) && (value < 0))
- || ((attribute.getBase() >= 100) && (value > 0)))
- return;
-
- if (value < 0)
- attribute.setBase(std::max(0.f, attribute.getBase() + value));
- else
- attribute.setBase(std::min(100.f, attribute.getBase() + value));
-
+ modStat(attribute, value);
ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute);
}
};
@@ -372,7 +376,7 @@ namespace MWScript
MWMechanics::NpcStats& stats = ptr.getClass().getNpcStats (ptr);
- stats.getSkill (mIndex).setBase (value);
+ stats.getSkill(mIndex).setBase(value, true);
}
};
@@ -395,18 +399,7 @@ namespace MWScript
MWMechanics::SkillValue &skill = ptr.getClass()
.getNpcStats(ptr)
.getSkill(mIndex);
-
- if (value == 0)
- return;
-
- if (((skill.getBase() <= 0.f) && (value < 0.f))
- || ((skill.getBase() >= 100.f) && (value > 0.f)))
- return;
-
- if (value < 0)
- skill.setBase(std::max(0.f, skill.getBase() + value));
- else
- skill.setBase(std::min(100.f, skill.getBase() + value));
+ modStat(skill, value);
}
};
@@ -1194,13 +1187,23 @@ namespace MWScript
{
bool wasEnabled = ptr.getRefData().isEnabled();
MWBase::Environment::get().getWorld()->undeleteObject(ptr);
- MWBase::Environment::get().getWorld()->removeContainerScripts(ptr);
- MWBase::Environment::get().getWindowManager()->onDeleteCustomData(ptr);
-
+ auto windowManager = MWBase::Environment::get().getWindowManager();
+ bool wasOpen = windowManager->containsMode(MWGui::GM_Container);
+ windowManager->onDeleteCustomData(ptr);
// HACK: disable/enable object to re-add it to the scene properly (need a new Animation).
MWBase::Environment::get().getWorld()->disable(ptr);
- // resets runtime state such as inventory, stats and AI. does not reset position in the world
- ptr.getRefData().setCustomData(nullptr);
+ if (wasOpen && !windowManager->containsMode(MWGui::GM_Container))
+ {
+ // Reopen the loot GUI if it was closed because we resurrected the actor we were looting
+ MWBase::Environment::get().getMechanicsManager()->resurrect(ptr);
+ windowManager->forceLootMode(ptr);
+ }
+ else
+ {
+ MWBase::Environment::get().getWorld()->removeContainerScripts(ptr);
+ // resets runtime state such as inventory, stats and AI. does not reset position in the world
+ ptr.getRefData().setCustomData(nullptr);
+ }
if (wasEnabled)
MWBase::Environment::get().getWorld()->enable(ptr);
}
diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp
index 5cfb2b2989..8a159a5685 100644
--- a/apps/openmw/mwscript/transformationextensions.cpp
+++ b/apps/openmw/mwscript/transformationextensions.cpp
@@ -355,7 +355,8 @@ namespace MWScript
if (ptr.getContainerStore())
return;
- if (ptr == MWMechanics::getPlayer())
+ bool isPlayer = ptr == MWMechanics::getPlayer();
+ if (isPlayer)
{
MWBase::Environment::get().getWorld()->getPlayer().setTeleported(true);
}
@@ -378,17 +379,21 @@ namespace MWScript
}
catch(std::exception&)
{
- // cell not found, move to exterior instead (vanilla PositionCell compatibility)
+ // cell not found, move to exterior instead if moving the player (vanilla PositionCell compatibility)
const ESM::Cell* cell = MWBase::Environment::get().getWorld()->getExterior(cellID);
- int cx,cy;
- MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy);
- store = MWBase::Environment::get().getWorld()->getExterior(cx,cy);
if(!cell)
{
- std::string error = "Warning: PositionCell: unknown interior cell (" + cellID + "), moving to exterior instead";
+ std::string error = "Warning: PositionCell: unknown interior cell (" + cellID + ")";
+ if(isPlayer)
+ error += ", moving to exterior instead";
runtime.getContext().report (error);
Log(Debug::Warning) << error;
+ if(!isPlayer)
+ return;
}
+ int cx,cy;
+ MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy);
+ store = MWBase::Environment::get().getWorld()->getExterior(cx,cy);
}
if(store)
{
@@ -400,7 +405,7 @@ namespace MWScript
// Note that you must specify ZRot in minutes (1 degree = 60 minutes; north = 0, east = 5400, south = 10800, west = 16200)
// except for when you position the player, then degrees must be used.
// See "Morrowind Scripting for Dummies (9th Edition)" pages 50 and 54 for reference.
- if(ptr != MWMechanics::getPlayer())
+ if(!isPlayer)
zRot = zRot/60.0f;
rot.z() = osg::DegreesToRadians(zRot);
MWBase::Environment::get().getWorld()->rotateObject(ptr,rot);
@@ -622,12 +627,13 @@ namespace MWScript
runtime.pop();
auto rot = ptr.getRefData().getPosition().asRotationVec3();
- if (axis == "x")
+ // Regardless of the axis argument, the player may only be rotated on Z
+ if (axis == "z" || MWMechanics::getPlayer() == ptr)
+ rot.z() += rotation;
+ else if (axis == "x")
rot.x() += rotation;
else if (axis == "y")
rot.y() += rotation;
- else if (axis == "z")
- rot.z() += rotation;
MWBase::Environment::get().getWorld()->rotateObject(ptr,rot);
}
};
diff --git a/apps/openmw/mwsound/loudness.cpp b/apps/openmw/mwsound/loudness.cpp
index ae31d60949..a36615ee4a 100644
--- a/apps/openmw/mwsound/loudness.cpp
+++ b/apps/openmw/mwsound/loudness.cpp
@@ -40,7 +40,7 @@ void Sound_Loudness::analyzeLoudness(const std::vector< char >& data)
else if (mSampleType == SampleType_Float32)
{
value = *reinterpret_cast<const float*>(&mQueue[sample*advance]);
- value = std::max(-1.f, std::min(1.f, value)); // Float samples *should* be scaled to [-1,1] already.
+ value = std::clamp(value, -1.f, 1.f); // Float samples *should* be scaled to [-1,1] already.
}
sum += value*value;
@@ -64,8 +64,7 @@ float Sound_Loudness::getLoudnessAtTime(float sec) const
if(mSamplesPerSec <= 0.0f || mSamples.empty() || sec < 0.0f)
return 0.0f;
- size_t index = static_cast<size_t>(sec * mSamplesPerSec);
- index = std::max<size_t>(0, std::min(index, mSamples.size()-1));
+ size_t index = std::clamp<size_t>(sec * mSamplesPerSec, 0, mSamples.size() - 1);
return mSamples[index];
}
diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openal_output.cpp
index 67b52309d5..b3cc81b803 100644
--- a/apps/openmw/mwsound/openal_output.cpp
+++ b/apps/openmw/mwsound/openal_output.cpp
@@ -13,6 +13,7 @@
#include <components/debug/debuglog.hpp>
#include <components/misc/constants.hpp>
+#include <components/misc/resourcehelpers.hpp>
#include <components/vfs/manager.hpp>
#include "openal_output.hpp"
@@ -954,17 +955,7 @@ std::pair<Sound_Handle,size_t> OpenAL_Output::loadSound(const std::string &fname
try
{
DecoderPtr decoder = mManager.getDecoder();
- // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav.
- if(decoder->mResourceMgr->exists(fname))
- decoder->open(fname);
- else
- {
- std::string file = fname;
- std::string::size_type pos = file.rfind('.');
- if(pos != std::string::npos)
- file = file.substr(0, pos)+".mp3";
- decoder->open(file);
- }
+ decoder->open(Misc::ResourceHelpers::correctSoundPath(fname, decoder->mResourceMgr));
ChannelConfig chans;
SampleType type;
@@ -1109,13 +1100,8 @@ void OpenAL_Output::initCommon3D(ALuint source, const osg::Vec3f &pos, ALfloat m
alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
}
-void OpenAL_Output::updateCommon(ALuint source, const osg::Vec3f& pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv, bool is3d)
+void OpenAL_Output::updateCommon(ALuint source, const osg::Vec3f& pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv)
{
- if(is3d)
- {
- if((pos - mListenerPos).length2() > maxdist*maxdist)
- gain = 0.0f;
- }
if(useenv && mListenerEnv == Env_Underwater && !mWaterFilter)
{
gain *= 0.9f;
@@ -1243,7 +1229,7 @@ void OpenAL_Output::updateSound(Sound *sound)
ALuint source = GET_PTRID(sound->mHandle);
updateCommon(source, sound->getPosition(), sound->getMaxDistance(), sound->getRealVolume(),
- sound->getPitch(), sound->getUseEnv(), sound->getIs3D());
+ sound->getPitch(), sound->getUseEnv());
getALError();
}
@@ -1369,7 +1355,7 @@ void OpenAL_Output::updateStream(Stream *sound)
ALuint source = stream->mSource;
updateCommon(source, sound->getPosition(), sound->getMaxDistance(), sound->getRealVolume(),
- sound->getPitch(), sound->getUseEnv(), sound->getIs3D());
+ sound->getPitch(), sound->getUseEnv());
getALError();
}
diff --git a/apps/openmw/mwsound/openal_output.hpp b/apps/openmw/mwsound/openal_output.hpp
index 2a19e6768a..47845c0802 100644
--- a/apps/openmw/mwsound/openal_output.hpp
+++ b/apps/openmw/mwsound/openal_output.hpp
@@ -53,7 +53,7 @@ namespace MWSound
void initCommon2D(ALuint source, const osg::Vec3f &pos, ALfloat gain, ALfloat pitch, bool loop, bool useenv);
void initCommon3D(ALuint source, const osg::Vec3f &pos, ALfloat mindist, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool loop, bool useenv);
- void updateCommon(ALuint source, const osg::Vec3f &pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv, bool is3d);
+ void updateCommon(ALuint source, const osg::Vec3f &pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv);
OpenAL_Output& operator=(const OpenAL_Output &rhs);
OpenAL_Output(const OpenAL_Output &rhs);
diff --git a/apps/openmw/mwsound/sound.hpp b/apps/openmw/mwsound/sound.hpp
index 17f052aec0..2a07f05779 100644
--- a/apps/openmw/mwsound/sound.hpp
+++ b/apps/openmw/mwsound/sound.hpp
@@ -11,7 +11,11 @@ namespace MWSound
enum PlayModeEx
{
Play_2D = 0,
+ Play_StopAtFadeEnd = 1 << 28,
+ Play_FadeExponential = 1 << 29,
+ Play_InFade = 1 << 30,
Play_3D = 1 << 31,
+ Play_FadeFlagsMask = (Play_StopAtFadeEnd | Play_FadeExponential),
};
// For testing individual PlayMode flags
@@ -21,13 +25,15 @@ namespace MWSound
struct SoundParams
{
osg::Vec3f mPos;
- float mVolume = 1;
- float mBaseVolume = 1;
- float mPitch = 1;
- float mMinDistance = 1;
- float mMaxDistance = 1000;
+ float mVolume = 1.0f;
+ float mBaseVolume = 1.0f;
+ float mPitch = 1.0f;
+ float mMinDistance = 1.0f;
+ float mMaxDistance = 1000.0f;
int mFlags = 0;
- float mFadeOutTime = 0;
+ float mFadeVolume = 1.0f;
+ float mFadeTarget = 0.0f;
+ float mFadeStep = 0.0f;
};
class SoundBase {
@@ -46,19 +52,97 @@ namespace MWSound
void setPosition(const osg::Vec3f &pos) { mParams.mPos = pos; }
void setVolume(float volume) { mParams.mVolume = volume; }
void setBaseVolume(float volume) { mParams.mBaseVolume = volume; }
- void setFadeout(float duration) { mParams.mFadeOutTime = duration; }
- void updateFade(float duration)
+ void setFadeout(float duration) { setFade(duration, 0.0, Play_StopAtFadeEnd); }
+
+ /// Fade to the given linear gain within the specified amount of time.
+ /// Note that the fade gain is independent of the sound volume.
+ ///
+ /// \param duration specifies the duration of the fade. For *linear*
+ /// fades (default) this will be exactly the time at which the desired
+ /// volume is reached. Let v0 be the initial volume, v1 be the target
+ /// volume, and t0 be the initial time. Then the volume over time is
+ /// given as
+ ///
+ /// v(t) = v0 + (v1 - v0) * (t - t0) / duration if t <= t0 + duration
+ /// v(t) = v1 if t > t0 + duration
+ ///
+ /// For *exponential* fades this determines the time-constant of the
+ /// exponential process describing the fade. In particular, we guarantee
+ /// that we reach v0 + 0.99 * (v1 - v0) within the given duration.
+ ///
+ /// v(t) = v1 + (v0 - v1) * exp(-4.6 * (t0 - t) / duration)
+ ///
+ /// where -4.6 is approximately log(1%) (i.e., -40 dB).
+ ///
+ /// This interpolation mode is meant for environmental sound effects to
+ /// achieve less jarring transitions.
+ ///
+ /// \param targetVolume is the linear gain that should be reached at
+ /// the end of the fade.
+ ///
+ /// \param flags may be a combination of Play_FadeExponential and
+ /// Play_StopAtFadeEnd. If Play_StopAtFadeEnd is set, stops the sound
+ /// once the fade duration has passed or the target volume has been
+ /// reached. If Play_FadeExponential is set, enables the exponential
+ /// fade mode (see above).
+ void setFade(float duration, float targetVolume, int flags = 0) {
+ // Approximation of log(1%) (i.e., -40 dB).
+ constexpr float minus40Decibel = -4.6f;
+
+ // Do nothing if already at the target, unless we need to trigger a stop event
+ if ((mParams.mFadeVolume == targetVolume) && !(flags & Play_StopAtFadeEnd))
+ return;
+
+ mParams.mFadeTarget = targetVolume;
+ mParams.mFlags = (mParams.mFlags & ~Play_FadeFlagsMask) | (flags & Play_FadeFlagsMask) | Play_InFade;
+ if (duration > 0.0f)
+ {
+ if (mParams.mFlags & Play_FadeExponential)
+ mParams.mFadeStep = -minus40Decibel / duration;
+ else
+ mParams.mFadeStep = (mParams.mFadeTarget - mParams.mFadeVolume) / duration;
+ }
+ else
+ {
+ mParams.mFadeVolume = mParams.mFadeTarget;
+ mParams.mFadeStep = 0.0f;
+ }
+ }
+
+ /// Updates the internal fading logic.
+ ///
+ /// \param dt is the time in seconds since the last call to update.
+ ///
+ /// \return true if the sound is still active, false if the sound has
+ /// reached a fading destination that was marked with Play_StopAtFadeEnd.
+ bool updateFade(float dt)
{
- if (mParams.mFadeOutTime > 0.0f)
+ // Mark fade as done at this volume difference (-80dB when fading to zero)
+ constexpr float minVolumeDifference = 1e-4f;
+
+ if (!getInFade())
+ return true;
+
+ // Perform the actual fade operation
+ const float deltaBefore = mParams.mFadeTarget - mParams.mFadeVolume;
+ if (mParams.mFlags & Play_FadeExponential)
+ mParams.mFadeVolume += mParams.mFadeStep * deltaBefore * dt;
+ else
+ mParams.mFadeVolume += mParams.mFadeStep * dt;
+ const float deltaAfter = mParams.mFadeTarget - mParams.mFadeVolume;
+
+ // Abort fade if we overshot or reached the minimum difference
+ if ((std::signbit(deltaBefore) != std::signbit(deltaAfter)) || (std::abs(deltaAfter) < minVolumeDifference))
{
- float soundDuration = std::min(duration, mParams.mFadeOutTime);
- mParams.mVolume *= (mParams.mFadeOutTime - soundDuration) / mParams.mFadeOutTime;
- mParams.mFadeOutTime -= soundDuration;
+ mParams.mFadeVolume = mParams.mFadeTarget;
+ mParams.mFlags &= ~Play_InFade;
}
+
+ return getInFade() || !(mParams.mFlags & Play_StopAtFadeEnd);
}
const osg::Vec3f &getPosition() const { return mParams.mPos; }
- float getRealVolume() const { return mParams.mVolume * mParams.mBaseVolume; }
+ float getRealVolume() const { return mParams.mVolume * mParams.mBaseVolume * mParams.mFadeVolume; }
float getPitch() const { return mParams.mPitch; }
float getMinDistance() const { return mParams.mMinDistance; }
float getMaxDistance() const { return mParams.mMaxDistance; }
@@ -69,6 +153,7 @@ namespace MWSound
bool getIsLooping() const { return mParams.mFlags & MWSound::PlayMode::Loop; }
bool getDistanceCull() const { return mParams.mFlags & MWSound::PlayMode::RemoveAtDistance; }
bool getIs3D() const { return mParams.mFlags & Play_3D; }
+ bool getInFade() const { return mParams.mFlags & Play_InFade; }
void init(const SoundParams& params)
{
diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp
index 96bfc27951..5399b95c97 100644
--- a/apps/openmw/mwsound/soundmanagerimp.cpp
+++ b/apps/openmw/mwsound/soundmanagerimp.cpp
@@ -6,6 +6,7 @@
#include <osg/Matrixf>
+#include <components/misc/resourcehelpers.hpp>
#include <components/misc/rng.hpp>
#include <components/debug/debuglog.hpp>
#include <components/vfs/manager.hpp>
@@ -33,6 +34,8 @@ namespace MWSound
namespace
{
constexpr float sMinUpdateInterval = 1.0f / 30.0f;
+ constexpr float sSfxFadeInDuration = 1.0f;
+ constexpr float sSfxFadeOutDuration = 1.0f;
WaterSoundUpdaterSettings makeWaterSoundUpdaterSettings()
{
@@ -47,6 +50,24 @@ namespace MWSound
return settings;
}
+
+ float initialFadeVolume(float squaredDist, Sound_Buffer *sfx, Type type, PlayMode mode)
+ {
+ // If a sound is farther away than its maximum distance, start playing it with a zero fade volume.
+ // It can still become audible once the player moves closer.
+ const float maxDist = sfx->getMaxDist();
+ if (squaredDist > (maxDist * maxDist))
+ return 0.0f;
+
+ // This is a *heuristic* that causes environment sounds to fade in. The idea is the following:
+ // - Only looped sounds playing through the effects channel are environment sounds
+ // - Do not fade in sounds if the player is already so close that the sound plays at maximum volume
+ const float minDist = sfx->getMinDist();
+ if ((squaredDist > (minDist * minDist)) && (type == Type::Sfx) && (mode & PlayMode::Loop))
+ return 0.0f;
+
+ return 1.0;
+ }
}
// For combining PlayMode and Type flags
@@ -125,19 +146,7 @@ namespace MWSound
try
{
DecoderPtr decoder = getDecoder();
-
- // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav.
- if(mVFS->exists(voicefile))
- decoder->open(voicefile);
- else
- {
- std::string file = voicefile;
- std::string::size_type pos = file.rfind('.');
- if(pos != std::string::npos)
- file = file.substr(0, pos)+".mp3";
- decoder->open(file);
- }
-
+ decoder->open(Misc::ResourceHelpers::correctSoundPath(voicefile, decoder->mResourceMgr));
return decoder;
}
catch(std::exception &e)
@@ -517,7 +526,8 @@ namespace MWSound
return nullptr;
const osg::Vec3f objpos(ptr.getRefData().getPosition().asVec3());
- if ((mode & PlayMode::RemoveAtDistance) && (mListenerPos - objpos).length2() > 2000 * 2000)
+ const float squaredDist = (mListenerPos - objpos).length2();
+ if ((mode & PlayMode::RemoveAtDistance) && squaredDist > 2000 * 2000)
return nullptr;
// Look up the sound in the ESM data
@@ -548,6 +558,7 @@ namespace MWSound
params.mPos = objpos;
params.mVolume = volume * sfx->getVolume();
params.mBaseVolume = volumeFromType(type);
+ params.mFadeVolume = initialFadeVolume(squaredDist, sfx, type, mode);
params.mPitch = pitch;
params.mMinDistance = sfx->getMinDist();
params.mMaxDistance = sfx->getMaxDist();
@@ -576,12 +587,15 @@ namespace MWSound
Sound_Buffer *sfx = mSoundBuffers.load(Misc::StringUtils::lowerCase(soundId));
if(!sfx) return nullptr;
+ const float squaredDist = (mListenerPos - initialPos).length2();
+
SoundPtr sound = getSoundRef();
sound->init([&] {
SoundParams params;
params.mPos = initialPos;
params.mVolume = volume * sfx->getVolume();
params.mBaseVolume = volumeFromType(type);
+ params.mFadeVolume = initialFadeVolume(squaredDist, sfx, type, mode);
params.mPitch = pitch;
params.mMinDistance = sfx->getMinDist();
params.mMaxDistance = sfx->getMaxDist();
@@ -777,10 +791,10 @@ namespace MWSound
break;
case WaterSoundAction::SetVolume:
mNearWaterSound->setVolume(update.mVolume * sfx->getVolume());
+ mNearWaterSound->setFade(sSfxFadeInDuration, 1.0f, Play_FadeExponential);
break;
case WaterSoundAction::FinishSound:
- mOutput->finishSound(mNearWaterSound);
- mNearWaterSound = nullptr;
+ mNearWaterSound->setFade(sSfxFadeOutDuration, 0.0f, Play_FadeExponential | Play_StopAtFadeEnd);
break;
case WaterSoundAction::PlaySound:
if (mNearWaterSound)
@@ -830,6 +844,28 @@ namespace MWSound
return {WaterSoundAction::DoNothing, nullptr};
}
+ void SoundManager::cull3DSound(SoundBase *sound)
+ {
+ // Hard-coded distance of 2000.0f is from vanilla Morrowind
+ const float maxDist = sound->getDistanceCull() ? 2000.0f : sound->getMaxDistance();
+ const float squaredMaxDist = maxDist * maxDist;
+
+ const osg::Vec3f pos = sound->getPosition();
+ const float squaredDist = (mListenerPos - pos).length2();
+
+ if (squaredDist > squaredMaxDist)
+ {
+ // If getDistanceCull() is set, delete the sound after it has faded out
+ sound->setFade(sSfxFadeOutDuration, 0.0f, Play_FadeExponential | (sound->getDistanceCull() ? Play_StopAtFadeEnd : 0));
+ }
+ else
+ {
+ // Fade sounds back in once they are in range
+ sound->setFade(sSfxFadeInDuration, 1.0f, Play_FadeExponential);
+ }
+ }
+
+
void SoundManager::updateSounds(float duration)
{
// We update active say sounds map for specific actors here
@@ -884,20 +920,15 @@ namespace MWSound
{
Sound *sound = sndidx->first.get();
- if(!ptr.isEmpty() && sound->getIs3D())
+ if (sound->getIs3D())
{
- const ESM::Position &pos = ptr.getRefData().getPosition();
- const osg::Vec3f objpos(pos.asVec3());
- sound->setPosition(objpos);
-
- if(sound->getDistanceCull())
- {
- if((mListenerPos - objpos).length2() > 2000*2000)
- mOutput->finishSound(sound);
- }
+ if (!ptr.isEmpty())
+ sound->setPosition(ptr.getRefData().getPosition().asVec3());
+
+ cull3DSound(sound);
}
- if(!mOutput->isSoundPlaying(sound))
+ if(!sound->updateFade(duration) || !mOutput->isSoundPlaying(sound))
{
mOutput->finishSound(sound);
if (sound == mUnderwaterSound)
@@ -909,8 +940,6 @@ namespace MWSound
}
else
{
- sound->updateFade(duration);
-
mOutput->updateSound(sound);
++sndidx;
}
@@ -926,28 +955,24 @@ namespace MWSound
{
MWWorld::ConstPtr ptr = sayiter->first;
Stream *sound = sayiter->second.get();
- if(!ptr.isEmpty() && sound->getIs3D())
+ if (sound->getIs3D())
{
- MWBase::World *world = MWBase::Environment::get().getWorld();
- const osg::Vec3f pos = world->getActorHeadTransform(ptr).getTrans();
- sound->setPosition(pos);
-
- if(sound->getDistanceCull())
+ if (!ptr.isEmpty())
{
- if((mListenerPos - pos).length2() > 2000*2000)
- mOutput->finishStream(sound);
+ MWBase::World *world = MWBase::Environment::get().getWorld();
+ sound->setPosition(world->getActorHeadTransform(ptr).getTrans());
}
+
+ cull3DSound(sound);
}
- if(!mOutput->isStreamPlaying(sound))
+ if(!sound->updateFade(duration) || !mOutput->isStreamPlaying(sound))
{
mOutput->finishStream(sound);
- mActiveSaySounds.erase(sayiter++);
+ sayiter = mActiveSaySounds.erase(sayiter);
}
else
{
- sound->updateFade(duration);
-
mOutput->updateStream(sound);
++sayiter;
}
diff --git a/apps/openmw/mwsound/soundmanagerimp.hpp b/apps/openmw/mwsound/soundmanagerimp.hpp
index 934402cd4c..e659e7a12a 100644
--- a/apps/openmw/mwsound/soundmanagerimp.hpp
+++ b/apps/openmw/mwsound/soundmanagerimp.hpp
@@ -34,6 +34,7 @@ namespace MWSound
{
class Sound_Output;
struct Sound_Decoder;
+ class SoundBase;
class Sound;
class Stream;
@@ -111,6 +112,8 @@ namespace MWSound
void advanceMusic(const std::string& filename);
void startRandomTitle();
+ void cull3DSound(SoundBase *sound);
+
void updateSounds(float duration);
void updateRegionSound(float duration);
void updateWaterSound();
diff --git a/apps/openmw/mwsound/volumesettings.cpp b/apps/openmw/mwsound/volumesettings.cpp
index cc4eac3d6d..fd79b97e9b 100644
--- a/apps/openmw/mwsound/volumesettings.cpp
+++ b/apps/openmw/mwsound/volumesettings.cpp
@@ -10,7 +10,7 @@ namespace MWSound
{
float clamp(float value)
{
- return std::max(0.0f, std::min(1.0f, value));
+ return std::clamp(value, 0.f, 1.f);
}
}
diff --git a/apps/openmw/mwstate/character.cpp b/apps/openmw/mwstate/character.cpp
index 59ddd2cd10..52696de104 100644
--- a/apps/openmw/mwstate/character.cpp
+++ b/apps/openmw/mwstate/character.cpp
@@ -8,6 +8,8 @@
#include <components/esm/esmreader.hpp>
#include <components/esm/defs.hpp>
+#include <components/misc/utf8stream.hpp>
+
bool MWState::operator< (const Slot& left, const Slot& right)
{
return left.mTimeStamp<right.mTimeStamp;
@@ -52,12 +54,14 @@ void MWState::Character::addSlot (const ESM::SavedGame& profile)
std::ostringstream stream;
// The profile description is user-supplied, so we need to escape the path
- for (std::string::const_iterator it = profile.mDescription.begin(); it != profile.mDescription.end(); ++it)
+ Utf8Stream description(profile.mDescription);
+ while(!description.eof())
{
- if (std::isalnum(*it)) // Ignores multibyte characters and non alphanumeric characters
- stream << *it;
+ auto c = description.consume();
+ if(c <= 0x7F && std::isalnum(c)) // Ignore multibyte characters and non alphanumeric characters
+ stream << static_cast<char>(c);
else
- stream << "_";
+ stream << '_';
}
const std::string ext = ".omwsave";
diff --git a/apps/openmw/mwstate/charactermanager.cpp b/apps/openmw/mwstate/charactermanager.cpp
index 027a4f38a4..301f33c5df 100644
--- a/apps/openmw/mwstate/charactermanager.cpp
+++ b/apps/openmw/mwstate/charactermanager.cpp
@@ -5,6 +5,8 @@
#include <boost/filesystem.hpp>
+#include <components/misc/utf8stream.hpp>
+
MWState::CharacterManager::CharacterManager (const boost::filesystem::path& saves,
const std::vector<std::string>& contentFiles)
: mPath (saves), mCurrent (nullptr), mGame (getFirstGameFile(contentFiles))
@@ -57,12 +59,14 @@ MWState::Character* MWState::CharacterManager::createCharacter(const std::string
std::ostringstream stream;
// The character name is user-supplied, so we need to escape the path
- for (std::string::const_iterator it = name.begin(); it != name.end(); ++it)
+ Utf8Stream nameStream(name);
+ while(!nameStream.eof())
{
- if (std::isalnum(*it)) // Ignores multibyte characters and non alphanumeric characters
- stream << *it;
+ auto c = nameStream.consume();
+ if(c <= 0x7F && std::isalnum(c)) // Ignore multibyte characters and non alphanumeric characters
+ stream << static_cast<char>(c);
else
- stream << "_";
+ stream << '_';
}
boost::filesystem::path path = mPath / stream.str();
diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp
index b2ac511509..0c871e4f58 100644
--- a/apps/openmw/mwworld/cellstore.cpp
+++ b/apps/openmw/mwworld/cellstore.cpp
@@ -1,4 +1,5 @@
#include "cellstore.hpp"
+#include "magiceffects.hpp"
#include <algorithm>
@@ -741,11 +742,7 @@ namespace MWWorld
case ESM::REC_NPC_: mNpcs.load(ref, deleted, store); break;
case ESM::REC_PROB: mProbes.load(ref, deleted, store); break;
case ESM::REC_REPA: mRepairs.load(ref, deleted, store); break;
- case ESM::REC_STAT:
- {
- if (ref.mRefNum.fromGroundcoverFile()) return;
- mStatics.load(ref, deleted, store); break;
- }
+ case ESM::REC_STAT: mStatics.load(ref, deleted, store); break;
case ESM::REC_WEAP: mWeapons.load(ref, deleted, store); break;
case ESM::REC_BODY: mBodyParts.load(ref, deleted, store); break;
@@ -1198,4 +1195,18 @@ namespace MWWorld
|| enchantment->mData.mType == ESM::Enchantment::WhenStrikes)
mRechargingItems.emplace_back(ptr.getBase(), static_cast<float>(enchantment->mData.mCharge));
}
+
+ Ptr MWWorld::CellStore::getMovedActor(int actorId) const
+ {
+ for(const auto& [cellRef, cell] : mMovedToAnotherCell)
+ {
+ if(cellRef->mClass->isActor() && cellRef->mData.getCustomData())
+ {
+ Ptr actor(cellRef, cell);
+ if(actor.getClass().getCreatureStats(actor).getActorId() == actorId)
+ return actor;
+ }
+ }
+ return {};
+ }
}
diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp
index 6e927fbea6..d284a291a5 100644
--- a/apps/openmw/mwworld/cellstore.hpp
+++ b/apps/openmw/mwworld/cellstore.hpp
@@ -397,6 +397,8 @@ namespace MWWorld
void respawn ();
///< Check mLastRespawn and respawn references if necessary. This is a no-op if the cell is not loaded.
+ Ptr getMovedActor(int actorId) const;
+
private:
/// Run through references and store IDs
diff --git a/apps/openmw/mwworld/contentloader.hpp b/apps/openmw/mwworld/contentloader.hpp
index b529ae9db8..55de77ad25 100644
--- a/apps/openmw/mwworld/contentloader.hpp
+++ b/apps/openmw/mwworld/contentloader.hpp
@@ -2,33 +2,20 @@
#define CONTENTLOADER_HPP
#include <boost/filesystem/path.hpp>
-#include <MyGUI_TextIterator.h>
-#include <components/debug/debuglog.hpp>
-#include "components/loadinglistener/loadinglistener.hpp"
+namespace Loading
+{
+ class Listener;
+}
namespace MWWorld
{
struct ContentLoader
{
- ContentLoader(Loading::Listener& listener)
- : mListener(listener)
- {
- }
-
- virtual ~ContentLoader()
- {
- }
-
- virtual void load(const boost::filesystem::path& filepath, int& index)
- {
- Log(Debug::Info) << "Loading content file " << filepath.string();
- mListener.setLabel(MyGUI::TextIterator::toTagsString(filepath.string()));
- }
+ virtual ~ContentLoader() = default;
- protected:
- Loading::Listener& mListener;
+ virtual void load(const boost::filesystem::path& filepath, int& index, Loading::Listener* listener) = 0;
};
} /* namespace MWWorld */
diff --git a/apps/openmw/mwworld/esmloader.cpp b/apps/openmw/mwworld/esmloader.cpp
index 1917c41428..de16e386f2 100644
--- a/apps/openmw/mwworld/esmloader.cpp
+++ b/apps/openmw/mwworld/esmloader.cpp
@@ -2,233 +2,27 @@
#include "esmstore.hpp"
#include <components/esm/esmreader.hpp>
-#include <components/esm/npcstate.hpp>
-
-#include "../mwbase/environment.hpp"
-#include "../mwbase/world.hpp"
-
-#include "../mwmechanics/magiceffects.hpp"
-
-namespace
-{
- template<class T>
- void getEnchantedItem(const std::string& id, std::string& enchantment, std::string& itemName)
- {
- const T* item = MWBase::Environment::get().getWorld()->getStore().get<T>().search(id);
- if(item)
- {
- enchantment = item->mEnchant;
- itemName = item->mName;
- }
- }
-}
namespace MWWorld
{
EsmLoader::EsmLoader(MWWorld::ESMStore& store, std::vector<ESM::ESMReader>& readers,
- ToUTF8::Utf8Encoder* encoder, Loading::Listener& listener)
- : ContentLoader(listener)
- , mEsm(readers)
- , mStore(store)
- , mEncoder(encoder)
+ ToUTF8::Utf8Encoder* encoder)
+ : mEsm(readers)
+ , mStore(store)
+ , mEncoder(encoder)
{
}
-void EsmLoader::load(const boost::filesystem::path& filepath, int& index)
+void EsmLoader::load(const boost::filesystem::path& filepath, int& index, Loading::Listener* listener)
{
- ContentLoader::load(filepath.filename(), index);
-
- ESM::ESMReader lEsm;
- lEsm.setEncoder(mEncoder);
- lEsm.setIndex(index);
- lEsm.open(filepath.string());
- lEsm.resolveParentFileIndices(mEsm);
- mEsm[index] = lEsm;
- mStore.load(mEsm[index], &mListener);
+ ESM::ESMReader lEsm;
+ lEsm.setEncoder(mEncoder);
+ lEsm.setIndex(index);
+ lEsm.open(filepath.string());
+ lEsm.resolveParentFileIndices(mEsm);
+ mEsm[index] = lEsm;
+ mStore.load(mEsm[index], listener);
}
- void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats)
- {
- const auto& store = MWBase::Environment::get().getWorld()->getStore();
- // Convert corprus to format 10
- for (const auto& [id, oldStats] : creatureStats.mSpells.mCorprusSpells)
- {
- const ESM::Spell* spell = store.get<ESM::Spell>().search(id);
- if (!spell)
- continue;
-
- ESM::CreatureStats::CorprusStats stats;
- stats.mNextWorsening = oldStats.mNextWorsening;
- for (int i=0; i<ESM::Attribute::Length; ++i)
- stats.mWorsenings[i] = 0;
-
- for (auto& effect : spell->mEffects.mList)
- {
- if (effect.mEffectID == ESM::MagicEffect::DrainAttribute)
- stats.mWorsenings[effect.mAttribute] = oldStats.mWorsenings;
- }
- creatureStats.mCorprusSpells[id] = stats;
- }
- // Convert to format 17
- for(const auto& [id, oldParams] : creatureStats.mSpells.mSpellParams)
- {
- const ESM::Spell* spell = store.get<ESM::Spell>().search(id);
- if (!spell || spell->mData.mType == ESM::Spell::ST_Spell || spell->mData.mType == ESM::Spell::ST_Power)
- continue;
- ESM::ActiveSpells::ActiveSpellParams params;
- params.mId = id;
- params.mDisplayName = spell->mName;
- params.mItem.unset();
- params.mCasterActorId = creatureStats.mActorId;
- if(spell->mData.mType == ESM::Spell::ST_Ability)
- params.mType = ESM::ActiveSpells::Type_Ability;
- else
- params.mType = ESM::ActiveSpells::Type_Permanent;
- params.mWorsenings = -1;
- int effectIndex = 0;
- for(const auto& enam : spell->mEffects.mList)
- {
- if(oldParams.mPurgedEffects.find(effectIndex) == oldParams.mPurgedEffects.end())
- {
- ESM::ActiveEffect effect;
- effect.mEffectId = enam.mEffectID;
- effect.mArg = MWMechanics::EffectKey(enam).mArg;
- effect.mDuration = -1;
- effect.mTimeLeft = -1;
- effect.mEffectIndex = effectIndex;
- auto rand = oldParams.mEffectRands.find(effectIndex);
- if(rand != oldParams.mEffectRands.end())
- {
- float magnitude = (enam.mMagnMax - enam.mMagnMin) * rand->second + enam.mMagnMin;
- effect.mMagnitude = magnitude;
- effect.mMinMagnitude = magnitude;
- effect.mMaxMagnitude = magnitude;
- // Prevent recalculation of resistances and don't reflect or absorb the effect
- effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption;
- }
- else
- {
- effect.mMagnitude = 0.f;
- effect.mMinMagnitude = enam.mMagnMin;
- effect.mMaxMagnitude = enam.mMagnMax;
- effect.mFlags = ESM::ActiveEffect::Flag_None;
- }
- params.mEffects.emplace_back(effect);
- }
- effectIndex++;
- }
- creatureStats.mActiveSpells.mSpells.emplace_back(params);
- }
- std::multimap<std::string, int> equippedItems;
- for(std::size_t i = 0; i < inventory.mItems.size(); ++i)
- {
- const ESM::ObjectState& item = inventory.mItems[i];
- auto slot = inventory.mEquipmentSlots.find(i);
- if(slot != inventory.mEquipmentSlots.end())
- equippedItems.emplace(item.mRef.mRefID, slot->second);
- }
- for(const auto& [id, oldMagnitudes] : inventory.mPermanentMagicEffectMagnitudes)
- {
- std::string eId;
- std::string name;
- switch(store.find(id))
- {
- case ESM::REC_ARMO:
- getEnchantedItem<ESM::Armor>(id, eId, name);
- break;
- case ESM::REC_CLOT:
- getEnchantedItem<ESM::Clothing>(id, eId, name);
- break;
- case ESM::REC_WEAP:
- getEnchantedItem<ESM::Weapon>(id, eId, name);
- break;
- }
- if(eId.empty())
- continue;
- const ESM::Enchantment* enchantment = store.get<ESM::Enchantment>().search(eId);
- if(!enchantment)
- continue;
- ESM::ActiveSpells::ActiveSpellParams params;
- params.mId = id;
- params.mDisplayName = name;
- params.mCasterActorId = creatureStats.mActorId;
- params.mType = ESM::ActiveSpells::Type_Enchantment;
- params.mWorsenings = -1;
- for(std::size_t effectIndex = 0; effectIndex < oldMagnitudes.size() && effectIndex < enchantment->mEffects.mList.size(); ++effectIndex)
- {
- const auto& enam = enchantment->mEffects.mList[effectIndex];
- auto [random, multiplier] = oldMagnitudes[effectIndex];
- float magnitude = (enam.mMagnMax - enam.mMagnMin) * random + enam.mMagnMin;
- magnitude *= multiplier;
- if(magnitude <= 0)
- continue;
- ESM::ActiveEffect effect;
- effect.mEffectId = enam.mEffectID;
- effect.mMagnitude = magnitude;
- effect.mMinMagnitude = magnitude;
- effect.mMaxMagnitude = magnitude;
- effect.mArg = MWMechanics::EffectKey(enam).mArg;
- effect.mDuration = -1;
- effect.mTimeLeft = -1;
- effect.mEffectIndex = static_cast<int>(effectIndex);
- // Prevent recalculation of resistances and don't reflect or absorb the effect
- effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption;
- params.mEffects.emplace_back(effect);
- }
- auto [begin, end] = equippedItems.equal_range(id);
- for(auto it = begin; it != end; ++it)
- {
- params.mItem = { static_cast<unsigned int>(it->second), 0 };
- creatureStats.mActiveSpells.mSpells.emplace_back(params);
- }
- }
- for(const auto& spell : creatureStats.mCorprusSpells)
- {
- auto it = std::find_if(creatureStats.mActiveSpells.mSpells.begin(), creatureStats.mActiveSpells.mSpells.end(), [&] (const auto& params) { return params.mId == spell.first; });
- if(it != creatureStats.mActiveSpells.mSpells.end())
- {
- it->mNextWorsening = spell.second.mNextWorsening;
- int worsenings = 0;
- for(int i = 0; i < ESM::Attribute::Length; ++i)
- worsenings = std::max(spell.second.mWorsenings[i], worsenings);
- it->mWorsenings = worsenings;
- }
- }
- for(const auto& [key, actorId] : creatureStats.mSummonedCreatureMap)
- {
- if(actorId == -1)
- continue;
- for(auto& params : creatureStats.mActiveSpells.mSpells)
- {
- if(params.mId == key.mSourceId)
- {
- bool found = false;
- for(auto& effect : params.mEffects)
- {
- if(effect.mEffectId == key.mEffectId && effect.mEffectIndex == key.mEffectIndex)
- {
- effect.mArg = actorId;
- found = true;
- break;
- }
- }
- if(found)
- break;
- }
- }
- }
- // Reset modifiers that were previously recalculated each frame
- for(std::size_t i = 0; i < ESM::Attribute::Length; ++i)
- creatureStats.mAttributes[i].mMod = 0.f;
- for(std::size_t i = 0; i < 3; ++i)
- creatureStats.mDynamic[i].mMod = 0.f;
- for(std::size_t i = 0; i < 4; ++i)
- creatureStats.mAiSettings[i].mMod = 0.f;
- if(npcStats)
- {
- for(std::size_t i = 0; i < ESM::Skill::Length; ++i)
- npcStats->mSkills[i].mMod = 0.f;
- }
- }
} /* namespace MWWorld */
diff --git a/apps/openmw/mwworld/esmloader.hpp b/apps/openmw/mwworld/esmloader.hpp
index 50631603de..db50d44146 100644
--- a/apps/openmw/mwworld/esmloader.hpp
+++ b/apps/openmw/mwworld/esmloader.hpp
@@ -13,9 +13,6 @@ namespace ToUTF8
namespace ESM
{
class ESMReader;
- struct CreatureStats;
- struct InventoryState;
- struct NpcStats;
}
namespace MWWorld
@@ -26,18 +23,16 @@ class ESMStore;
struct EsmLoader : public ContentLoader
{
EsmLoader(MWWorld::ESMStore& store, std::vector<ESM::ESMReader>& readers,
- ToUTF8::Utf8Encoder* encoder, Loading::Listener& listener);
+ ToUTF8::Utf8Encoder* encoder);
- void load(const boost::filesystem::path& filepath, int& index) override;
+ void load(const boost::filesystem::path& filepath, int& index, Loading::Listener* listener) override;
private:
- std::vector<ESM::ESMReader>& mEsm;
- MWWorld::ESMStore& mStore;
- ToUTF8::Utf8Encoder* mEncoder;
+ std::vector<ESM::ESMReader>& mEsm;
+ MWWorld::ESMStore& mStore;
+ ToUTF8::Utf8Encoder* mEncoder;
};
-void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats = nullptr);
-
} /* namespace MWWorld */
#endif // ESMLOADER_HPP
diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp
index bafdc8f37d..1284df694a 100644
--- a/apps/openmw/mwworld/esmstore.cpp
+++ b/apps/openmw/mwworld/esmstore.cpp
@@ -153,7 +153,7 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener)
// Land texture loading needs to use a separate internal store for each plugin.
// We set the number of plugins here so we can properly verify if valid plugin
// indices are being passed to the LandTexture Store retrieval methods.
- mLandTextures.addPlugin();
+ mLandTextures.resize(esm.getIndex()+1);
// Loop through all records
while(esm.hasMoreRecs())
diff --git a/apps/openmw/mwworld/groundcoverstore.cpp b/apps/openmw/mwworld/groundcoverstore.cpp
new file mode 100644
index 0000000000..543cd3e0d8
--- /dev/null
+++ b/apps/openmw/mwworld/groundcoverstore.cpp
@@ -0,0 +1,54 @@
+#include "groundcoverstore.hpp"
+
+#include <components/esmloader/load.hpp>
+#include <components/misc/stringops.hpp>
+
+namespace MWWorld
+{
+ void GroundcoverStore::init(const Store<ESM::Static>& statics, const Files::Collections& fileCollections, const std::vector<std::string>& groundcoverFiles, ToUTF8::Utf8Encoder* encoder)
+ {
+ EsmLoader::Query query;
+ query.mLoadStatics = true;
+ query.mLoadCells = true;
+
+ std::vector<ESM::ESMReader> readers(groundcoverFiles.size());
+ const EsmLoader::EsmData content = EsmLoader::loadEsmData(query, groundcoverFiles, fileCollections, readers, encoder);
+
+ for (const ESM::Static& stat : statics)
+ {
+ std::string id = Misc::StringUtils::lowerCase(stat.mId);
+ mMeshCache[id] = "meshes\\" + Misc::StringUtils::lowerCase(stat.mModel);
+ }
+
+ for (const ESM::Static& stat : content.mStatics)
+ {
+ std::string id = Misc::StringUtils::lowerCase(stat.mId);
+ mMeshCache[id] = "meshes\\" + Misc::StringUtils::lowerCase(stat.mModel);
+ }
+
+ for (const ESM::Cell& cell : content.mCells)
+ {
+ if (!cell.isExterior()) continue;
+ auto cellIndex = std::make_pair(cell.getCellId().mIndex.mX, cell.getCellId().mIndex.mY);
+ mCellContexts[cellIndex] = std::move(cell.mContextList);
+ }
+ }
+
+ std::string GroundcoverStore::getGroundcoverModel(const std::string& id) const
+ {
+ std::string idLower = Misc::StringUtils::lowerCase(id);
+ auto search = mMeshCache.find(idLower);
+ if (search == mMeshCache.end()) return std::string();
+
+ return search->second;
+ }
+
+ void GroundcoverStore::initCell(ESM::Cell& cell, int cellX, int cellY) const
+ {
+ cell.blank();
+
+ auto searchCell = mCellContexts.find(std::make_pair(cellX, cellY));
+ if (searchCell != mCellContexts.end())
+ cell.mContextList = searchCell->second;
+ }
+}
diff --git a/apps/openmw/mwworld/groundcoverstore.hpp b/apps/openmw/mwworld/groundcoverstore.hpp
new file mode 100644
index 0000000000..197be2a998
--- /dev/null
+++ b/apps/openmw/mwworld/groundcoverstore.hpp
@@ -0,0 +1,29 @@
+#ifndef GAME_MWWORLD_GROUNDCOVER_STORE_H
+#define GAME_MWWORLD_GROUNDCOVER_STORE_H
+
+#include <vector>
+#include <string>
+#include <map>
+
+#include <components/esm/esmreader.hpp>
+#include <components/esmloader/esmdata.hpp>
+#include <components/files/collections.hpp>
+
+#include "esmstore.hpp"
+
+namespace MWWorld
+{
+ class GroundcoverStore
+ {
+ private:
+ std::map<std::string, std::string> mMeshCache;
+ std::map<std::pair<int, int>, std::vector<ESM::ESM_Context>> mCellContexts;
+
+ public:
+ void init(const Store<ESM::Static>& statics, const Files::Collections& fileCollections, const std::vector<std::string>& groundcoverFiles, ToUTF8::Utf8Encoder* encoder);
+ std::string getGroundcoverModel(const std::string& id) const;
+ void initCell(ESM::Cell& cell, int cellX, int cellY) const;
+ };
+}
+
+#endif
diff --git a/apps/openmw/mwworld/magiceffects.cpp b/apps/openmw/mwworld/magiceffects.cpp
new file mode 100644
index 0000000000..7d7e2857fe
--- /dev/null
+++ b/apps/openmw/mwworld/magiceffects.cpp
@@ -0,0 +1,210 @@
+#include "magiceffects.hpp"
+#include "esmstore.hpp"
+
+#include <components/esm/npcstate.hpp>
+
+#include "../mwbase/environment.hpp"
+#include "../mwbase/world.hpp"
+
+#include "../mwmechanics/magiceffects.hpp"
+
+namespace
+{
+ template<class T>
+ void getEnchantedItem(const std::string& id, std::string& enchantment, std::string& itemName)
+ {
+ const T* item = MWBase::Environment::get().getWorld()->getStore().get<T>().search(id);
+ if(item)
+ {
+ enchantment = item->mEnchant;
+ itemName = item->mName;
+ }
+ }
+}
+
+namespace MWWorld
+{
+ void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats)
+ {
+ const auto& store = MWBase::Environment::get().getWorld()->getStore();
+ // Convert corprus to format 10
+ for (const auto& [id, oldStats] : creatureStats.mSpells.mCorprusSpells)
+ {
+ const ESM::Spell* spell = store.get<ESM::Spell>().search(id);
+ if (!spell)
+ continue;
+
+ ESM::CreatureStats::CorprusStats stats;
+ stats.mNextWorsening = oldStats.mNextWorsening;
+ for (int i=0; i<ESM::Attribute::Length; ++i)
+ stats.mWorsenings[i] = 0;
+
+ for (auto& effect : spell->mEffects.mList)
+ {
+ if (effect.mEffectID == ESM::MagicEffect::DrainAttribute)
+ stats.mWorsenings[effect.mAttribute] = oldStats.mWorsenings;
+ }
+ creatureStats.mCorprusSpells[id] = stats;
+ }
+ // Convert to format 17
+ for(const auto& [id, oldParams] : creatureStats.mSpells.mSpellParams)
+ {
+ const ESM::Spell* spell = store.get<ESM::Spell>().search(id);
+ if (!spell || spell->mData.mType == ESM::Spell::ST_Spell || spell->mData.mType == ESM::Spell::ST_Power)
+ continue;
+ ESM::ActiveSpells::ActiveSpellParams params;
+ params.mId = id;
+ params.mDisplayName = spell->mName;
+ params.mItem.unset();
+ params.mCasterActorId = creatureStats.mActorId;
+ if(spell->mData.mType == ESM::Spell::ST_Ability)
+ params.mType = ESM::ActiveSpells::Type_Ability;
+ else
+ params.mType = ESM::ActiveSpells::Type_Permanent;
+ params.mWorsenings = -1;
+ int effectIndex = 0;
+ for(const auto& enam : spell->mEffects.mList)
+ {
+ if(oldParams.mPurgedEffects.find(effectIndex) == oldParams.mPurgedEffects.end())
+ {
+ ESM::ActiveEffect effect;
+ effect.mEffectId = enam.mEffectID;
+ effect.mArg = MWMechanics::EffectKey(enam).mArg;
+ effect.mDuration = -1;
+ effect.mTimeLeft = -1;
+ effect.mEffectIndex = effectIndex;
+ auto rand = oldParams.mEffectRands.find(effectIndex);
+ if(rand != oldParams.mEffectRands.end())
+ {
+ float magnitude = (enam.mMagnMax - enam.mMagnMin) * rand->second + enam.mMagnMin;
+ effect.mMagnitude = magnitude;
+ effect.mMinMagnitude = magnitude;
+ effect.mMaxMagnitude = magnitude;
+ // Prevent recalculation of resistances and don't reflect or absorb the effect
+ effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption;
+ }
+ else
+ {
+ effect.mMagnitude = 0.f;
+ effect.mMinMagnitude = enam.mMagnMin;
+ effect.mMaxMagnitude = enam.mMagnMax;
+ effect.mFlags = ESM::ActiveEffect::Flag_None;
+ }
+ params.mEffects.emplace_back(effect);
+ }
+ effectIndex++;
+ }
+ creatureStats.mActiveSpells.mSpells.emplace_back(params);
+ }
+ std::multimap<std::string, int> equippedItems;
+ for(std::size_t i = 0; i < inventory.mItems.size(); ++i)
+ {
+ const ESM::ObjectState& item = inventory.mItems[i];
+ auto slot = inventory.mEquipmentSlots.find(i);
+ if(slot != inventory.mEquipmentSlots.end())
+ equippedItems.emplace(item.mRef.mRefID, slot->second);
+ }
+ for(const auto& [id, oldMagnitudes] : inventory.mPermanentMagicEffectMagnitudes)
+ {
+ std::string eId;
+ std::string name;
+ switch(store.find(id))
+ {
+ case ESM::REC_ARMO:
+ getEnchantedItem<ESM::Armor>(id, eId, name);
+ break;
+ case ESM::REC_CLOT:
+ getEnchantedItem<ESM::Clothing>(id, eId, name);
+ break;
+ case ESM::REC_WEAP:
+ getEnchantedItem<ESM::Weapon>(id, eId, name);
+ break;
+ }
+ if(eId.empty())
+ continue;
+ const ESM::Enchantment* enchantment = store.get<ESM::Enchantment>().search(eId);
+ if(!enchantment)
+ continue;
+ ESM::ActiveSpells::ActiveSpellParams params;
+ params.mId = id;
+ params.mDisplayName = name;
+ params.mCasterActorId = creatureStats.mActorId;
+ params.mType = ESM::ActiveSpells::Type_Enchantment;
+ params.mWorsenings = -1;
+ for(std::size_t effectIndex = 0; effectIndex < oldMagnitudes.size() && effectIndex < enchantment->mEffects.mList.size(); ++effectIndex)
+ {
+ const auto& enam = enchantment->mEffects.mList[effectIndex];
+ auto [random, multiplier] = oldMagnitudes[effectIndex];
+ float magnitude = (enam.mMagnMax - enam.mMagnMin) * random + enam.mMagnMin;
+ magnitude *= multiplier;
+ if(magnitude <= 0)
+ continue;
+ ESM::ActiveEffect effect;
+ effect.mEffectId = enam.mEffectID;
+ effect.mMagnitude = magnitude;
+ effect.mMinMagnitude = magnitude;
+ effect.mMaxMagnitude = magnitude;
+ effect.mArg = MWMechanics::EffectKey(enam).mArg;
+ effect.mDuration = -1;
+ effect.mTimeLeft = -1;
+ effect.mEffectIndex = static_cast<int>(effectIndex);
+ // Prevent recalculation of resistances and don't reflect or absorb the effect
+ effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption;
+ params.mEffects.emplace_back(effect);
+ }
+ auto [begin, end] = equippedItems.equal_range(id);
+ for(auto it = begin; it != end; ++it)
+ {
+ params.mItem = { static_cast<unsigned int>(it->second), 0 };
+ creatureStats.mActiveSpells.mSpells.emplace_back(params);
+ }
+ }
+ for(const auto& spell : creatureStats.mCorprusSpells)
+ {
+ auto it = std::find_if(creatureStats.mActiveSpells.mSpells.begin(), creatureStats.mActiveSpells.mSpells.end(), [&] (const auto& params) { return params.mId == spell.first; });
+ if(it != creatureStats.mActiveSpells.mSpells.end())
+ {
+ it->mNextWorsening = spell.second.mNextWorsening;
+ int worsenings = 0;
+ for(int i = 0; i < ESM::Attribute::Length; ++i)
+ worsenings = std::max(spell.second.mWorsenings[i], worsenings);
+ it->mWorsenings = worsenings;
+ }
+ }
+ for(const auto& [key, actorId] : creatureStats.mSummonedCreatureMap)
+ {
+ if(actorId == -1)
+ continue;
+ for(auto& params : creatureStats.mActiveSpells.mSpells)
+ {
+ if(params.mId == key.mSourceId)
+ {
+ bool found = false;
+ for(auto& effect : params.mEffects)
+ {
+ if(effect.mEffectId == key.mEffectId && effect.mEffectIndex == key.mEffectIndex)
+ {
+ effect.mArg = actorId;
+ found = true;
+ break;
+ }
+ }
+ if(found)
+ break;
+ }
+ }
+ }
+ // Reset modifiers that were previously recalculated each frame
+ for(std::size_t i = 0; i < ESM::Attribute::Length; ++i)
+ creatureStats.mAttributes[i].mMod = 0.f;
+ for(std::size_t i = 0; i < 3; ++i)
+ creatureStats.mDynamic[i].mMod = 0.f;
+ for(std::size_t i = 0; i < 4; ++i)
+ creatureStats.mAiSettings[i].mMod = 0.f;
+ if(npcStats)
+ {
+ for(std::size_t i = 0; i < ESM::Skill::Length; ++i)
+ npcStats->mSkills[i].mMod = 0.f;
+ }
+ }
+}
diff --git a/apps/openmw/mwworld/magiceffects.hpp b/apps/openmw/mwworld/magiceffects.hpp
new file mode 100644
index 0000000000..31d5ed2038
--- /dev/null
+++ b/apps/openmw/mwworld/magiceffects.hpp
@@ -0,0 +1,17 @@
+#ifndef OPENMW_MWWORLD_MAGICEFFECTS_H
+#define OPENMW_MWWORLD_MAGICEFFECTS_H
+
+namespace ESM
+{
+ struct CreatureStats;
+ struct InventoryState;
+ struct NpcStats;
+}
+
+namespace MWWorld
+{
+ void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory,
+ ESM::NpcStats* npcStats = nullptr);
+}
+
+#endif
diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp
index caa0600f7c..270889a23e 100644
--- a/apps/openmw/mwworld/player.cpp
+++ b/apps/openmw/mwworld/player.cpp
@@ -12,6 +12,7 @@
#include "../mwworld/esmstore.hpp"
#include "../mwworld/inventorystore.hpp"
+#include "../mwworld/magiceffects.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
@@ -57,9 +58,9 @@ namespace MWWorld
MWMechanics::NpcStats& stats = getPlayer().getClass().getNpcStats(getPlayer());
for (int i=0; i<ESM::Skill::Length; ++i)
- mSaveSkills[i] = stats.getSkill(i);
+ mSaveSkills[i] = stats.getSkill(i).getModified();
for (int i=0; i<ESM::Attribute::Length; ++i)
- mSaveAttributes[i] = stats.getAttribute(i);
+ mSaveAttributes[i] = stats.getAttribute(i).getModified();
}
void Player::restoreStats()
@@ -68,11 +69,20 @@ namespace MWWorld
MWMechanics::CreatureStats& creatureStats = getPlayer().getClass().getCreatureStats(getPlayer());
MWMechanics::NpcStats& npcStats = getPlayer().getClass().getNpcStats(getPlayer());
MWMechanics::DynamicStat<float> health = creatureStats.getDynamic(0);
- creatureStats.setHealth(int(health.getBase() / gmst.find("fWereWolfHealth")->mValue.getFloat()));
+ creatureStats.setHealth(health.getBase() / gmst.find("fWereWolfHealth")->mValue.getFloat());
for (int i=0; i<ESM::Skill::Length; ++i)
- npcStats.setSkill(i, mSaveSkills[i]);
+ {
+ auto& skill = npcStats.getSkill(i);
+ skill.restore(skill.getDamage());
+ skill.setModifier(mSaveSkills[i] - skill.getBase());
+ }
for (int i=0; i<ESM::Attribute::Length; ++i)
- npcStats.setAttribute(i, mSaveAttributes[i]);
+ {
+ auto attribute = npcStats.getAttribute(i);
+ attribute.restore(attribute.getDamage());
+ attribute.setModifier(mSaveAttributes[i] - attribute.getBase());
+ npcStats.setAttribute(i, attribute);
+ }
}
void Player::setWerewolfStats()
@@ -81,7 +91,7 @@ namespace MWWorld
MWMechanics::CreatureStats& creatureStats = getPlayer().getClass().getCreatureStats(getPlayer());
MWMechanics::NpcStats& npcStats = getPlayer().getClass().getNpcStats(getPlayer());
MWMechanics::DynamicStat<float> health = creatureStats.getDynamic(0);
- creatureStats.setHealth(int(health.getBase() * gmst.find("fWereWolfHealth")->mValue.getFloat()));
+ creatureStats.setHealth(health.getBase() * gmst.find("fWereWolfHealth")->mValue.getFloat());
for(size_t i = 0;i < ESM::Attribute::Length;++i)
{
// Oh, Bethesda. It's "Intelligence".
@@ -89,7 +99,7 @@ namespace MWWorld
ESM::Attribute::sAttributeNames[i]);
MWMechanics::AttributeValue value = npcStats.getAttribute(i);
- value.setBase(int(gmst.find(name)->mValue.getFloat()));
+ value.setModifier(gmst.find(name)->mValue.getFloat() - value.getModified());
npcStats.setAttribute(i, value);
}
@@ -103,9 +113,8 @@ namespace MWWorld
std::string name = "fWerewolf"+((i==ESM::Skill::Mercantile) ? std::string("Merchantile") :
ESM::Skill::sSkillNames[i]);
- MWMechanics::SkillValue value = npcStats.getSkill(i);
- value.setBase(int(gmst.find(name)->mValue.getFloat()));
- npcStats.setSkill(i, value);
+ MWMechanics::SkillValue& value = npcStats.getSkill(i);
+ value.setModifier(gmst.find(name)->mValue.getFloat() - value.getModified());
}
}
@@ -315,14 +324,12 @@ namespace MWWorld
for (int i=0; i<ESM::Skill::Length; ++i)
{
- mSaveSkills[i].setBase(0);
- mSaveSkills[i].setModifier(0);
+ mSaveSkills[i] = 0.f;
}
for (int i=0; i<ESM::Attribute::Length; ++i)
{
- mSaveAttributes[i].setBase(0);
- mSaveAttributes[i].setModifier(0);
+ mSaveAttributes[i] = 0.f;
}
mMarkedPosition.pos[0] = 0;
@@ -359,9 +366,9 @@ namespace MWWorld
player.mHasMark = false;
for (int i=0; i<ESM::Attribute::Length; ++i)
- mSaveAttributes[i].writeState(player.mSaveAttributes[i]);
+ player.mSaveAttributes[i] = mSaveAttributes[i];
for (int i=0; i<ESM::Skill::Length; ++i)
- mSaveSkills[i].writeState(player.mSaveSkills[i]);
+ player.mSaveSkills[i] = mSaveSkills[i];
player.mPreviousItems = mPreviousItems;
@@ -383,13 +390,7 @@ namespace MWWorld
throw std::runtime_error ("invalid player state record (object state)");
}
if (reader.getFormat() < 17)
- {
convertMagicEffects(player.mObject.mCreatureStats, player.mObject.mInventory, &player.mObject.mNpcStats);
- for(std::size_t i = 0; i < ESM::Attribute::Length; ++i)
- player.mSaveAttributes[i].mMod = 0.f;
- for(std::size_t i = 0; i < ESM::Skill::Length; ++i)
- player.mSaveSkills[i].mMod = 0.f;
- }
if (!player.mObject.mEnabled)
{
@@ -400,14 +401,23 @@ namespace MWWorld
mPlayer.load (player.mObject);
for (int i=0; i<ESM::Attribute::Length; ++i)
- mSaveAttributes[i].readState(player.mSaveAttributes[i]);
+ mSaveAttributes[i] = player.mSaveAttributes[i];
for (int i=0; i<ESM::Skill::Length; ++i)
- mSaveSkills[i].readState(player.mSaveSkills[i]);
+ mSaveSkills[i] = player.mSaveSkills[i];
- if (player.mObject.mNpcStats.mWerewolfDeprecatedData && player.mObject.mNpcStats.mIsWerewolf)
+ if (player.mObject.mNpcStats.mIsWerewolf)
{
- saveStats();
- setWerewolfStats();
+ if (player.mObject.mNpcStats.mWerewolfDeprecatedData)
+ {
+ saveStats();
+ setWerewolfStats();
+ }
+ else if (reader.getFormat() < 19)
+ {
+ setWerewolfStats();
+ if (player.mSetWerewolfAcrobatics)
+ MWBase::Environment::get().getMechanicsManager()->applyWerewolfAcrobatics(getPlayer());
+ }
}
getPlayer().getClass().getCreatureStats(getPlayer()).getAiSequence().clear();
diff --git a/apps/openmw/mwworld/player.hpp b/apps/openmw/mwworld/player.hpp
index a7e42d95e1..1a9744e8a3 100644
--- a/apps/openmw/mwworld/player.hpp
+++ b/apps/openmw/mwworld/player.hpp
@@ -53,8 +53,8 @@ namespace MWWorld
PreviousItems mPreviousItems;
// Saved stats prior to becoming a werewolf
- MWMechanics::SkillValue mSaveSkills[ESM::Skill::Length];
- MWMechanics::AttributeValue mSaveAttributes[ESM::Attribute::Length];
+ float mSaveSkills[ESM::Skill::Length];
+ float mSaveAttributes[ESM::Attribute::Length];
bool mAttackingOrSpell;
bool mJumping;
diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp
index e6a7195f5a..3bff1854a6 100644
--- a/apps/openmw/mwworld/projectilemanager.cpp
+++ b/apps/openmw/mwworld/projectilemanager.cpp
@@ -22,6 +22,8 @@
#include <components/sceneutil/lightmanager.hpp>
#include <components/sceneutil/nodecallback.hpp>
+#include <components/settings/settings.hpp>
+
#include "../mwworld/manualref.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
@@ -407,6 +409,7 @@ namespace MWWorld
void ProjectileManager::moveMagicBolts(float duration)
{
+ static const bool normaliseRaceSpeed = Settings::Manager::getBool("normalise race speed", "Game");
for (auto& magicBoltState : mMagicBolts)
{
if (magicBoltState.mToDelete)
@@ -426,10 +429,16 @@ namespace MWWorld
}
}
+ const auto& store = MWBase::Environment::get().getWorld()->getStore();
osg::Quat orient = magicBoltState.mNode->getAttitude();
- static float fTargetSpellMaxSpeed = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>()
- .find("fTargetSpellMaxSpeed")->mValue.getFloat();
+ static float fTargetSpellMaxSpeed = store.get<ESM::GameSetting>().find("fTargetSpellMaxSpeed")->mValue.getFloat();
float speed = fTargetSpellMaxSpeed * magicBoltState.mSpeed;
+ if (!normaliseRaceSpeed && !caster.isEmpty() && caster.getClass().isNpc())
+ {
+ const auto npc = caster.get<ESM::NPC>()->mBase;
+ const auto race = store.get<ESM::Race>().find(npc->mRace);
+ speed *= npc->isMale() ? race->mData.mWeight.mMale : race->mData.mWeight.mFemale;
+ }
osg::Vec3f direction = orient * osg::Vec3f(0,1,0);
direction.normalize();
projectile->setVelocity(direction * speed);
diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp
index f9e792c7d2..054e26e855 100644
--- a/apps/openmw/mwworld/scene.cpp
+++ b/apps/openmw/mwworld/scene.cpp
@@ -64,14 +64,23 @@ namespace
* osg::Quat(zr, osg::Vec3(0, 0, -1));
}
- osg::Quat makeNodeRotation(const MWWorld::Ptr& ptr, RotationOrder order)
+ osg::Quat makeInverseNodeRotation(const MWWorld::Ptr& ptr)
{
const auto pos = ptr.getRefData().getPosition();
+ return ptr.getClass().isActor() ? makeActorOsgQuat(pos) : makeInversedOrderObjectOsgQuat(pos);
+ }
- const auto rot = ptr.getClass().isActor() ? makeActorOsgQuat(pos)
- : (order == RotationOrder::inverse ? makeInversedOrderObjectOsgQuat(pos) : Misc::Convert::makeOsgQuat(pos));
+ osg::Quat makeDirectNodeRotation(const MWWorld::Ptr& ptr)
+ {
+ const auto pos = ptr.getRefData().getPosition();
+ return ptr.getClass().isActor() ? makeActorOsgQuat(pos) : Misc::Convert::makeOsgQuat(pos);
+ }
- return rot;
+ osg::Quat makeNodeRotation(const MWWorld::Ptr& ptr, RotationOrder order)
+ {
+ if (order == RotationOrder::inverse)
+ return makeInverseNodeRotation(ptr);
+ return makeDirectNodeRotation(ptr);
}
void setNodeRotation(const MWWorld::Ptr& ptr, MWRender::RenderingManager& rendering, const osg::Quat &rotation)
@@ -101,7 +110,7 @@ namespace
}
std::string model = getModel(ptr, rendering.getResourceSystem()->getVFS());
- const auto rotation = makeNodeRotation(ptr, RotationOrder::direct);
+ const auto rotation = makeDirectNodeRotation(ptr);
const ESM::RefNum& refnum = ptr.getCellRef().getRefNum();
if (!refnum.hasContentFile() || pagedRefs.find(refnum) == pagedRefs.end())
@@ -128,6 +137,8 @@ namespace
{
if (const auto object = physics.getObject(ptr))
{
+ const DetourNavigator::ObjectTransform objectTransform {ptr.getRefData().getPosition(), ptr.getCellRef().getScale()};
+
if (ptr.getClass().isDoor() && !ptr.getCellRef().getTeleport())
{
btVector3 aabbMin;
@@ -147,19 +158,19 @@ namespace
transform.getOrigin()
);
- const auto start = Misc::Convert::makeOsgVec3f(closedDoorTransform(center + toPoint));
+ const auto start = Misc::Convert::toOsg(closedDoorTransform(center + toPoint));
const auto startPoint = physics.castRay(start, start - osg::Vec3f(0, 0, 1000), ptr, {},
MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Water);
const auto connectionStart = startPoint.mHit ? startPoint.mHitPos : start;
- const auto end = Misc::Convert::makeOsgVec3f(closedDoorTransform(center - toPoint));
+ const auto end = Misc::Convert::toOsg(closedDoorTransform(center - toPoint));
const auto endPoint = physics.castRay(end, end - osg::Vec3f(0, 0, 1000), ptr, {},
MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Water);
const auto connectionEnd = endPoint.mHit ? endPoint.mHitPos : end;
navigator.addObject(
DetourNavigator::ObjectId(object),
- DetourNavigator::DoorShapes(object->getShapeInstance(), connectionStart, connectionEnd),
+ DetourNavigator::DoorShapes(object->getShapeInstance(), objectTransform, connectionStart, connectionEnd),
transform
);
}
@@ -167,7 +178,7 @@ namespace
{
navigator.addObject(
DetourNavigator::ObjectId(object),
- DetourNavigator::ObjectShapes(object->getShapeInstance()),
+ DetourNavigator::ObjectShapes(object->getShapeInstance(), objectTransform),
object->getTransform()
);
}
@@ -339,8 +350,7 @@ namespace MWWorld
if (const auto pathgrid = world->getStore().get<ESM::Pathgrid>().search(*cell->getCell()))
mNavigator.removePathgrid(*pathgrid);
- const auto player = world->getPlayerPtr();
- mNavigator.update(player.getRefData().getPosition().asVec3());
+ mNavigator.update(world->getPlayerPtr().getRefData().getPosition().asVec3());
MWBase::Environment::get().getMechanicsManager()->drop (cell);
@@ -367,6 +377,8 @@ namespace MWWorld
const int cellX = cell->getCell()->getGridX();
const int cellY = cell->getCell()->getGridY();
+ mNavigator.setWorldspace(cell->getCell()->mCellId.mWorldspace);
+
if (cell->getCell()->isExterior())
{
osg::ref_ptr<const ESMTerrain::LandObject> land = mRendering.getLandManager()->getLand(cellX, cellY);
@@ -404,7 +416,7 @@ namespace MWWorld
return heights;
}
} ();
- mNavigator.addHeightfield(cellPosition, ESM::Land::REAL_SIZE, shift, shape);
+ mNavigator.addHeightfield(cellPosition, ESM::Land::REAL_SIZE, shape);
}
}
@@ -434,18 +446,11 @@ namespace MWWorld
if (cell->getCell()->isExterior())
{
if (const auto heightField = mPhysics->getHeightField(cellX, cellY))
- {
- const btTransform& transform =heightField->getCollisionObject()->getWorldTransform();
- mNavigator.addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE,
- osg::Vec3f(static_cast<float>(transform.getOrigin().x()),
- static_cast<float>(transform.getOrigin().y()),
- waterLevel));
- }
+ mNavigator.addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE, waterLevel);
}
else
{
- mNavigator.addWater(osg::Vec2i(cellX, cellY), std::numeric_limits<int>::max(),
- osg::Vec3f(0, 0, waterLevel));
+ mNavigator.addWater(osg::Vec2i(cellX, cellY), std::numeric_limits<int>::max(), waterLevel);
}
}
else
@@ -503,10 +508,13 @@ namespace MWWorld
void Scene::playerMoved(const osg::Vec3f &pos)
{
+ if (mCurrentCell == nullptr)
+ return;
+
const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
mNavigator.updatePlayerPosition(player.getRefData().getPosition().asVec3());
- if (!mCurrentCell || !mCurrentCell->isExterior())
+ if (!mCurrentCell->isExterior())
return;
osg::Vec2i newCell = getNewGridCenter(pos, &mCurrentGridCenter);
@@ -882,8 +890,11 @@ namespace MWWorld
addObject(ptr, *mPhysics, mRendering, mPagedRefs);
addObject(ptr, *mPhysics, mNavigator);
MWBase::Environment::get().getWorld()->scaleObject(ptr, ptr.getCellRef().getScale());
- const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
- mNavigator.update(player.getRefData().getPosition().asVec3());
+ if (mCurrentCell != nullptr)
+ {
+ const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
+ mNavigator.update(player.getRefData().getPosition().asVec3());
+ }
}
catch (std::exception& e)
{
@@ -899,8 +910,11 @@ namespace MWWorld
if (const auto object = mPhysics->getObject(ptr))
{
mNavigator.removeObject(DetourNavigator::ObjectId(object));
- const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
- mNavigator.update(player.getRefData().getPosition().asVec3());
+ if (mCurrentCell != nullptr)
+ {
+ const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
+ mNavigator.update(player.getRefData().getPosition().asVec3());
+ }
}
else if (mPhysics->getActor(ptr))
{
diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp
index 4d720a11a7..c767bd669a 100644
--- a/apps/openmw/mwworld/store.cpp
+++ b/apps/openmw/mwworld/store.cpp
@@ -981,8 +981,11 @@ namespace MWWorld
// Dialogue
//=========================================================================
+ Store<ESM::Dialogue>::Store()
+ : mKeywordSearchModFlag(true)
+ {
+ }
- template<>
void Store<ESM::Dialogue>::setUp()
{
// DialInfos marked as deleted are kept during the loading phase, so that the linked list
@@ -997,9 +1000,46 @@ namespace MWWorld
// TODO: verify and document this inconsistent behaviour
// TODO: if we require this behaviour, maybe we should move it to the place that requires it
std::sort(mShared.begin(), mShared.end(), [](const ESM::Dialogue* l, const ESM::Dialogue* r) -> bool { return l->mId < r->mId; });
+
+ mKeywordSearchModFlag = true;
+ }
+
+ const ESM::Dialogue *Store<ESM::Dialogue>::search(const std::string &id) const
+ {
+ typename Static::const_iterator it = mStatic.find(id);
+ if (it != mStatic.end())
+ return &(it->second);
+
+ return nullptr;
+ }
+
+ const ESM::Dialogue *Store<ESM::Dialogue>::find(const std::string &id) const
+ {
+ const ESM::Dialogue *ptr = search(id);
+ if (ptr == nullptr)
+ {
+ std::stringstream msg;
+ msg << ESM::Dialogue::getRecordType() << " '" << id << "' not found";
+ throw std::runtime_error(msg.str());
+ }
+ return ptr;
+ }
+
+ typename Store<ESM::Dialogue>::iterator Store<ESM::Dialogue>::begin() const
+ {
+ return mShared.begin();
+ }
+
+ typename Store<ESM::Dialogue>::iterator Store<ESM::Dialogue>::end() const
+ {
+ return mShared.end();
+ }
+
+ size_t Store<ESM::Dialogue>::getSize() const
+ {
+ return mShared.size();
}
- template <>
inline RecordId Store<ESM::Dialogue>::load(ESM::ESMReader &esm) {
// The original letter case of a dialogue ID is saved, because it's printed
ESM::Dialogue dialogue;
@@ -1018,17 +1058,40 @@ namespace MWWorld
found->second.loadData(esm, isDeleted);
dialogue.mId = found->second.mId;
}
+
+ mKeywordSearchModFlag = true;
return RecordId(dialogue.mId, isDeleted);
}
- template<>
bool Store<ESM::Dialogue>::eraseStatic(const std::string &id)
{
- mStatic.erase(id);
+ if (mStatic.erase(id))
+ mKeywordSearchModFlag = true;
+
return true;
}
+ const MWDialogue::KeywordSearch<std::string, int>& Store<ESM::Dialogue>::getDialogIdKeywordSearch() const
+ {
+ if (mKeywordSearchModFlag)
+ {
+ mKeywordSearch.clear();
+
+ std::vector<std::string> keywordList;
+ keywordList.reserve(getSize());
+ for (const auto& it : *this)
+ keywordList.push_back(Misc::StringUtils::lowerCase(it.mId));
+ sort(keywordList.begin(), keywordList.end());
+
+ for (const auto& it : keywordList)
+ mKeywordSearch.seed(it, 0 /*unused*/);
+
+ mKeywordSearchModFlag = false;
+ }
+
+ return mKeywordSearch;
+ }
}
template class MWWorld::Store<ESM::Activator>;
@@ -1044,7 +1107,7 @@ template class MWWorld::Store<ESM::Clothing>;
template class MWWorld::Store<ESM::Container>;
template class MWWorld::Store<ESM::Creature>;
template class MWWorld::Store<ESM::CreatureLevList>;
-template class MWWorld::Store<ESM::Dialogue>;
+//template class MWWorld::Store<ESM::Dialogue>;
template class MWWorld::Store<ESM::Door>;
template class MWWorld::Store<ESM::Enchantment>;
template class MWWorld::Store<ESM::Faction>;
diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp
index 17a37c23ea..1ec51ad5fd 100644
--- a/apps/openmw/mwworld/store.hpp
+++ b/apps/openmw/mwworld/store.hpp
@@ -11,6 +11,8 @@
#include <components/esm/records.hpp>
#include <components/misc/stringops.hpp>
+#include "../mwdialogue/keywordsearch.hpp"
+
namespace ESM
{
struct Land;
@@ -154,7 +156,6 @@ namespace MWWorld
/// @par mShared usually preserves the record order as it came from the content files (this
/// is relevant for the spell autocalc code and selection order
/// for heads/hairs in the character creation)
- /// @warning ESM::Dialogue Store currently implements a sorted order for unknown reasons.
std::vector<T*> mShared;
typedef std::unordered_map<std::string, T, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> Dynamic;
Dynamic mDynamic;
@@ -222,8 +223,7 @@ namespace MWWorld
const ESM::LandTexture *search(size_t index, size_t plugin) const;
const ESM::LandTexture *find(size_t index, size_t plugin) const;
- /// Resize the internal store to hold another plugin.
- void addPlugin() { mStatic.emplace_back(); }
+ void resize(size_t num) { mStatic.resize(num); }
size_t getSize() const override;
size_t getSize(size_t plugin) const;
@@ -441,6 +441,41 @@ namespace MWWorld
iterator end() const;
};
+ template <>
+ class Store<ESM::Dialogue> : public StoreBase
+ {
+ typedef std::unordered_map<std::string, ESM::Dialogue, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> Static;
+ Static mStatic;
+ /// @par mShared usually preserves the record order as it came from the content files (this
+ /// is relevant for the spell autocalc code and selection order
+ /// for heads/hairs in the character creation)
+ /// @warning ESM::Dialogue Store currently implements a sorted order for unknown reasons.
+ std::vector<ESM::Dialogue*> mShared;
+
+ mutable bool mKeywordSearchModFlag;
+ mutable MWDialogue::KeywordSearch<std::string, int /*unused*/> mKeywordSearch;
+
+ public:
+ Store();
+
+ typedef SharedIterator<ESM::Dialogue> iterator;
+
+ void setUp() override;
+
+ const ESM::Dialogue *search(const std::string &id) const;
+ const ESM::Dialogue *find(const std::string &id) const;
+
+ iterator begin() const;
+ iterator end() const;
+
+ size_t getSize() const override;
+
+ bool eraseStatic(const std::string &id) override;
+
+ RecordId load(ESM::ESMReader &esm) override;
+
+ const MWDialogue::KeywordSearch<std::string, int>& getDialogIdKeywordSearch() const;
+ };
} //end namespace
diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp
index d35f72e80c..501378d7cc 100644
--- a/apps/openmw/mwworld/worldimp.cpp
+++ b/apps/openmw/mwworld/worldimp.cpp
@@ -7,6 +7,8 @@
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
+#include <MyGUI_TextIterator.h>
+
#include <components/debug/debuglog.hpp>
#include <components/esm/esmreader.hpp>
@@ -27,10 +29,10 @@
#include <components/sceneutil/positionattitudetransform.hpp>
-#include <components/detournavigator/debug.hpp>
-#include <components/detournavigator/navigatorimpl.hpp>
-#include <components/detournavigator/navigatorstub.hpp>
-#include <components/detournavigator/recastglobalallocator.hpp>
+#include <components/detournavigator/navigator.hpp>
+#include <components/detournavigator/settings.hpp>
+
+#include <components/loadinglistener/loadinglistener.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/soundmanager.hpp"
@@ -81,43 +83,40 @@ namespace MWWorld
{
struct GameContentLoader : public ContentLoader
{
- GameContentLoader(Loading::Listener& listener)
- : ContentLoader(listener)
+ void addLoader(std::string&& extension, ContentLoader& loader)
{
+ mLoaders.emplace(std::move(extension), &loader);
}
- bool addLoader(const std::string& extension, ContentLoader* loader)
+ void load(const boost::filesystem::path& filepath, int& index, Loading::Listener* listener) override
{
- return mLoaders.insert(std::make_pair(extension, loader)).second;
- }
-
- void load(const boost::filesystem::path& filepath, int& index) override
- {
- LoadersContainer::iterator it(mLoaders.find(Misc::StringUtils::lowerCase(filepath.extension().string())));
+ const auto it = mLoaders.find(Misc::StringUtils::lowerCase(filepath.extension().string()));
if (it != mLoaders.end())
{
- it->second->load(filepath, index);
+ const std::string filename = filepath.filename().string();
+ Log(Debug::Info) << "Loading content file " << filename;
+ if (listener != nullptr)
+ listener->setLabel(MyGUI::TextIterator::toTagsString(filename));
+ it->second->load(filepath, index, listener);
}
else
{
- std::string msg("Cannot load file: ");
- msg += filepath.string();
- throw std::runtime_error(msg.c_str());
+ std::string msg("Cannot load file: ");
+ msg += filepath.string();
+ throw std::runtime_error(msg.c_str());
}
}
private:
- typedef std::map<std::string, ContentLoader*> LoadersContainer;
- LoadersContainer mLoaders;
+ std::map<std::string, ContentLoader*> mLoaders;
};
struct OMWScriptsLoader : public ContentLoader
{
ESMStore& mStore;
- OMWScriptsLoader(Loading::Listener& listener, ESMStore& store) : ContentLoader(listener), mStore(store) {}
- void load(const boost::filesystem::path& filepath, int& index) override
+ OMWScriptsLoader(ESMStore& store) : mStore(store) {}
+ void load(const boost::filesystem::path& filepath, int& /*index*/, Loading::Listener* /*listener*/) override
{
- ContentLoader::load(filepath.filename(), index);
mStore.addOMWScripts(filepath.string());
}
};
@@ -154,33 +153,24 @@ namespace MWWorld
mLevitationEnabled(true), mGoToJail(false), mDaysInPrison(0),
mPlayerTraveling(false), mPlayerInJail(false), mSpellPreloadTimer(0.f)
{
- mEsm.resize(contentFiles.size() + groundcoverFiles.size());
+ mEsm.resize(contentFiles.size());
Loading::Listener* listener = MWBase::Environment::get().getWindowManager()->getLoadingScreen();
listener->loadingOn();
- GameContentLoader gameContentLoader(*listener);
- EsmLoader esmLoader(mStore, mEsm, encoder, *listener);
-
- gameContentLoader.addLoader(".esm", &esmLoader);
- gameContentLoader.addLoader(".esp", &esmLoader);
- gameContentLoader.addLoader(".omwgame", &esmLoader);
- gameContentLoader.addLoader(".omwaddon", &esmLoader);
- gameContentLoader.addLoader(".project", &esmLoader);
-
- OMWScriptsLoader omwScriptsLoader(*listener, mStore);
- gameContentLoader.addLoader(".omwscripts", &omwScriptsLoader);
-
- loadContentFiles(fileCollections, contentFiles, groundcoverFiles, gameContentLoader);
+ loadContentFiles(fileCollections, contentFiles, mStore, mEsm, encoder, listener);
+ loadGroundcoverFiles(fileCollections, groundcoverFiles, encoder);
listener->loadingOff();
- // insert records that may not be present in all versions of MW
- if (mEsm[0].getFormat() == 0)
- ensureNeededRecords();
-
- // TODO: We can and should validate before we call loadContentFiles().
- // Currently we validate here to prevent merge conflicts with groundcover ESMStore fixes.
- validateMasterFiles(mEsm);
+ // Find main game file
+ for (const ESM::ESMReader& reader : mEsm)
+ {
+ if (!Misc::StringUtils::ciEndsWith(reader.getName(), ".esm") && !Misc::StringUtils::ciEndsWith(reader.getName(), ".omwgame"))
+ continue;
+ if (reader.getFormat() == 0)
+ ensureNeededRecords(); // and insert records that may not be present in all versions of MW.
+ break;
+ }
mCurrentDate.reset(new DateTimeManager());
@@ -196,16 +186,15 @@ namespace MWWorld
if (Settings::Manager::getBool("enable", "Navigator"))
{
auto navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager();
- navigatorSettings.mSwimHeightScale = mSwimHeightScale;
- DetourNavigator::RecastGlobalAllocator::init();
- mNavigator.reset(new DetourNavigator::NavigatorImpl(navigatorSettings));
+ navigatorSettings.mRecast.mSwimHeightScale = mSwimHeightScale;
+ mNavigator = DetourNavigator::makeNavigator(navigatorSettings, userDataPath);
}
else
{
- mNavigator.reset(new DetourNavigator::NavigatorStub());
+ mNavigator = DetourNavigator::makeNavigatorStub();
}
- mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, resourceSystem, workQueue, resourcePath, *mNavigator));
+ mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, resourceSystem, workQueue, resourcePath, *mNavigator, mGroundcoverStore));
mProjectileManager.reset(new ProjectileManager(mRendering->getLightRoot(), resourceSystem, mRendering.get(), mPhysics.get()));
mRendering->preloadCommonAssets();
@@ -561,6 +550,14 @@ namespace MWWorld
const ESM::Cell *cell = mStore.get<ESM::Cell>().searchExtByName (cellName);
if (cell)
return cell;
+ // treat "Wilderness" like an empty string
+ static const std::string defaultName = mStore.get<ESM::GameSetting>().find("sDefaultCellname")->mValue.getString();
+ if (Misc::StringUtils::ciEqual(cellName, defaultName))
+ {
+ cell = mStore.get<ESM::Cell>().searchExtByName("");
+ if (cell)
+ return cell;
+ }
// didn't work -> now check for regions
for (const ESM::Region &region : mStore.get<ESM::Region>())
@@ -609,13 +606,7 @@ namespace MWWorld
void World::useDeathCamera()
{
- if(mRendering->getCamera()->isVanityOrPreviewModeEnabled() )
- {
- mRendering->getCamera()->togglePreviewMode(false);
- mRendering->getCamera()->toggleVanityMode(false);
- }
- if(mRendering->getCamera()->isFirstPerson())
- mRendering->getCamera()->toggleViewMode(true);
+ mRendering->getCamera()->setMode(MWRender::Camera::Mode::ThirdPerson);
}
MWWorld::Player& World::getPlayer()
@@ -1292,15 +1283,14 @@ namespace MWWorld
void World::scaleObject (const Ptr& ptr, float scale)
{
+ if (scale == ptr.getCellRef().getScale())
+ return;
if (mPhysics->getActor(ptr))
mNavigator->removeAgent(getPathfindingHalfExtents(ptr));
- if (scale != ptr.getCellRef().getScale())
- {
- ptr.getCellRef().setScale(scale);
- mRendering->pagingBlacklistObject(mStore.find(ptr.getCellRef().getRefId()), ptr);
- mWorldScene->removeFromPagedRefs(ptr);
- }
+ ptr.getCellRef().setScale(scale);
+ mRendering->pagingBlacklistObject(mStore.find(ptr.getCellRef().getRefId()), ptr);
+ mWorldScene->removeFromPagedRefs(ptr);
if(ptr.getRefData().getBaseNode() != nullptr)
mWorldScene->updateObjectScale(ptr);
@@ -1334,7 +1324,7 @@ namespace MWWorld
* currently it's done so for rotating the camera, which needs
* clamping.
*/
- objRot[0] = osg::clampBetween<float>(objRot[0], -osg::PI_2, osg::PI_2);
+ objRot[0] = std::clamp<float>(objRot[0], -osg::PI_2, osg::PI_2);
objRot[1] = Misc::normalizeAngle(objRot[1]);
objRot[2] = Misc::normalizeAngle(objRot[2]);
}
@@ -1541,16 +1531,19 @@ namespace MWWorld
if (const auto object = mPhysics->getObject(door.first))
updateNavigatorObject(*object);
- if (mShouldUpdateNavigator)
+ auto player = getPlayerPtr();
+ if (mShouldUpdateNavigator && player.getCell() != nullptr)
{
- mNavigator->update(getPlayerPtr().getRefData().getPosition().asVec3());
+ mNavigator->update(player.getRefData().getPosition().asVec3());
mShouldUpdateNavigator = false;
}
}
void World::updateNavigatorObject(const MWPhysics::Object& object)
{
- const DetourNavigator::ObjectShapes shapes(object.getShapeInstance());
+ const MWWorld::Ptr ptr = object.getPtr();
+ const DetourNavigator::ObjectShapes shapes(object.getShapeInstance(),
+ DetourNavigator::ObjectTransform {ptr.getRefData().getPosition(), ptr.getCellRef().getScale()});
mShouldUpdateNavigator = mNavigator->updateObject(DetourNavigator::ObjectId(&object), shapes, object.getTransform())
|| mShouldUpdateNavigator;
}
@@ -1872,7 +1865,7 @@ namespace MWWorld
}
bool isWerewolf = player.getClass().getNpcStats(player).isWerewolf();
- bool isFirstPerson = mRendering->getCamera()->isFirstPerson();
+ bool isFirstPerson = this->isFirstPerson();
if (isWerewolf && isFirstPerson)
{
float werewolfFov = Fallback::Map::getFloat("General_Werewolf_FOV");
@@ -1901,7 +1894,7 @@ namespace MWWorld
const auto& magicEffects = player.getClass().getCreatureStats(player).getMagicEffects();
if (!mGodMode)
blind = static_cast<int>(magicEffects.get(ESM::MagicEffect::Blind).getMagnitude());
- MWBase::Environment::get().getWindowManager()->setBlindness(std::max(0, std::min(100, blind)));
+ MWBase::Environment::get().getWindowManager()->setBlindness(std::clamp(blind, 0, 100));
int nightEye = static_cast<int>(magicEffects.get(ESM::MagicEffect::NightEye).getMagnitude());
mRendering->setNightEyeFactor(std::min(1.f, (nightEye/100.f)));
@@ -1942,11 +1935,12 @@ namespace MWWorld
void World::updateSoundListener()
{
+ osg::Vec3f cameraPosition = mRendering->getCamera()->getPosition();
const ESM::Position& refpos = getPlayerPtr().getRefData().getPosition();
osg::Vec3f listenerPos;
if (isFirstPerson())
- listenerPos = mRendering->getCameraPosition();
+ listenerPos = cameraPosition;
else
listenerPos = refpos.asVec3() + osg::Vec3f(0, 0, 1.85f * mPhysics->getHalfExtents(getPlayerPtr()).z());
@@ -1957,7 +1951,7 @@ namespace MWWorld
osg::Vec3f forward = listenerOrient * osg::Vec3f(0,1,0);
osg::Vec3f up = listenerOrient * osg::Vec3f(0,0,1);
- bool underwater = isUnderwater(getPlayerPtr().getCell(), mRendering->getCameraPosition());
+ bool underwater = isUnderwater(getPlayerPtr().getCell(), cameraPosition);
MWBase::Environment::get().getSoundManager()->setListenerPosDir(listenerPos, forward, up, underwater);
}
@@ -2409,7 +2403,7 @@ namespace MWWorld
bool World::isFirstPerson() const
{
- return mRendering->getCamera()->isFirstPerson();
+ return mRendering->getCamera()->getMode() == MWRender::Camera::Mode::FirstPerson;
}
bool World::isPreviewModeEnabled() const
@@ -2417,11 +2411,6 @@ namespace MWWorld
return mRendering->getCamera()->getMode() == MWRender::Camera::Mode::Preview;
}
- void World::togglePreviewMode(bool enable)
- {
- mRendering->getCamera()->togglePreviewMode(enable);
- }
-
bool World::toggleVanityMode(bool enable)
{
return mRendering->getCamera()->toggleVanityMode(enable);
@@ -2437,25 +2426,19 @@ namespace MWWorld
mRendering->getCamera()->applyDeferredPreviewRotationToPlayer(dt);
}
- void World::allowVanityMode(bool allow)
- {
- mRendering->getCamera()->allowVanityMode(allow);
- }
+ MWRender::Camera* World::getCamera() { return mRendering->getCamera(); }
bool World::vanityRotateCamera(float * rot)
{
- if(!mRendering->getCamera()->isVanityOrPreviewModeEnabled())
+ auto* camera = mRendering->getCamera();
+ if(!camera->isVanityOrPreviewModeEnabled())
return false;
- mRendering->getCamera()->rotateCamera(rot[0], rot[2], true);
+ camera->setPitch(camera->getPitch() + rot[0]);
+ camera->setYaw(camera->getYaw() + rot[2]);
return true;
}
- void World::adjustCameraDistance(float dist)
- {
- mRendering->getCamera()->adjustCameraDistance(dist);
- }
-
void World::saveLoaded()
{
mStore.validateDynamic();
@@ -2962,9 +2945,21 @@ namespace MWWorld
return mScriptsEnabled;
}
- void World::loadContentFiles(const Files::Collections& fileCollections,
- const std::vector<std::string>& content, const std::vector<std::string>& groundcover, ContentLoader& contentLoader)
+ void World::loadContentFiles(const Files::Collections& fileCollections, const std::vector<std::string>& content, ESMStore& store, std::vector<ESM::ESMReader>& readers, ToUTF8::Utf8Encoder* encoder, Loading::Listener* listener)
{
+ GameContentLoader gameContentLoader;
+ EsmLoader esmLoader(store, readers, encoder);
+ validateMasterFiles(readers);
+
+ gameContentLoader.addLoader(".esm", esmLoader);
+ gameContentLoader.addLoader(".esp", esmLoader);
+ gameContentLoader.addLoader(".omwgame", esmLoader);
+ gameContentLoader.addLoader(".omwaddon", esmLoader);
+ gameContentLoader.addLoader(".project", esmLoader);
+
+ OMWScriptsLoader omwScriptsLoader(store);
+ gameContentLoader.addLoader(".omwscripts", omwScriptsLoader);
+
int idx = 0;
for (const std::string &file : content)
{
@@ -2972,7 +2967,7 @@ namespace MWWorld
const Files::MultiDirCollection& col = fileCollections.getCollection(filename.extension().string());
if (col.doesExist(file))
{
- contentLoader.load(col.getPath(file), idx);
+ gameContentLoader.load(col.getPath(file), idx, listener);
}
else
{
@@ -2981,24 +2976,15 @@ namespace MWWorld
}
idx++;
}
+ }
- ESM::GroundcoverIndex = idx;
+ void World::loadGroundcoverFiles(const Files::Collections& fileCollections, const std::vector<std::string>& groundcoverFiles, ToUTF8::Utf8Encoder* encoder)
+ {
+ if (!Settings::Manager::getBool("enabled", "Groundcover")) return;
- for (const std::string &file : groundcover)
- {
- boost::filesystem::path filename(file);
- const Files::MultiDirCollection& col = fileCollections.getCollection(filename.extension().string());
- if (col.doesExist(file))
- {
- contentLoader.load(col.getPath(file), idx);
- }
- else
- {
- std::string message = "Failed loading " + file + ": the groundcover file does not exist";
- throw std::runtime_error(message);
- }
- idx++;
- }
+ Log(Debug::Info) << "Loading groundcover:";
+
+ mGroundcoverStore.init(mStore.get<ESM::Static>(), fileCollections, groundcoverFiles, encoder);
}
bool World::startSpellCast(const Ptr &actor)
@@ -3634,14 +3620,13 @@ namespace MWWorld
void World::goToJail()
{
+ const MWWorld::Ptr player = getPlayerPtr();
if (!mGoToJail)
{
// Reset bounty and forget the crime now, but don't change cell yet (the player should be able to read the dialog text first)
mGoToJail = true;
mPlayerInJail = true;
- MWWorld::Ptr player = getPlayerPtr();
-
int bounty = player.getClass().getNpcStats(player).getBounty();
player.getClass().getNpcStats(player).setBounty(0);
mPlayer->recordCrimeId();
@@ -3654,6 +3639,12 @@ namespace MWWorld
}
else
{
+ if (MWBase::Environment::get().getMechanicsManager()->isAttackPreparing(player))
+ {
+ mPlayer->setAttackingOrSpell(false);
+ }
+
+ mPlayer->setDrawState(MWMechanics::DrawState_Nothing);
mGoToJail = false;
MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Dialogue);
diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp
index afad359cfd..84d97ce37c 100644
--- a/apps/openmw/mwworld/worldimp.hpp
+++ b/apps/openmw/mwworld/worldimp.hpp
@@ -15,6 +15,7 @@
#include "timestamp.hpp"
#include "globals.hpp"
#include "contentloader.hpp"
+#include "groundcoverstore.hpp"
namespace osg
{
@@ -80,6 +81,7 @@ namespace MWWorld
std::vector<ESM::ESMReader> mEsm;
MWWorld::ESMStore mStore;
+ GroundcoverStore mGroundcoverStore;
LocalScripts mLocalScripts;
MWWorld::Globals mGlobalVariables;
@@ -163,14 +165,9 @@ namespace MWWorld
void updateSkyDate();
- /**
- * @brief loadContentFiles - Loads content files (esm,esp,omwgame,omwaddon)
- * @param fileCollections- Container which holds content file names and their paths
- * @param content - Container which holds content file names
- * @param contentLoader -
- */
- void loadContentFiles(const Files::Collections& fileCollections,
- const std::vector<std::string>& content, const std::vector<std::string>& groundcover, ContentLoader& contentLoader);
+ void loadContentFiles(const Files::Collections& fileCollections, const std::vector<std::string>& content, ESMStore& store, std::vector<ESM::ESMReader>& readers, ToUTF8::Utf8Encoder* encoder, Loading::Listener* listener);
+
+ void loadGroundcoverFiles(const Files::Collections& fileCollections, const std::vector<std::string>& groundcoverFiles, ToUTF8::Utf8Encoder* encoder);
float feetToGameUnits(float feet);
float getActivationDistancePlusTelekinesis();
@@ -532,13 +529,10 @@ namespace MWWorld
bool isFirstPerson() const override;
bool isPreviewModeEnabled() const override;
- void togglePreviewMode(bool enable) override;
-
bool toggleVanityMode(bool enable) override;
- void allowVanityMode(bool allow) override;
+ MWRender::Camera* getCamera() override;
bool vanityRotateCamera(float * rot) override;
- void adjustCameraDistance(float dist) override;
void applyDeferredPreviewRotationToPlayer(float dt) override;
void disableDeferredPreviewRotation() override;
diff --git a/apps/openmw/options.cpp b/apps/openmw/options.cpp
index d68809468c..bde10755ea 100644
--- a/apps/openmw/options.cpp
+++ b/apps/openmw/options.cpp
@@ -1,7 +1,7 @@
#include "options.hpp"
#include <components/fallback/validate.hpp>
-#include <components/files/escape.hpp>
+#include <components/files/configurationmanager.hpp>
#include <components/misc/rng.hpp>
#include <boost/program_options.hpp>
@@ -9,6 +9,7 @@
namespace
{
namespace bpo = boost::program_options;
+ typedef std::vector<std::string> StringsVector;
}
namespace OpenMW
@@ -21,28 +22,28 @@ namespace OpenMW
("help", "print help message")
("version", "print version information and quit")
- ("replace", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
+ ("replace", bpo::value<StringsVector>()->default_value(StringsVector(), "")
->multitoken()->composing(), "settings where the values from the current source should replace those from lower-priority sources instead of being appended")
- ("data", bpo::value<Files::EscapePathContainer>()->default_value(Files::EscapePathContainer(), "data")
+ ("data", bpo::value<Files::MaybeQuotedPathContainer>()->default_value(Files::MaybeQuotedPathContainer(), "data")
->multitoken()->composing(), "set data directories (later directories have higher priority)")
- ("data-local", bpo::value<Files::EscapePath>()->default_value(Files::EscapePath(), ""),
+ ("data-local", bpo::value<Files::MaybeQuotedPathContainer::value_type>()->default_value(Files::MaybeQuotedPathContainer::value_type(), ""),
"set local data directory (highest priority)")
- ("fallback-archive", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "fallback-archive")
+ ("fallback-archive", bpo::value<StringsVector>()->default_value(StringsVector(), "fallback-archive")
->multitoken()->composing(), "set fallback BSA archives (later archives have higher priority)")
- ("resources", bpo::value<Files::EscapePath>()->default_value(Files::EscapePath(), "resources"),
+ ("resources", bpo::value<Files::MaybeQuotedPath>()->default_value(Files::MaybeQuotedPath(), "resources"),
"set resources directory")
- ("start", bpo::value<Files::EscapeHashString>()->default_value(""),
+ ("start", bpo::value<std::string>()->default_value(""),
"set initial cell")
- ("content", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
+ ("content", bpo::value<StringsVector>()->default_value(StringsVector(), "")
->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon/omwscripts")
- ("groundcover", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
+ ("groundcover", bpo::value<StringsVector>()->default_value(StringsVector(), "")
->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon")
("no-sound", bpo::value<bool>()->implicit_value(true)
@@ -57,7 +58,7 @@ namespace OpenMW
("script-console", bpo::value<bool>()->implicit_value(true)
->default_value(false), "enable console-only script functionality")
- ("script-run", bpo::value<Files::EscapeHashString>()->default_value(""),
+ ("script-run", bpo::value<std::string>()->default_value(""),
"select a file containing a list of console commands that is executed on startup")
("script-warn", bpo::value<int>()->implicit_value (1)
@@ -67,13 +68,13 @@ namespace OpenMW
"\t1 - show warning but consider script as correctly compiled anyway\n"
"\t2 - treat warnings as errors")
- ("script-blacklist", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
+ ("script-blacklist", bpo::value<StringsVector>()->default_value(StringsVector(), "")
->multitoken()->composing(), "ignore the specified script (if the use of the blacklist is enabled)")
("script-blacklist-use", bpo::value<bool>()->implicit_value(true)
->default_value(true), "enable script blacklisting")
- ("load-savegame", bpo::value<Files::EscapePath>()->default_value(Files::EscapePath(), ""),
+ ("load-savegame", bpo::value<Files::MaybeQuotedPath>()->default_value(Files::MaybeQuotedPath(), ""),
"load a save game file on game startup (specify an absolute filename or a filename relative to the current working directory)")
("skip-menu", bpo::value<bool>()->implicit_value(true)
@@ -85,7 +86,7 @@ namespace OpenMW
("fs-strict", bpo::value<bool>()->implicit_value(true)
->default_value(false), "strict file system handling (no case folding)")
- ("encoding", bpo::value<Files::EscapeHashString>()->
+ ("encoding", bpo::value<std::string>()->
default_value("win1252"),
"Character encoding used in OpenMW game messages:\n"
"\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n"
diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt
index bf235331cf..da68a21998 100644
--- a/apps/openmw_test_suite/CMakeLists.txt
+++ b/apps/openmw_test_suite/CMakeLists.txt
@@ -23,9 +23,14 @@ if (GTEST_FOUND AND GMOCK_FOUND)
lua/test_serialization.cpp
lua/test_querypackage.cpp
lua/test_configuration.cpp
+ lua/test_i18n.cpp
+ lua/test_storage.cpp
+
+ lua/test_ui_content.cpp
misc/test_stringops.cpp
misc/test_endianness.cpp
+ misc/test_resourcehelpers.cpp
misc/progressreporter.cpp
misc/compression.cpp
@@ -38,10 +43,14 @@ if (GTEST_FOUND AND GMOCK_FOUND)
detournavigator/recastmeshobject.cpp
detournavigator/navmeshtilescache.cpp
detournavigator/tilecachedrecastmeshmanager.cpp
- detournavigator/serialization/binaryreader.cpp
- detournavigator/serialization/binarywriter.cpp
- detournavigator/serialization/sizeaccumulator.cpp
- detournavigator/serialization/integration.cpp
+ detournavigator/navmeshdb.cpp
+ detournavigator/serialization.cpp
+ detournavigator/asyncnavmeshupdater.cpp
+
+ serialization/binaryreader.cpp
+ serialization/binarywriter.cpp
+ serialization/sizeaccumulator.cpp
+ serialization/integration.cpp
settings/parser.cpp
@@ -59,13 +68,15 @@ if (GTEST_FOUND AND GMOCK_FOUND)
esmloader/load.cpp
esmloader/esmdata.cpp
+
+ files/hash.cpp
)
source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES})
openmw_add_executable(openmw_test_suite openmw_test_suite.cpp ${UNITTEST_SRC_FILES})
- target_link_libraries(openmw_test_suite ${GMOCK_LIBRARIES} components ${LUA_LIBRARIES})
+ target_link_libraries(openmw_test_suite ${GMOCK_LIBRARIES} components)
# Fix for not visible pthreads functions for linker with glibc 2.15
if (UNIX AND NOT APPLE)
target_link_libraries(openmw_test_suite ${CMAKE_THREAD_LIBS_INIT})
diff --git a/apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp b/apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp
new file mode 100644
index 0000000000..b23446cea7
--- /dev/null
+++ b/apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp
@@ -0,0 +1,242 @@
+#include "settings.hpp"
+
+#include <components/detournavigator/asyncnavmeshupdater.hpp>
+#include <components/detournavigator/makenavmesh.hpp>
+#include <components/detournavigator/serialization.hpp>
+#include <components/loadinglistener/loadinglistener.hpp>
+#include <components/detournavigator/navmeshdbutils.hpp>
+#include <components/detournavigator/dbrefgeometryobject.hpp>
+
+#include <DetourNavMesh.h>
+
+#include <gtest/gtest.h>
+
+#include <map>
+
+namespace
+{
+ using namespace testing;
+ using namespace DetourNavigator;
+ using namespace DetourNavigator::Tests;
+
+ void addHeightFieldPlane(TileCachedRecastMeshManager& recastMeshManager)
+ {
+ const osg::Vec2i cellPosition(0, 0);
+ const int cellSize = 8192;
+ recastMeshManager.addHeightfield(cellPosition, cellSize, HeightfieldPlane {0});
+ }
+
+ void addObject(const btBoxShape& shape, TileCachedRecastMeshManager& recastMeshManager)
+ {
+ const ObjectId id(&shape);
+ osg::ref_ptr<Resource::BulletShape> bulletShape(new Resource::BulletShape);
+ bulletShape->mFileName = "test.nif";
+ bulletShape->mFileHash = "test_hash";
+ ObjectTransform objectTransform;
+ std::fill(std::begin(objectTransform.mPosition.pos), std::end(objectTransform.mPosition.pos), 0.1f);
+ std::fill(std::begin(objectTransform.mPosition.rot), std::end(objectTransform.mPosition.rot), 0.2f);
+ objectTransform.mScale = 3.14f;
+ const CollisionShape collisionShape(
+ osg::ref_ptr<Resource::BulletShapeInstance>(new Resource::BulletShapeInstance(bulletShape)),
+ shape, objectTransform
+ );
+ recastMeshManager.addObject(id, collisionShape, btTransform::getIdentity(), AreaType_ground);
+ }
+
+ struct DetourNavigatorAsyncNavMeshUpdaterTest : Test
+ {
+ Settings mSettings = makeSettings();
+ TileCachedRecastMeshManager mRecastMeshManager {mSettings.mRecast};
+ OffMeshConnectionsManager mOffMeshConnectionsManager {mSettings.mRecast};
+ const osg::Vec3f mAgentHalfExtents {29, 29, 66};
+ const TilePosition mPlayerTile {0, 0};
+ const std::string mWorldspace = "sys::default";
+ const btBoxShape mBox {btVector3(100, 100, 20)};
+ Loading::Listener mListener;
+ };
+
+ TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, for_all_jobs_done_when_empty_wait_should_terminate)
+ {
+ AsyncNavMeshUpdater updater {mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr};
+ updater.wait(mListener, WaitConditionType::allJobsDone);
+ }
+
+ TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, for_required_tiles_present_when_empty_wait_should_terminate)
+ {
+ AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr);
+ updater.wait(mListener, WaitConditionType::requiredTilesPresent);
+ }
+
+ TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_should_generate_navmesh_tile)
+ {
+ mRecastMeshManager.setWorldspace(mWorldspace);
+ addHeightFieldPlane(mRecastMeshManager);
+ AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr);
+ const auto navMeshCacheItem = std::make_shared<GuardedNavMeshCacheItem>(makeEmptyNavMesh(mSettings), 1);
+ const std::map<TilePosition, ChangeType> changedTiles {{TilePosition {0, 0}, ChangeType::add}};
+ updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
+ updater.wait(mListener, WaitConditionType::allJobsDone);
+ EXPECT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0);
+ }
+
+ TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, repeated_post_should_lead_to_cache_hit)
+ {
+ mRecastMeshManager.setWorldspace(mWorldspace);
+ addHeightFieldPlane(mRecastMeshManager);
+ AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr);
+ const auto navMeshCacheItem = std::make_shared<GuardedNavMeshCacheItem>(makeEmptyNavMesh(mSettings), 1);
+ const std::map<TilePosition, ChangeType> changedTiles {{TilePosition {0, 0}, ChangeType::add}};
+ updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
+ updater.wait(mListener, WaitConditionType::allJobsDone);
+ {
+ const auto stats = updater.getStats();
+ ASSERT_EQ(stats.mCache.mGetCount, 1);
+ ASSERT_EQ(stats.mCache.mHitCount, 0);
+ }
+ updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
+ updater.wait(mListener, WaitConditionType::allJobsDone);
+ {
+ const auto stats = updater.getStats();
+ EXPECT_EQ(stats.mCache.mGetCount, 2);
+ EXPECT_EQ(stats.mCache.mHitCount, 1);
+ }
+ }
+
+ TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_for_update_change_type_should_not_update_cache)
+ {
+ mRecastMeshManager.setWorldspace(mWorldspace);
+ addHeightFieldPlane(mRecastMeshManager);
+ AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr);
+ const auto navMeshCacheItem = std::make_shared<GuardedNavMeshCacheItem>(makeEmptyNavMesh(mSettings), 1);
+ const std::map<TilePosition, ChangeType> changedTiles {{TilePosition {0, 0}, ChangeType::update}};
+ updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
+ updater.wait(mListener, WaitConditionType::allJobsDone);
+ {
+ const auto stats = updater.getStats();
+ ASSERT_EQ(stats.mCache.mGetCount, 1);
+ ASSERT_EQ(stats.mCache.mHitCount, 0);
+ }
+ updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
+ updater.wait(mListener, WaitConditionType::allJobsDone);
+ {
+ const auto stats = updater.getStats();
+ EXPECT_EQ(stats.mCache.mGetCount, 2);
+ EXPECT_EQ(stats.mCache.mHitCount, 0);
+ }
+ }
+
+ TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_should_write_generated_tile_to_db)
+ {
+ mRecastMeshManager.setWorldspace(mWorldspace);
+ addHeightFieldPlane(mRecastMeshManager);
+ addObject(mBox, mRecastMeshManager);
+ auto db = std::make_unique<NavMeshDb>(":memory:");
+ NavMeshDb* const dbPtr = db.get();
+ AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db));
+ const auto navMeshCacheItem = std::make_shared<GuardedNavMeshCacheItem>(makeEmptyNavMesh(mSettings), 1);
+ const TilePosition tilePosition {0, 0};
+ const std::map<TilePosition, ChangeType> changedTiles {{tilePosition, ChangeType::add}};
+ updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
+ updater.wait(mListener, WaitConditionType::allJobsDone);
+ const auto recastMesh = mRecastMeshManager.getMesh(mWorldspace, tilePosition);
+ ASSERT_NE(recastMesh, nullptr);
+ ShapeId nextShapeId {1};
+ const std::vector<DbRefGeometryObject> objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(),
+ [&] (const MeshSource& v) { return resolveMeshSource(*dbPtr, v, nextShapeId); });
+ const auto tile = dbPtr->findTile(mWorldspace, tilePosition, serialize(mSettings.mRecast, *recastMesh, objects));
+ ASSERT_TRUE(tile.has_value());
+ EXPECT_EQ(tile->mTileId, 1);
+ EXPECT_EQ(tile->mVersion, mSettings.mNavMeshVersion);
+ }
+
+ TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_when_writing_to_db_disabled_should_not_write_tiles)
+ {
+ mRecastMeshManager.setWorldspace(mWorldspace);
+ addHeightFieldPlane(mRecastMeshManager);
+ addObject(mBox, mRecastMeshManager);
+ auto db = std::make_unique<NavMeshDb>(":memory:");
+ NavMeshDb* const dbPtr = db.get();
+ mSettings.mWriteToNavMeshDb = false;
+ AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db));
+ const auto navMeshCacheItem = std::make_shared<GuardedNavMeshCacheItem>(makeEmptyNavMesh(mSettings), 1);
+ const TilePosition tilePosition {0, 0};
+ const std::map<TilePosition, ChangeType> changedTiles {{tilePosition, ChangeType::add}};
+ updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
+ updater.wait(mListener, WaitConditionType::allJobsDone);
+ const auto recastMesh = mRecastMeshManager.getMesh(mWorldspace, tilePosition);
+ ASSERT_NE(recastMesh, nullptr);
+ ShapeId nextShapeId {1};
+ const std::vector<DbRefGeometryObject> objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(),
+ [&] (const MeshSource& v) { return resolveMeshSource(*dbPtr, v, nextShapeId); });
+ const auto tile = dbPtr->findTile(mWorldspace, tilePosition, serialize(mSettings.mRecast, *recastMesh, objects));
+ ASSERT_FALSE(tile.has_value());
+ }
+
+ TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_when_writing_to_db_disabled_should_not_write_shapes)
+ {
+ mRecastMeshManager.setWorldspace(mWorldspace);
+ addHeightFieldPlane(mRecastMeshManager);
+ addObject(mBox, mRecastMeshManager);
+ auto db = std::make_unique<NavMeshDb>(":memory:");
+ NavMeshDb* const dbPtr = db.get();
+ mSettings.mWriteToNavMeshDb = false;
+ AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db));
+ const auto navMeshCacheItem = std::make_shared<GuardedNavMeshCacheItem>(makeEmptyNavMesh(mSettings), 1);
+ const TilePosition tilePosition {0, 0};
+ const std::map<TilePosition, ChangeType> changedTiles {{tilePosition, ChangeType::add}};
+ updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
+ updater.wait(mListener, WaitConditionType::allJobsDone);
+ const auto recastMesh = mRecastMeshManager.getMesh(mWorldspace, tilePosition);
+ ASSERT_NE(recastMesh, nullptr);
+ const auto objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(),
+ [&] (const MeshSource& v) { return resolveMeshSource(*dbPtr, v); });
+ EXPECT_FALSE(objects.has_value());
+ }
+
+ TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_should_read_from_db_on_cache_miss)
+ {
+ mRecastMeshManager.setWorldspace(mWorldspace);
+ addHeightFieldPlane(mRecastMeshManager);
+ mSettings.mMaxNavMeshTilesCacheSize = 0;
+ AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::make_unique<NavMeshDb>(":memory:"));
+ const auto navMeshCacheItem = std::make_shared<GuardedNavMeshCacheItem>(makeEmptyNavMesh(mSettings), 1);
+ const std::map<TilePosition, ChangeType> changedTiles {{TilePosition {0, 0}, ChangeType::add}};
+ updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
+ updater.wait(mListener, WaitConditionType::allJobsDone);
+ {
+ const auto stats = updater.getStats();
+ ASSERT_EQ(stats.mCache.mGetCount, 1);
+ ASSERT_EQ(stats.mCache.mHitCount, 0);
+ ASSERT_TRUE(stats.mDb.has_value());
+ ASSERT_EQ(stats.mDb->mGetTileCount, 1);
+ ASSERT_EQ(stats.mDbGetTileHits, 0);
+ }
+ updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
+ updater.wait(mListener, WaitConditionType::allJobsDone);
+ {
+ const auto stats = updater.getStats();
+ EXPECT_EQ(stats.mCache.mGetCount, 2);
+ EXPECT_EQ(stats.mCache.mHitCount, 0);
+ ASSERT_TRUE(stats.mDb.has_value());
+ EXPECT_EQ(stats.mDb->mGetTileCount, 2);
+ EXPECT_EQ(stats.mDbGetTileHits, 1);
+ }
+ }
+
+ TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, on_changing_player_tile_post_should_remove_tiles_out_of_range)
+ {
+ mRecastMeshManager.setWorldspace(mWorldspace);
+ addHeightFieldPlane(mRecastMeshManager);
+ AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr);
+ const auto navMeshCacheItem = std::make_shared<GuardedNavMeshCacheItem>(makeEmptyNavMesh(mSettings), 1);
+ const std::map<TilePosition, ChangeType> changedTilesAdd {{TilePosition {0, 0}, ChangeType::add}};
+ updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTilesAdd);
+ updater.wait(mListener, WaitConditionType::allJobsDone);
+ ASSERT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0);
+ const std::map<TilePosition, ChangeType> changedTilesRemove {{TilePosition {0, 0}, ChangeType::remove}};
+ const TilePosition playerTile(100, 100);
+ updater.post(mAgentHalfExtents, navMeshCacheItem, playerTile, mWorldspace, changedTilesRemove);
+ updater.wait(mListener, WaitConditionType::allJobsDone);
+ EXPECT_EQ(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0);
+ }
+}
diff --git a/apps/openmw_test_suite/detournavigator/generate.hpp b/apps/openmw_test_suite/detournavigator/generate.hpp
new file mode 100644
index 0000000000..52d04495a7
--- /dev/null
+++ b/apps/openmw_test_suite/detournavigator/generate.hpp
@@ -0,0 +1,51 @@
+#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_GENERATE_H
+#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_GENERATE_H
+
+#include <algorithm>
+#include <numeric>
+#include <random>
+#include <type_traits>
+
+namespace DetourNavigator
+{
+ namespace Tests
+ {
+ template <class T, class Random>
+ inline auto generateValue(T& value, Random& random)
+ -> std::enable_if_t<sizeof(T) >= 2>
+ {
+ using Distribution = std::conditional_t<
+ std::is_floating_point_v<T>,
+ std::uniform_real_distribution<T>,
+ std::uniform_int_distribution<T>
+ >;
+ Distribution distribution(std::numeric_limits<T>::min(), std::numeric_limits<T>::max());
+ value = distribution(random);
+ }
+
+ template <class T, class Random>
+ inline auto generateValue(T& value, Random& random)
+ -> std::enable_if_t<sizeof(T) == 1>
+ {
+ unsigned short v;
+ generateValue(v, random);
+ value = static_cast<T>(v % 256);
+ }
+
+ template <class Random>
+ inline void generateValue(unsigned char& value, Random& random)
+ {
+ unsigned short v;
+ generateValue(v, random);
+ value = static_cast<unsigned char>(v % 256);
+ }
+
+ template <class I, class Random>
+ inline void generateRange(I begin, I end, Random& random)
+ {
+ std::for_each(begin, end, [&] (auto& v) { generateValue(v, random); });
+ }
+ }
+}
+
+#endif
diff --git a/apps/openmw_test_suite/detournavigator/gettilespositions.cpp b/apps/openmw_test_suite/detournavigator/gettilespositions.cpp
index 1ad5c063d0..ced33a99f0 100644
--- a/apps/openmw_test_suite/detournavigator/gettilespositions.cpp
+++ b/apps/openmw_test_suite/detournavigator/gettilespositions.cpp
@@ -21,7 +21,7 @@ namespace
struct DetourNavigatorGetTilesPositionsTest : Test
{
- Settings mSettings;
+ RecastSettings mSettings;
std::vector<TilePosition> mTilesPositions;
CollectTilesPositions mCollect {mTilesPositions};
diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp
index d4bdcb13b8..e7cd0a7db0 100644
--- a/apps/openmw_test_suite/detournavigator/navigator.cpp
+++ b/apps/openmw_test_suite/detournavigator/navigator.cpp
@@ -1,11 +1,15 @@
#include "operators.hpp"
+#include "settings.hpp"
#include <components/detournavigator/navigatorimpl.hpp>
#include <components/detournavigator/exceptions.hpp>
+#include <components/detournavigator/navigatorutils.hpp>
+#include <components/detournavigator/navmeshdb.hpp>
#include <components/misc/rng.hpp>
#include <components/loadinglistener/loadinglistener.hpp>
#include <components/esm/loadland.hpp>
#include <components/resource/bulletshape.hpp>
+#include <components/bullethelpers/heightfield.hpp>
#include <osg/ref_ptr>
@@ -29,13 +33,15 @@ namespace
{
using namespace testing;
using namespace DetourNavigator;
+ using namespace DetourNavigator::Tests;
struct DetourNavigatorNavigatorTest : Test
{
- Settings mSettings;
+ Settings mSettings = makeSettings();
std::unique_ptr<Navigator> mNavigator;
- osg::Vec3f mPlayerPosition;
- osg::Vec3f mAgentHalfExtents;
+ const osg::Vec3f mPlayerPosition;
+ const std::string mWorldspace;
+ const osg::Vec3f mAgentHalfExtents;
osg::Vec3f mStart;
osg::Vec3f mEnd;
std::deque<osg::Vec3f> mPath;
@@ -45,46 +51,20 @@ namespace
Loading::Listener mListener;
const osg::Vec2i mCellPosition {0, 0};
const int mHeightfieldTileSize = ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1);
- const osg::Vec3f mShift {0, 0, 0};
const float mEndTolerance = 0;
+ const btTransform mTransform {btMatrix3x3::getIdentity(), btVector3(256, 256, 0)};
+ const ObjectTransform mObjectTransform {ESM::Position {{256, 256, 0}, {0, 0, 0}}, 0.0f};
DetourNavigatorNavigatorTest()
- : mPlayerPosition(0, 0, 0)
+ : mPlayerPosition(256, 256, 0)
+ , mWorldspace("sys::default")
, mAgentHalfExtents(29, 29, 66)
- , mStart(-204, 204, 1)
- , mEnd(204, -204, 1)
+ , mStart(52, 460, 1)
+ , mEnd(460, 52, 1)
, mOut(mPath)
, mStepSize(28.333332061767578125f)
{
- mSettings.mEnableWriteRecastMeshToFile = false;
- mSettings.mEnableWriteNavMeshToFile = false;
- mSettings.mEnableRecastMeshFileNameRevision = false;
- mSettings.mEnableNavMeshFileNameRevision = false;
- mSettings.mBorderSize = 16;
- mSettings.mCellHeight = 0.2f;
- mSettings.mCellSize = 0.2f;
- mSettings.mDetailSampleDist = 6;
- mSettings.mDetailSampleMaxError = 1;
- mSettings.mMaxClimb = 34;
- mSettings.mMaxSimplificationError = 1.3f;
- mSettings.mMaxSlope = 49;
- mSettings.mRecastScaleFactor = 0.017647058823529415f;
- mSettings.mSwimHeightScale = 0.89999997615814208984375f;
- mSettings.mMaxEdgeLen = 12;
- mSettings.mMaxNavMeshQueryNodes = 2048;
- mSettings.mMaxVertsPerPoly = 6;
- mSettings.mRegionMergeSize = 20;
- mSettings.mRegionMinSize = 8;
- mSettings.mTileSize = 64;
- mSettings.mWaitUntilMinDistanceToPlayer = std::numeric_limits<int>::max();
- mSettings.mAsyncNavMeshUpdaterThreads = 1;
- mSettings.mMaxNavMeshTilesCacheSize = 1024 * 1024;
- mSettings.mMaxPolygonPathSize = 1024;
- mSettings.mMaxSmoothPathSize = 1024;
- mSettings.mMaxPolys = 4096;
- mSettings.mMaxTilesNumber = 512;
- mSettings.mMinUpdateInterval = std::chrono::milliseconds(50);
- mNavigator.reset(new NavigatorImpl(mSettings));
+ mNavigator.reset(new NavigatorImpl(mSettings, std::make_unique<NavMeshDb>(":memory:")));
}
};
@@ -134,9 +114,14 @@ namespace
osg::ref_ptr<const Resource::BulletShapeInstance> mInstance;
};
+ btVector3 getHeightfieldShift(const osg::Vec2i& cellPosition, int cellSize, float minHeight, float maxHeight)
+ {
+ return BulletHelpers::getHeightfieldShift(cellPosition.x(), cellPosition.x(), cellSize, minHeight, maxHeight);
+ }
+
TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_return_empty)
{
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::NavMeshNotFound);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>());
}
@@ -144,7 +129,7 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception)
{
mNavigator->addAgent(mAgentHalfExtents);
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::StartPolygonNotFound);
}
@@ -153,7 +138,7 @@ namespace
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->removeAgent(mAgentHalfExtents);
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::StartPolygonNotFound);
}
@@ -167,38 +152,39 @@ namespace
0, -25, -100, -100, -100,
}};
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
+ const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::requiredTilesPresent);
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(-204.0000152587890625, 204, 1.99998295307159423828125),
- Vec3fEq(-183.96533203125, 183.9653167724609375, 1.99998819828033447265625),
- Vec3fEq(-163.930633544921875, 163.9306182861328125, 1.99999344348907470703125),
- Vec3fEq(-143.8959503173828125, 143.89593505859375, -2.720611572265625),
- Vec3fEq(-123.86126708984375, 123.86124420166015625, -13.1089687347412109375),
- Vec3fEq(-103.82657623291015625, 103.8265533447265625, -23.497333526611328125),
- Vec3fEq(-83.7918853759765625, 83.7918548583984375, -33.885692596435546875),
- Vec3fEq(-63.757190704345703125, 63.757171630859375, -44.274051666259765625),
- Vec3fEq(-43.722503662109375, 43.72248077392578125, -54.66241455078125),
- Vec3fEq(-23.687808990478515625, 23.6877918243408203125, -65.05077362060546875),
- Vec3fEq(-3.6531188488006591796875, 3.6531002521514892578125, -75.43914031982421875),
- Vec3fEq(16.3815746307373046875, -16.381591796875, -69.74927520751953125),
- Vec3fEq(36.416263580322265625, -36.416286468505859375, -60.4739532470703125),
- Vec3fEq(56.450958251953125, -56.450977325439453125, -51.1986236572265625),
- Vec3fEq(76.48564910888671875, -76.4856719970703125, -41.92330169677734375),
- Vec3fEq(96.5203399658203125, -96.52036285400390625, -31.46941375732421875),
- Vec3fEq(116.55503082275390625, -116.5550537109375, -19.597003936767578125),
- Vec3fEq(136.5897216796875, -136.5897369384765625, -7.724592685699462890625),
- Vec3fEq(156.624420166015625, -156.624420166015625, 1.99999535083770751953125),
- Vec3fEq(176.6591033935546875, -176.65911865234375, 1.99999010562896728515625),
- Vec3fEq(196.69378662109375, -196.6938018798828125, 1.99998486042022705078125),
- Vec3fEq(204, -204.0000152587890625, 1.99998295307159423828125)
+ Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125),
+ Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375),
+ Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875),
+ Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125),
+ Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875),
+ Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875),
+ Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375),
+ Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125),
+ Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875),
+ Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375),
+ Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125),
+ Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625),
+ Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875),
+ Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125),
+ Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375),
+ Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375),
+ Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875),
+ Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625),
+ Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625),
+ Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125),
+ Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875),
+ Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125)
)) << mPath;
}
@@ -212,76 +198,77 @@ namespace
0, -25, -100, -100, -100,
}};
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
+ const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
CollisionShapeInstance compound(std::make_unique<btCompoundShape>());
compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), new btBoxShape(btVector3(20, 20, 100)));
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(-204, 204, 1.99998295307159423828125),
- Vec3fEq(-183.965301513671875, 183.965301513671875, 1.99998819828033447265625),
- Vec3fEq(-163.9306182861328125, 163.9306182861328125, 1.99999344348907470703125),
- Vec3fEq(-143.89593505859375, 143.89593505859375, -2.7206256389617919921875),
- Vec3fEq(-123.86124420166015625, 123.86124420166015625, -13.1089839935302734375),
- Vec3fEq(-103.8265533447265625, 103.8265533447265625, -23.4973468780517578125),
- Vec3fEq(-83.7918548583984375, 83.7918548583984375, -33.885707855224609375),
- Vec3fEq(-63.75716400146484375, 63.75716400146484375, -44.27407073974609375),
- Vec3fEq(-43.72247314453125, 43.72247314453125, -54.662433624267578125),
- Vec3fEq(-23.6877803802490234375, 23.6877803802490234375, -65.0507965087890625),
- Vec3fEq(-3.653090000152587890625, 3.653090000152587890625, -75.43915557861328125),
- Vec3fEq(16.3816013336181640625, -16.3816013336181640625, -69.749267578125),
- Vec3fEq(36.416290283203125, -36.416290283203125, -60.4739532470703125),
- Vec3fEq(56.450984954833984375, -56.450984954833984375, -51.1986236572265625),
- Vec3fEq(76.4856719970703125, -76.4856719970703125, -41.92330169677734375),
- Vec3fEq(96.52036285400390625, -96.52036285400390625, -31.46941375732421875),
- Vec3fEq(116.5550537109375, -116.5550537109375, -19.597003936767578125),
- Vec3fEq(136.5897369384765625, -136.5897369384765625, -7.724592685699462890625),
- Vec3fEq(156.6244354248046875, -156.6244354248046875, 1.99999535083770751953125),
- Vec3fEq(176.6591339111328125, -176.6591339111328125, 1.99999010562896728515625),
- Vec3fEq(196.693817138671875, -196.693817138671875, 1.99998486042022705078125),
- Vec3fEq(204, -204, 1.99998295307159423828125)
+ Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125),
+ Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375),
+ Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875),
+ Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125),
+ Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875),
+ Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875),
+ Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375),
+ Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125),
+ Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875),
+ Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375),
+ Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125),
+ Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625),
+ Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875),
+ Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125),
+ Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375),
+ Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375),
+ Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875),
+ Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625),
+ Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625),
+ Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125),
+ Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875),
+ Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125)
)) << mPath;
- mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), btTransform::getIdentity());
+ mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), mTransform);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
mPath.clear();
mOut = std::back_inserter(mPath);
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(-204, 204, 1.99998295307159423828125),
- Vec3fEq(-189.9427337646484375, 179.3997802734375, -3.622931003570556640625),
- Vec3fEq(-175.8854522705078125, 154.7995452880859375, -9.24583911895751953125),
- Vec3fEq(-161.82818603515625, 130.1993255615234375, -14.86874866485595703125),
- Vec3fEq(-147.770904541015625, 105.5991058349609375, -20.4916591644287109375),
- Vec3fEq(-133.7136383056640625, 80.99887847900390625, -26.1145648956298828125),
- Vec3fEq(-119.65636444091796875, 56.39865875244140625, -31.7374725341796875),
- Vec3fEq(-105.59909820556640625, 31.798435211181640625, -26.133396148681640625),
- Vec3fEq(-91.54183197021484375, 7.1982135772705078125, -31.5624217987060546875),
- Vec3fEq(-77.48455810546875, -17.402008056640625, -26.98972320556640625),
- Vec3fEq(-63.427295684814453125, -42.00223541259765625, -19.9045581817626953125),
- Vec3fEq(-42.193531036376953125, -60.761363983154296875, -20.4544773101806640625),
- Vec3fEq(-20.9597682952880859375, -79.5204925537109375, -23.599918365478515625),
- Vec3fEq(3.8312885761260986328125, -93.2384033203125, -30.7141361236572265625),
- Vec3fEq(28.6223468780517578125, -106.95632171630859375, -24.8243885040283203125),
- Vec3fEq(53.413402557373046875, -120.6742401123046875, -31.3303241729736328125),
- Vec3fEq(78.20446014404296875, -134.39215087890625, -25.8431549072265625),
- Vec3fEq(102.99552154541015625, -148.110076904296875, -20.3559894561767578125),
- Vec3fEq(127.7865753173828125, -161.827972412109375, -14.868824005126953125),
- Vec3fEq(152.57763671875, -175.5458984375, -9.3816623687744140625),
- Vec3fEq(177.3686981201171875, -189.2638092041015625, -3.894496917724609375),
- Vec3fEq(202.1597442626953125, -202.9817047119140625, 1.59266507625579833984375),
- Vec3fEq(204, -204, 1.99998295307159423828125)
+ Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125),
+ Vec3fEq(69.5299530029296875, 434.754913330078125, -2.6775772571563720703125),
+ Vec3fEq(82.39324951171875, 409.50982666015625, -7.355137348175048828125),
+ Vec3fEq(95.25653839111328125, 384.2647705078125, -12.0326976776123046875),
+ Vec3fEq(108.11983489990234375, 359.019683837890625, -16.71025848388671875),
+ Vec3fEq(120.983123779296875, 333.774627685546875, -21.3878192901611328125),
+ Vec3fEq(133.8464202880859375, 308.529541015625, -26.0653781890869140625),
+ Vec3fEq(146.7097015380859375, 283.284454345703125, -30.7429370880126953125),
+ Vec3fEq(159.572998046875, 258.039398193359375, -35.420497894287109375),
+ Vec3fEq(172.4362945556640625, 232.7943115234375, -27.2731761932373046875),
+ Vec3fEq(185.2996063232421875, 207.54925537109375, -19.575878143310546875),
+ Vec3fEq(206.6449737548828125, 188.917236328125, -20.3511219024658203125),
+ Vec3fEq(227.9903564453125, 170.28521728515625, -22.9776935577392578125),
+ Vec3fEq(253.4362640380859375, 157.8239593505859375, -31.1692962646484375),
+ Vec3fEq(278.8822021484375, 145.3627166748046875, -30.253124237060546875),
+ Vec3fEq(304.328094482421875, 132.9014739990234375, -22.219127655029296875),
+ Vec3fEq(329.774017333984375, 120.44022369384765625, -13.2701435089111328125),
+ Vec3fEq(355.219940185546875, 107.97898101806640625, -5.330339908599853515625),
+ Vec3fEq(380.665863037109375, 95.51773834228515625, -3.5501649379730224609375),
+ Vec3fEq(406.111785888671875, 83.05649566650390625, -1.76998889446258544921875),
+ Vec3fEq(431.557708740234375, 70.5952606201171875, 0.01018683053553104400634765625),
+ Vec3fEq(457.003662109375, 58.134021759033203125, 1.79036080837249755859375),
+ Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125)
)) << mPath;
}
@@ -295,79 +282,80 @@ namespace
0, -25, -100, -100, -100,
}};
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
+ const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
CollisionShapeInstance compound(std::make_unique<btCompoundShape>());
compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), new btBoxShape(btVector3(20, 20, 100)));
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
- mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), btTransform::getIdentity());
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
+ mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), mTransform);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(-204, 204, 1.99998295307159423828125),
- Vec3fEq(-189.9427337646484375, 179.3997802734375, -3.622931003570556640625),
- Vec3fEq(-175.8854522705078125, 154.7995452880859375, -9.24583911895751953125),
- Vec3fEq(-161.82818603515625, 130.1993255615234375, -14.86874866485595703125),
- Vec3fEq(-147.770904541015625, 105.5991058349609375, -20.4916591644287109375),
- Vec3fEq(-133.7136383056640625, 80.99887847900390625, -26.1145648956298828125),
- Vec3fEq(-119.65636444091796875, 56.39865875244140625, -31.7374725341796875),
- Vec3fEq(-105.59909820556640625, 31.798435211181640625, -26.133396148681640625),
- Vec3fEq(-91.54183197021484375, 7.1982135772705078125, -31.5624217987060546875),
- Vec3fEq(-77.48455810546875, -17.402008056640625, -26.98972320556640625),
- Vec3fEq(-63.427295684814453125, -42.00223541259765625, -19.9045581817626953125),
- Vec3fEq(-42.193531036376953125, -60.761363983154296875, -20.4544773101806640625),
- Vec3fEq(-20.9597682952880859375, -79.5204925537109375, -23.599918365478515625),
- Vec3fEq(3.8312885761260986328125, -93.2384033203125, -30.7141361236572265625),
- Vec3fEq(28.6223468780517578125, -106.95632171630859375, -24.8243885040283203125),
- Vec3fEq(53.413402557373046875, -120.6742401123046875, -31.3303241729736328125),
- Vec3fEq(78.20446014404296875, -134.39215087890625, -25.8431549072265625),
- Vec3fEq(102.99552154541015625, -148.110076904296875, -20.3559894561767578125),
- Vec3fEq(127.7865753173828125, -161.827972412109375, -14.868824005126953125),
- Vec3fEq(152.57763671875, -175.5458984375, -9.3816623687744140625),
- Vec3fEq(177.3686981201171875, -189.2638092041015625, -3.894496917724609375),
- Vec3fEq(202.1597442626953125, -202.9817047119140625, 1.59266507625579833984375),
- Vec3fEq(204, -204, 1.99998295307159423828125)
+ Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125),
+ Vec3fEq(69.5299530029296875, 434.754913330078125, -2.6775772571563720703125),
+ Vec3fEq(82.39324951171875, 409.50982666015625, -7.355137348175048828125),
+ Vec3fEq(95.25653839111328125, 384.2647705078125, -12.0326976776123046875),
+ Vec3fEq(108.11983489990234375, 359.019683837890625, -16.71025848388671875),
+ Vec3fEq(120.983123779296875, 333.774627685546875, -21.3878192901611328125),
+ Vec3fEq(133.8464202880859375, 308.529541015625, -26.0653781890869140625),
+ Vec3fEq(146.7097015380859375, 283.284454345703125, -30.7429370880126953125),
+ Vec3fEq(159.572998046875, 258.039398193359375, -35.420497894287109375),
+ Vec3fEq(172.4362945556640625, 232.7943115234375, -27.2731761932373046875),
+ Vec3fEq(185.2996063232421875, 207.54925537109375, -19.575878143310546875),
+ Vec3fEq(206.6449737548828125, 188.917236328125, -20.3511219024658203125),
+ Vec3fEq(227.9903564453125, 170.28521728515625, -22.9776935577392578125),
+ Vec3fEq(253.4362640380859375, 157.8239593505859375, -31.1692962646484375),
+ Vec3fEq(278.8822021484375, 145.3627166748046875, -30.253124237060546875),
+ Vec3fEq(304.328094482421875, 132.9014739990234375, -22.219127655029296875),
+ Vec3fEq(329.774017333984375, 120.44022369384765625, -13.2701435089111328125),
+ Vec3fEq(355.219940185546875, 107.97898101806640625, -5.330339908599853515625),
+ Vec3fEq(380.665863037109375, 95.51773834228515625, -3.5501649379730224609375),
+ Vec3fEq(406.111785888671875, 83.05649566650390625, -1.76998889446258544921875),
+ Vec3fEq(431.557708740234375, 70.5952606201171875, 0.01018683053553104400634765625),
+ Vec3fEq(457.003662109375, 58.134021759033203125, 1.79036080837249755859375),
+ Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125)
)) << mPath;
compound.shape().updateChildTransform(0, btTransform(btMatrix3x3::getIdentity(), btVector3(1000, 0, 0)));
- mNavigator->updateObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), btTransform::getIdentity());
+ mNavigator->updateObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), mTransform);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
mPath.clear();
mOut = std::back_inserter(mPath);
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(-204, 204, 1.99998295307159423828125),
- Vec3fEq(-183.965301513671875, 183.965301513671875, 1.99998819828033447265625),
- Vec3fEq(-163.9306182861328125, 163.9306182861328125, 1.99999344348907470703125),
- Vec3fEq(-143.89593505859375, 143.89593505859375, -2.7206256389617919921875),
- Vec3fEq(-123.86124420166015625, 123.86124420166015625, -13.1089839935302734375),
- Vec3fEq(-103.8265533447265625, 103.8265533447265625, -23.4973468780517578125),
- Vec3fEq(-83.7918548583984375, 83.7918548583984375, -33.885707855224609375),
- Vec3fEq(-63.75716400146484375, 63.75716400146484375, -44.27407073974609375),
- Vec3fEq(-43.72247314453125, 43.72247314453125, -54.662433624267578125),
- Vec3fEq(-23.6877803802490234375, 23.6877803802490234375, -65.0507965087890625),
- Vec3fEq(-3.653090000152587890625, 3.653090000152587890625, -75.43915557861328125),
- Vec3fEq(16.3816013336181640625, -16.3816013336181640625, -69.749267578125),
- Vec3fEq(36.416290283203125, -36.416290283203125, -60.4739532470703125),
- Vec3fEq(56.450984954833984375, -56.450984954833984375, -51.1986236572265625),
- Vec3fEq(76.4856719970703125, -76.4856719970703125, -41.92330169677734375),
- Vec3fEq(96.52036285400390625, -96.52036285400390625, -31.46941375732421875),
- Vec3fEq(116.5550537109375, -116.5550537109375, -19.597003936767578125),
- Vec3fEq(136.5897369384765625, -136.5897369384765625, -7.724592685699462890625),
- Vec3fEq(156.6244354248046875, -156.6244354248046875, 1.99999535083770751953125),
- Vec3fEq(176.6591339111328125, -176.6591339111328125, 1.99999010562896728515625),
- Vec3fEq(196.693817138671875, -196.693817138671875, 1.99998486042022705078125),
- Vec3fEq(204, -204, 1.99998295307159423828125)
+ Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125),
+ Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375),
+ Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875),
+ Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125),
+ Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875),
+ Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875),
+ Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375),
+ Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125),
+ Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875),
+ Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375),
+ Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125),
+ Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625),
+ Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875),
+ Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125),
+ Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375),
+ Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375),
+ Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875),
+ Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625),
+ Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625),
+ Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125),
+ Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875),
+ Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125)
)) << mPath;
}
@@ -394,37 +382,37 @@ namespace
heightfield2.shape().setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addObject(ObjectId(&heightfield1.shape()), ObjectShapes(heightfield1.instance()), btTransform::getIdentity());
- mNavigator->addObject(ObjectId(&heightfield2.shape()), ObjectShapes(heightfield2.instance()), btTransform::getIdentity());
+ mNavigator->addObject(ObjectId(&heightfield1.shape()), ObjectShapes(heightfield1.instance(), mObjectTransform), mTransform);
+ mNavigator->addObject(ObjectId(&heightfield2.shape()), ObjectShapes(heightfield2.instance(), mObjectTransform), mTransform);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(-204, 204, 1.999981403350830078125),
- Vec3fEq(-183.965301513671875, 183.965301513671875, -0.428465187549591064453125),
- Vec3fEq(-163.9306182861328125, 163.9306182861328125, -2.8569104671478271484375),
- Vec3fEq(-143.89593505859375, 143.89593505859375, -5.28535556793212890625),
- Vec3fEq(-123.86124420166015625, 123.86124420166015625, -7.7138004302978515625),
- Vec3fEq(-103.8265533447265625, 103.8265533447265625, -10.142246246337890625),
- Vec3fEq(-83.7918548583984375, 83.7918548583984375, -12.3704509735107421875),
- Vec3fEq(-63.75716400146484375, 63.75716400146484375, -14.354084014892578125),
- Vec3fEq(-43.72247314453125, 43.72247314453125, -16.3377170562744140625),
- Vec3fEq(-23.6877803802490234375, 23.6877803802490234375, -18.32135009765625),
- Vec3fEq(-3.653090000152587890625, 3.653090000152587890625, -20.3049831390380859375),
- Vec3fEq(16.3816013336181640625, -16.3816013336181640625, -19.044734954833984375),
- Vec3fEq(36.416290283203125, -36.416290283203125, -17.061100006103515625),
- Vec3fEq(56.450984954833984375, -56.450984954833984375, -15.0774688720703125),
- Vec3fEq(76.4856719970703125, -76.4856719970703125, -13.0938358306884765625),
- Vec3fEq(96.52036285400390625, -96.52036285400390625, -11.02784252166748046875),
- Vec3fEq(116.5550537109375, -116.5550537109375, -8.5993976593017578125),
- Vec3fEq(136.5897369384765625, -136.5897369384765625, -6.170953273773193359375),
- Vec3fEq(156.6244354248046875, -156.6244354248046875, -3.74250507354736328125),
- Vec3fEq(176.6591339111328125, -176.6591339111328125, -1.314060688018798828125),
- Vec3fEq(196.693817138671875, -196.693817138671875, 1.1143856048583984375),
- Vec3fEq(204, -204, 1.9999811649322509765625)
+ Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125),
+ Vec3fEq(76.70135498046875, 439.965301513671875, -0.903246104717254638671875),
+ Vec3fEq(96.73604583740234375, 419.93060302734375, -3.8064472675323486328125),
+ Vec3fEq(116.770751953125, 399.89593505859375, -6.709649562835693359375),
+ Vec3fEq(136.8054351806640625, 379.861236572265625, -9.33333873748779296875),
+ Vec3fEq(156.840118408203125, 359.826568603515625, -9.33333873748779296875),
+ Vec3fEq(176.8748016357421875, 339.7918701171875, -9.33333873748779296875),
+ Vec3fEq(196.90948486328125, 319.757171630859375, -9.33333873748779296875),
+ Vec3fEq(216.944183349609375, 299.722503662109375, -9.33333873748779296875),
+ Vec3fEq(236.9788665771484375, 279.68780517578125, -9.33333873748779296875),
+ Vec3fEq(257.0135498046875, 259.65313720703125, -9.33333873748779296875),
+ Vec3fEq(277.048248291015625, 239.618438720703125, -9.33333873748779296875),
+ Vec3fEq(297.082916259765625, 219.583740234375, -9.33333873748779296875),
+ Vec3fEq(317.11761474609375, 199.549041748046875, -9.33333873748779296875),
+ Vec3fEq(337.15228271484375, 179.5143585205078125, -9.33333873748779296875),
+ Vec3fEq(357.186981201171875, 159.47967529296875, -9.33333873748779296875),
+ Vec3fEq(377.221649169921875, 139.4449920654296875, -9.33333873748779296875),
+ Vec3fEq(397.25634765625, 119.41030120849609375, -6.891522884368896484375),
+ Vec3fEq(417.291046142578125, 99.3756103515625, -4.053897380828857421875),
+ Vec3fEq(437.325714111328125, 79.340911865234375, -1.21627247333526611328125),
+ Vec3fEq(457.360443115234375, 59.3062286376953125, 1.621352672576904296875),
+ Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125)
)) << mPath;
}
@@ -438,6 +426,7 @@ namespace
0, -25, -100, -100, -100,
}};
const HeightfieldSurface surface1 = makeSquareHeightfieldSurface(heightfieldData1);
+ const int cellSize1 = mHeightfieldTileSize * (surface1.mSize - 1);
const std::array<float, 5 * 5> heightfieldData2 {{
-25, -25, -25, -25, -25,
@@ -447,10 +436,11 @@ namespace
-25, -25, -25, -25, -25,
}};
const HeightfieldSurface surface2 = makeSquareHeightfieldSurface(heightfieldData2);
+ const int cellSize2 = mHeightfieldTileSize * (surface2.mSize - 1);
mNavigator->addAgent(mAgentHalfExtents);
- EXPECT_TRUE(mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface1.mSize - 1), mShift, surface1));
- EXPECT_FALSE(mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface2.mSize - 1), mShift, surface2));
+ EXPECT_TRUE(mNavigator->addHeightfield(mCellPosition, cellSize1, surface1));
+ EXPECT_FALSE(mNavigator->addHeightfield(mCellPosition, cellSize2, surface2));
}
TEST_F(DetourNavigatorNavigatorTest, path_should_be_around_avoid_shape)
@@ -482,37 +472,37 @@ namespace
osg::ref_ptr<const Resource::BulletShapeInstance> instance(new Resource::BulletShapeInstance(bulletShape));
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addObject(ObjectId(instance->mCollisionShape.get()), ObjectShapes(instance), btTransform::getIdentity());
+ mNavigator->addObject(ObjectId(instance->mCollisionShape.get()), ObjectShapes(instance, mObjectTransform), mTransform);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(-204, 204, 1.99997997283935546875),
- Vec3fEq(-191.328948974609375, 178.65789794921875, -0.815807759761810302734375),
- Vec3fEq(-178.65789794921875, 153.3157806396484375, -3.6315968036651611328125),
- Vec3fEq(-165.986846923828125, 127.9736785888671875, -6.4473857879638671875),
- Vec3fEq(-153.3157806396484375, 102.6315765380859375, -9.26317310333251953125),
- Vec3fEq(-140.6447296142578125, 77.28946685791015625, -12.07896137237548828125),
- Vec3fEq(-127.9736785888671875, 51.947368621826171875, -14.894748687744140625),
- Vec3fEq(-115.3026275634765625, 26.6052646636962890625, -17.7105388641357421875),
- Vec3fEq(-102.63158416748046875, 1.2631585597991943359375, -20.5263233184814453125),
- Vec3fEq(-89.9605712890625, -24.0789661407470703125, -19.591716766357421875),
- Vec3fEq(-68.54410552978515625, -42.629238128662109375, -19.847625732421875),
- Vec3fEq(-47.127635955810546875, -61.17951202392578125, -20.1035366058349609375),
- Vec3fEq(-25.711170196533203125, -79.72978973388671875, -20.359447479248046875),
- Vec3fEq(-4.294706821441650390625, -98.280059814453125, -20.6153545379638671875),
- Vec3fEq(17.121753692626953125, -116.83034515380859375, -17.3710460662841796875),
- Vec3fEq(42.7990570068359375, -128.80755615234375, -14.7094440460205078125),
- Vec3fEq(68.4763641357421875, -140.7847747802734375, -12.0478420257568359375),
- Vec3fEq(94.15366363525390625, -152.761993408203125, -9.3862361907958984375),
- Vec3fEq(119.83097076416015625, -164.7392120361328125, -6.724635601043701171875),
- Vec3fEq(145.508270263671875, -176.7164306640625, -4.06303119659423828125),
- Vec3fEq(171.185577392578125, -188.69366455078125, -1.40142619609832763671875),
- Vec3fEq(196.862884521484375, -200.6708831787109375, 1.2601754665374755859375),
- Vec3fEq(204, -204, 1.999979496002197265625)
+ Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125),
+ Vec3fEq(69.013885498046875, 434.49853515625, -0.74384129047393798828125),
+ Vec3fEq(81.36110687255859375, 408.997100830078125, -3.4876689910888671875),
+ Vec3fEq(93.7083282470703125, 383.495635986328125, -6.2314929962158203125),
+ Vec3fEq(106.0555419921875, 357.99420166015625, -8.97531890869140625),
+ Vec3fEq(118.40276336669921875, 332.49273681640625, -11.7191448211669921875),
+ Vec3fEq(130.7499847412109375, 306.991302490234375, -14.4629726409912109375),
+ Vec3fEq(143.0972137451171875, 281.4898681640625, -17.206798553466796875),
+ Vec3fEq(155.4444122314453125, 255.9884033203125, -19.9506206512451171875),
+ Vec3fEq(167.7916412353515625, 230.4869537353515625, -19.91887664794921875),
+ Vec3fEq(189.053619384765625, 211.75982666015625, -20.1138629913330078125),
+ Vec3fEq(210.3155975341796875, 193.032684326171875, -20.3088512420654296875),
+ Vec3fEq(231.577606201171875, 174.3055419921875, -20.503841400146484375),
+ Vec3fEq(252.839599609375, 155.5784149169921875, -19.9803981781005859375),
+ Vec3fEq(278.407989501953125, 143.3704071044921875, -17.2675113677978515625),
+ Vec3fEq(303.976348876953125, 131.16241455078125, -14.55462360382080078125),
+ Vec3fEq(329.54473876953125, 118.9544219970703125, -11.84173583984375),
+ Vec3fEq(355.11309814453125, 106.74642181396484375, -9.12884807586669921875),
+ Vec3fEq(380.681488037109375, 94.538421630859375, -6.4159603118896484375),
+ Vec3fEq(406.249847412109375, 82.33042144775390625, -3.7030735015869140625),
+ Vec3fEq(431.8182373046875, 70.1224365234375, -0.990187108516693115234375),
+ Vec3fEq(457.38665771484375, 57.9144439697265625, 1.72269880771636962890625),
+ Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125)
)) << mPath;
}
@@ -526,38 +516,39 @@ namespace
0, -50, -100, -100, -100,
}};
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
+ const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, osg::Vec3f(0, 0, 300));
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
+ mNavigator->addWater(mCellPosition, cellSize, 300);
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- mStart.x() = 0;
+ mStart.x() = 256;
mStart.z() = 300;
- mEnd.x() = 0;
+ mEnd.x() = 256;
mEnd.z() = 300;
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(0, 204, 185.33331298828125),
- Vec3fEq(0, 175.6666717529296875, 185.33331298828125),
- Vec3fEq(0, 147.3333282470703125, 185.33331298828125),
- Vec3fEq(0, 119, 185.33331298828125),
- Vec3fEq(0, 90.6666717529296875, 185.33331298828125),
- Vec3fEq(0, 62.333339691162109375, 185.33331298828125),
- Vec3fEq(0, 34.00000762939453125, 185.33331298828125),
- Vec3fEq(0, 5.66667461395263671875, 185.33331298828125),
- Vec3fEq(0, -22.6666584014892578125, 185.33331298828125),
- Vec3fEq(0, -50.999988555908203125, 185.33331298828125),
- Vec3fEq(0, -79.33332061767578125, 185.33331298828125),
- Vec3fEq(0, -107.666656494140625, 185.33331298828125),
- Vec3fEq(0, -135.9999847412109375, 185.33331298828125),
- Vec3fEq(0, -164.33331298828125, 185.33331298828125),
- Vec3fEq(0, -192.666656494140625, 185.33331298828125),
- Vec3fEq(0, -204, 185.33331298828125)
+ Vec3fEq(256, 460, 185.33331298828125),
+ Vec3fEq(256, 431.666656494140625, 185.33331298828125),
+ Vec3fEq(256, 403.33331298828125, 185.33331298828125),
+ Vec3fEq(256, 375, 185.33331298828125),
+ Vec3fEq(256, 346.666656494140625, 185.33331298828125),
+ Vec3fEq(256, 318.33331298828125, 185.33331298828125),
+ Vec3fEq(256, 290, 185.33331298828125),
+ Vec3fEq(256, 261.666656494140625, 185.33331298828125),
+ Vec3fEq(256, 233.3333282470703125, 185.33331298828125),
+ Vec3fEq(256, 205, 185.33331298828125),
+ Vec3fEq(256, 176.6666717529296875, 185.33331298828125),
+ Vec3fEq(256, 148.3333282470703125, 185.33331298828125),
+ Vec3fEq(256, 120, 185.33331298828125),
+ Vec3fEq(256, 91.6666717529296875, 185.33331298828125),
+ Vec3fEq(255.999969482421875, 63.33333587646484375, 185.33331298828125),
+ Vec3fEq(255.999969482421875, 56.66666412353515625, 185.33331298828125)
)) << mPath;
}
@@ -573,36 +564,37 @@ namespace
0, 0, 0, 0, 0, 0, 0,
}};
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
+ const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addWater(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), osg::Vec3f(0, 0, -25));
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
+ mNavigator->addWater(mCellPosition, cellSize, -25);
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- mStart.x() = 0;
- mEnd.x() = 0;
+ mStart.x() = 256;
+ mEnd.x() = 256;
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(0, 204, -98.000030517578125),
- Vec3fEq(0, 175.6666717529296875, -108.30306243896484375),
- Vec3fEq(0, 147.3333282470703125, -118.6060791015625),
- Vec3fEq(0, 119, -128.90911865234375),
- Vec3fEq(0, 90.6666717529296875, -139.2121429443359375),
- Vec3fEq(0, 62.333339691162109375, -143.3333587646484375),
- Vec3fEq(0, 34.00000762939453125, -143.3333587646484375),
- Vec3fEq(0, 5.66667461395263671875, -143.3333587646484375),
- Vec3fEq(0, -22.6666584014892578125, -143.3333587646484375),
- Vec3fEq(0, -50.999988555908203125, -143.3333587646484375),
- Vec3fEq(0, -79.33332061767578125, -143.3333587646484375),
- Vec3fEq(0, -107.666656494140625, -133.0303192138671875),
- Vec3fEq(0, -135.9999847412109375, -122.72728729248046875),
- Vec3fEq(0, -164.33331298828125, -112.4242706298828125),
- Vec3fEq(0, -192.666656494140625, -102.12123870849609375),
- Vec3fEq(0, -204, -98.00002288818359375)
+ Vec3fEq(256, 460, -129.4098663330078125),
+ Vec3fEq(256, 431.666656494140625, -129.6970062255859375),
+ Vec3fEq(256, 403.33331298828125, -129.6970062255859375),
+ Vec3fEq(256, 375, -129.4439239501953125),
+ Vec3fEq(256, 346.666656494140625, -129.02587890625),
+ Vec3fEq(256, 318.33331298828125, -128.6078338623046875),
+ Vec3fEq(256, 290, -128.1021728515625),
+ Vec3fEq(256, 261.666656494140625, -126.46875),
+ Vec3fEq(256, 233.3333282470703125, -119.4891357421875),
+ Vec3fEq(256, 205, -110.62021636962890625),
+ Vec3fEq(256, 176.6666717529296875, -101.7512969970703125),
+ Vec3fEq(256, 148.3333282470703125, -92.88237762451171875),
+ Vec3fEq(256, 120, -75.29378509521484375),
+ Vec3fEq(256, 91.6666717529296875, -55.201839447021484375),
+ Vec3fEq(256.000030517578125, 63.33333587646484375, -34.800380706787109375),
+ Vec3fEq(256.000030517578125, 56.66666412353515625, -30.00003814697265625)
)) << mPath;
}
@@ -618,36 +610,37 @@ namespace
0, 0, 0, 0, 0, 0, 0,
}};
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
+ const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
- mNavigator->addWater(osg::Vec2i(0, 0), std::numeric_limits<int>::max(), osg::Vec3f(0, 0, -25));
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
+ mNavigator->addWater(mCellPosition, std::numeric_limits<int>::max(), -25);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- mStart.x() = 0;
- mEnd.x() = 0;
+ mStart.x() = 256;
+ mEnd.x() = 256;
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(0, 204, -98.000030517578125),
- Vec3fEq(0, 175.6666717529296875, -108.30306243896484375),
- Vec3fEq(0, 147.3333282470703125, -118.6060791015625),
- Vec3fEq(0, 119, -128.90911865234375),
- Vec3fEq(0, 90.6666717529296875, -139.2121429443359375),
- Vec3fEq(0, 62.333339691162109375, -143.3333587646484375),
- Vec3fEq(0, 34.00000762939453125, -143.3333587646484375),
- Vec3fEq(0, 5.66667461395263671875, -143.3333587646484375),
- Vec3fEq(0, -22.6666584014892578125, -143.3333587646484375),
- Vec3fEq(0, -50.999988555908203125, -143.3333587646484375),
- Vec3fEq(0, -79.33332061767578125, -143.3333587646484375),
- Vec3fEq(0, -107.666656494140625, -133.0303192138671875),
- Vec3fEq(0, -135.9999847412109375, -122.72728729248046875),
- Vec3fEq(0, -164.33331298828125, -112.4242706298828125),
- Vec3fEq(0, -192.666656494140625, -102.12123870849609375),
- Vec3fEq(0, -204, -98.00002288818359375)
+ Vec3fEq(256, 460, -129.4098663330078125),
+ Vec3fEq(256, 431.666656494140625, -129.6970062255859375),
+ Vec3fEq(256, 403.33331298828125, -129.6970062255859375),
+ Vec3fEq(256, 375, -129.4439239501953125),
+ Vec3fEq(256, 346.666656494140625, -129.02587890625),
+ Vec3fEq(256, 318.33331298828125, -128.6078338623046875),
+ Vec3fEq(256, 290, -128.1021728515625),
+ Vec3fEq(256, 261.666656494140625, -126.46875),
+ Vec3fEq(256, 233.3333282470703125, -119.4891357421875),
+ Vec3fEq(256, 205, -110.62021636962890625),
+ Vec3fEq(256, 176.6666717529296875, -101.7512969970703125),
+ Vec3fEq(256, 148.3333282470703125, -92.88237762451171875),
+ Vec3fEq(256, 120, -75.29378509521484375),
+ Vec3fEq(256, 91.6666717529296875, -55.201839447021484375),
+ Vec3fEq(256.000030517578125, 63.33333587646484375, -34.800380706787109375),
+ Vec3fEq(256.000030517578125, 56.66666412353515625, -30.00003814697265625)
)) << mPath;
}
@@ -663,37 +656,37 @@ namespace
0, 0, 0, 0, 0, 0, 0,
}};
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
+ const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, osg::Vec3f(0, 0, -25));
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
+ mNavigator->addWater(mCellPosition, cellSize, -25);
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- mStart.x() = 0;
- mEnd.x() = 0;
+ mStart.x() = 256;
+ mEnd.x() = 256;
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(0, 204, -98.000030517578125),
- Vec3fEq(10.26930999755859375, 177.59320068359375, -107.4711456298828125),
- Vec3fEq(20.5386199951171875, 151.1864166259765625, -116.9422607421875),
- Vec3fEq(30.8079280853271484375, 124.77960968017578125, -126.41339111328125),
- Vec3fEq(41.077239990234375, 98.37281036376953125, -135.8845062255859375),
- Vec3fEq(51.346546173095703125, 71.96601104736328125, -138.2003936767578125),
- Vec3fEq(61.615856170654296875, 45.559215545654296875, -140.0838470458984375),
- Vec3fEq(71.88516998291015625, 19.1524181365966796875, -141.9673004150390625),
- Vec3fEq(82.15447235107421875, -7.254379749298095703125, -142.3074798583984375),
- Vec3fEq(81.04636383056640625, -35.56603240966796875, -142.7104339599609375),
- Vec3fEq(79.93825531005859375, -63.877685546875, -143.1133880615234375),
- Vec3fEq(78.83014678955078125, -92.18933868408203125, -138.7660675048828125),
- Vec3fEq(62.50392913818359375, -115.3460235595703125, -130.237823486328125),
- Vec3fEq(46.17771148681640625, -138.502716064453125, -121.8172149658203125),
- Vec3fEq(29.85149383544921875, -161.6594085693359375, -113.39659881591796875),
- Vec3fEq(13.52527523040771484375, -184.81610107421875, -104.97599029541015625),
- Vec3fEq(0, -204, -98.00002288818359375)
+ Vec3fEq(256, 460, -129.4098663330078125),
+ Vec3fEq(256, 431.666656494140625, -129.6970062255859375),
+ Vec3fEq(256, 403.33331298828125, -129.6970062255859375),
+ Vec3fEq(256, 375, -129.4439239501953125),
+ Vec3fEq(256, 346.666656494140625, -129.02587890625),
+ Vec3fEq(256, 318.33331298828125, -128.6078338623046875),
+ Vec3fEq(256, 290, -128.1021728515625),
+ Vec3fEq(256, 261.666656494140625, -126.46875),
+ Vec3fEq(256, 233.3333282470703125, -119.4891357421875),
+ Vec3fEq(256, 205, -110.62021636962890625),
+ Vec3fEq(256, 176.6666717529296875, -101.7512969970703125),
+ Vec3fEq(256, 148.3333282470703125, -92.88237762451171875),
+ Vec3fEq(256, 120, -75.29378509521484375),
+ Vec3fEq(256, 91.6666717529296875, -55.201839447021484375),
+ Vec3fEq(256.000030517578125, 63.33333587646484375, -34.800380706787109375),
+ Vec3fEq(256.000030517578125, 56.66666412353515625, -30.00003814697265625)
)) << mPath;
}
@@ -710,7 +703,7 @@ namespace
heightfield.shape().setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addObject(ObjectId(&heightfield.shape()), ObjectShapes(heightfield.instance()), btTransform::getIdentity());
+ mNavigator->addObject(ObjectId(&heightfield.shape()), ObjectShapes(heightfield.instance(), mObjectTransform), mTransform);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
@@ -718,36 +711,36 @@ namespace
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- mNavigator->addObject(ObjectId(&heightfield.shape()), ObjectShapes(heightfield.instance()), btTransform::getIdentity());
+ mNavigator->addObject(ObjectId(&heightfield.shape()), ObjectShapes(heightfield.instance(), mObjectTransform), mTransform);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(-204, 204, 1.99998295307159423828125),
- Vec3fEq(-183.965301513671875, 183.965301513671875, 1.99998819828033447265625),
- Vec3fEq(-163.9306182861328125, 163.9306182861328125, 1.99999344348907470703125),
- Vec3fEq(-143.89593505859375, 143.89593505859375, -2.7206256389617919921875),
- Vec3fEq(-123.86124420166015625, 123.86124420166015625, -13.1089839935302734375),
- Vec3fEq(-103.8265533447265625, 103.8265533447265625, -23.4973468780517578125),
- Vec3fEq(-83.7918548583984375, 83.7918548583984375, -33.885707855224609375),
- Vec3fEq(-63.75716400146484375, 63.75716400146484375, -44.27407073974609375),
- Vec3fEq(-43.72247314453125, 43.72247314453125, -54.662433624267578125),
- Vec3fEq(-23.6877803802490234375, 23.6877803802490234375, -65.0507965087890625),
- Vec3fEq(-3.653090000152587890625, 3.653090000152587890625, -75.43915557861328125),
- Vec3fEq(16.3816013336181640625, -16.3816013336181640625, -69.749267578125),
- Vec3fEq(36.416290283203125, -36.416290283203125, -60.4739532470703125),
- Vec3fEq(56.450984954833984375, -56.450984954833984375, -51.1986236572265625),
- Vec3fEq(76.4856719970703125, -76.4856719970703125, -41.92330169677734375),
- Vec3fEq(96.52036285400390625, -96.52036285400390625, -31.46941375732421875),
- Vec3fEq(116.5550537109375, -116.5550537109375, -19.597003936767578125),
- Vec3fEq(136.5897369384765625, -136.5897369384765625, -7.724592685699462890625),
- Vec3fEq(156.6244354248046875, -156.6244354248046875, 1.99999535083770751953125),
- Vec3fEq(176.6591339111328125, -176.6591339111328125, 1.99999010562896728515625),
- Vec3fEq(196.693817138671875, -196.693817138671875, 1.99998486042022705078125),
- Vec3fEq(204, -204, 1.99998295307159423828125)
+ Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125),
+ Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375),
+ Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875),
+ Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125),
+ Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875),
+ Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875),
+ Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375),
+ Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125),
+ Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875),
+ Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375),
+ Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125),
+ Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625),
+ Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875),
+ Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125),
+ Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375),
+ Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375),
+ Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875),
+ Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625),
+ Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625),
+ Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125),
+ Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875),
+ Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125)
)) << mPath;
}
@@ -761,9 +754,10 @@ namespace
0, -25, -100, -100, -100,
}};
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
+ const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
@@ -771,71 +765,73 @@ namespace
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(-204, 204, 1.99998295307159423828125),
- Vec3fEq(-183.965301513671875, 183.965301513671875, 1.99998819828033447265625),
- Vec3fEq(-163.9306182861328125, 163.9306182861328125, 1.99999344348907470703125),
- Vec3fEq(-143.89593505859375, 143.89593505859375, -2.7206256389617919921875),
- Vec3fEq(-123.86124420166015625, 123.86124420166015625, -13.1089839935302734375),
- Vec3fEq(-103.8265533447265625, 103.8265533447265625, -23.4973468780517578125),
- Vec3fEq(-83.7918548583984375, 83.7918548583984375, -33.885707855224609375),
- Vec3fEq(-63.75716400146484375, 63.75716400146484375, -44.27407073974609375),
- Vec3fEq(-43.72247314453125, 43.72247314453125, -54.662433624267578125),
- Vec3fEq(-23.6877803802490234375, 23.6877803802490234375, -65.0507965087890625),
- Vec3fEq(-3.653090000152587890625, 3.653090000152587890625, -75.43915557861328125),
- Vec3fEq(16.3816013336181640625, -16.3816013336181640625, -69.749267578125),
- Vec3fEq(36.416290283203125, -36.416290283203125, -60.4739532470703125),
- Vec3fEq(56.450984954833984375, -56.450984954833984375, -51.1986236572265625),
- Vec3fEq(76.4856719970703125, -76.4856719970703125, -41.92330169677734375),
- Vec3fEq(96.52036285400390625, -96.52036285400390625, -31.46941375732421875),
- Vec3fEq(116.5550537109375, -116.5550537109375, -19.597003936767578125),
- Vec3fEq(136.5897369384765625, -136.5897369384765625, -7.724592685699462890625),
- Vec3fEq(156.6244354248046875, -156.6244354248046875, 1.99999535083770751953125),
- Vec3fEq(176.6591339111328125, -176.6591339111328125, 1.99999010562896728515625),
- Vec3fEq(196.693817138671875, -196.693817138671875, 1.99998486042022705078125),
- Vec3fEq(204, -204, 1.99998295307159423828125)
+ Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125),
+ Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375),
+ Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875),
+ Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125),
+ Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875),
+ Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875),
+ Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375),
+ Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125),
+ Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875),
+ Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375),
+ Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125),
+ Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625),
+ Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875),
+ Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125),
+ Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375),
+ Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375),
+ Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875),
+ Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625),
+ Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625),
+ Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125),
+ Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875),
+ Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125)
)) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, update_then_find_random_point_around_circle_should_return_position)
{
- const std::array<float, 5 * 5> heightfieldData {{
- 0, 0, 0, 0, 0,
- 0, -25, -25, -25, -25,
- 0, -25, -100, -100, -100,
- 0, -25, -100, -100, -100,
- 0, -25, -100, -100, -100,
+ const std::array<float, 6 * 6> heightfieldData {{
+ 0, 0, 0, 0, 0, 0,
+ 0, -25, -25, -25, -25, -25,
+ 0, -25, -1000, -1000, -100, -100,
+ 0, -25, -1000, -1000, -100, -100,
+ 0, -25, -100, -100, -100, -100,
+ 0, -25, -100, -100, -100, -100,
}};
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
+ const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
Misc::Rng::init(42);
- const auto result = mNavigator->findRandomPointAroundCircle(mAgentHalfExtents, mStart, 100.0, Flag_walk);
+ const auto result = findRandomPointAroundCircle(*mNavigator, mAgentHalfExtents, mStart, 100.0, Flag_walk);
- ASSERT_THAT(result, Optional(Vec3fEq(-198.909332275390625, 123.06096649169921875, 1.99998414516448974609375)))
+ ASSERT_THAT(result, Optional(Vec3fEq(69.6253509521484375, 531.29852294921875, -2.6667339801788330078125)))
<< (result ? *result : osg::Vec3f());
const auto distance = (*result - mStart).length();
- EXPECT_FLOAT_EQ(distance, 81.105133056640625) << distance;
+ EXPECT_FLOAT_EQ(distance, 73.536231994628906) << distance;
}
TEST_F(DetourNavigatorNavigatorTest, multiple_threads_should_lock_tiles)
{
mSettings.mAsyncNavMeshUpdaterThreads = 2;
- mNavigator.reset(new NavigatorImpl(mSettings));
+ mNavigator.reset(new NavigatorImpl(mSettings, std::make_unique<NavMeshDb>(":memory:")));
const std::array<float, 5 * 5> heightfieldData {{
0, 0, 0, 0, 0,
@@ -845,58 +841,60 @@ namespace
0, -25, -100, -100, -100,
}};
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
+ const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
+ const btVector3 shift = getHeightfieldShift(mCellPosition, cellSize, surface.mMinHeight, surface.mMaxHeight);
std::vector<CollisionShapeInstance<btBoxShape>> boxes;
std::generate_n(std::back_inserter(boxes), 100, [] { return std::make_unique<btBoxShape>(btVector3(20, 20, 100)); });
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
for (std::size_t i = 0; i < boxes.size(); ++i)
{
- const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 10, i * 10, i * 10));
- mNavigator->addObject(ObjectId(&boxes[i].shape()), ObjectShapes(boxes[i].instance()), transform);
+ const btTransform transform(btMatrix3x3::getIdentity(), btVector3(shift.x() + i * 10, shift.y() + i * 10, i * 10));
+ mNavigator->addObject(ObjectId(&boxes[i].shape()), ObjectShapes(boxes[i].instance(), mObjectTransform), transform);
}
std::this_thread::sleep_for(std::chrono::microseconds(1));
for (std::size_t i = 0; i < boxes.size(); ++i)
{
- const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 10 + 1, i * 10 + 1, i * 10 + 1));
- mNavigator->updateObject(ObjectId(&boxes[i].shape()), ObjectShapes(boxes[i].instance()), transform);
+ const btTransform transform(btMatrix3x3::getIdentity(), btVector3(shift.x() + i * 10 + 1, shift.y() + i * 10 + 1, i * 10 + 1));
+ mNavigator->updateObject(ObjectId(&boxes[i].shape()), ObjectShapes(boxes[i].instance(), mObjectTransform), transform);
}
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(-204, 204, 1.99998295307159423828125),
- Vec3fEq(-189.9427337646484375, 179.3997802734375, 1.9999866485595703125),
- Vec3fEq(-175.8854522705078125, 154.7995452880859375, 1.99999034404754638671875),
- Vec3fEq(-161.82818603515625, 130.1993255615234375, -3.701923847198486328125),
- Vec3fEq(-147.770904541015625, 105.5991058349609375, -15.67664432525634765625),
- Vec3fEq(-133.7136383056640625, 80.99887847900390625, -27.6513614654541015625),
- Vec3fEq(-119.65636444091796875, 56.39865875244140625, -20.1209163665771484375),
- Vec3fEq(-105.59909820556640625, 31.798435211181640625, -25.0669879913330078125),
- Vec3fEq(-91.54183197021484375, 7.1982135772705078125, -31.5624217987060546875),
- Vec3fEq(-77.48455810546875, -17.402008056640625, -26.98972320556640625),
- Vec3fEq(-63.427295684814453125, -42.00223541259765625, -19.9045581817626953125),
- Vec3fEq(-42.193531036376953125, -60.761363983154296875, -20.4544773101806640625),
- Vec3fEq(-20.9597682952880859375, -79.5204925537109375, -23.599918365478515625),
- Vec3fEq(3.8312885761260986328125, -93.2384033203125, -30.7141361236572265625),
- Vec3fEq(28.6223468780517578125, -106.95632171630859375, -24.1782474517822265625),
- Vec3fEq(53.413402557373046875, -120.6742401123046875, -19.4096889495849609375),
- Vec3fEq(78.20446014404296875, -134.39215087890625, -27.6632633209228515625),
- Vec3fEq(102.99552154541015625, -148.110076904296875, -15.8613681793212890625),
- Vec3fEq(127.7865753173828125, -161.827972412109375, -4.059485912322998046875),
- Vec3fEq(152.57763671875, -175.5458984375, 1.9999904632568359375),
- Vec3fEq(177.3686981201171875, -189.2638092041015625, 1.9999866485595703125),
- Vec3fEq(202.1597442626953125, -202.9817047119140625, 1.9999830722808837890625),
- Vec3fEq(204, -204, 1.99998295307159423828125)
+ Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125),
+ Vec3fEq(69.5299530029296875, 434.754913330078125, -2.6775772571563720703125),
+ Vec3fEq(82.39324951171875, 409.50982666015625, -7.355137348175048828125),
+ Vec3fEq(95.25653839111328125, 384.2647705078125, -12.0326976776123046875),
+ Vec3fEq(108.11983489990234375, 359.019683837890625, -16.71025848388671875),
+ Vec3fEq(120.983123779296875, 333.774627685546875, -21.3878192901611328125),
+ Vec3fEq(133.8464202880859375, 308.529541015625, -26.0653781890869140625),
+ Vec3fEq(146.7097015380859375, 283.284454345703125, -30.7429370880126953125),
+ Vec3fEq(159.572998046875, 258.039398193359375, -35.420497894287109375),
+ Vec3fEq(172.4362945556640625, 232.7943115234375, -27.2731761932373046875),
+ Vec3fEq(185.2996063232421875, 207.54925537109375, -20.3612518310546875),
+ Vec3fEq(206.6449737548828125, 188.917236328125, -20.578319549560546875),
+ Vec3fEq(227.9903564453125, 170.28521728515625, -26.291717529296875),
+ Vec3fEq(253.4362640380859375, 157.8239593505859375, -34.784488677978515625),
+ Vec3fEq(278.8822021484375, 145.3627166748046875, -30.253124237060546875),
+ Vec3fEq(304.328094482421875, 132.9014739990234375, -25.72176361083984375),
+ Vec3fEq(329.774017333984375, 120.44022369384765625, -21.1904010772705078125),
+ Vec3fEq(355.219940185546875, 107.97898101806640625, -16.6590404510498046875),
+ Vec3fEq(380.665863037109375, 95.51773834228515625, -12.127681732177734375),
+ Vec3fEq(406.111785888671875, 83.05649566650390625, -7.5963191986083984375),
+ Vec3fEq(431.557708740234375, 70.5952606201171875, -3.0649592876434326171875),
+ Vec3fEq(457.003662109375, 58.134021759033203125, 1.4664003849029541015625),
+ Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125)
)) << mPath;
}
@@ -910,7 +908,7 @@ namespace
for (std::size_t i = 0; i < shapes.size(); ++i)
{
const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32, i * 32, i * 32));
- mNavigator->addObject(ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance()), transform);
+ mNavigator->addObject(ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance(), mObjectTransform), transform);
}
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
@@ -919,7 +917,7 @@ namespace
for (std::size_t i = 0; i < shapes.size(); ++i)
{
const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32 + 1, i * 32 + 1, i * 32 + 1));
- mNavigator->updateObject(ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance()), transform);
+ mNavigator->updateObject(ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance(), mObjectTransform), transform);
}
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
@@ -927,7 +925,7 @@ namespace
for (std::size_t i = 0; i < shapes.size(); ++i)
{
const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32 + 2, i * 32 + 2, i * 32 + 2));
- mNavigator->updateObject(ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance()), transform);
+ mNavigator->updateObject(ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance(), mObjectTransform), transform);
}
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
@@ -948,15 +946,18 @@ namespace
0, -25, -100, -100, -100,
}};
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
+ const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- const auto result = mNavigator->raycast(mAgentHalfExtents, mStart, mEnd, Flag_walk);
+ const osg::Vec3f start(57, 460, 1);
+ const osg::Vec3f end(460, 57, 1);
+ const auto result = raycast(*mNavigator, mAgentHalfExtents, start, end, Flag_walk);
- ASSERT_THAT(result, Optional(Vec3fEq(mEnd.x(), mEnd.y(), 1.99998295307159423828125)))
+ ASSERT_THAT(result, Optional(Vec3fEq(end.x(), end.y(), 1.95257937908172607421875)))
<< (result ? *result : osg::Vec3f());
}
@@ -970,81 +971,77 @@ namespace
0, -25, -100, -100, -100,
}};
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
+ const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
CollisionShapeInstance oscillatingBox(std::make_unique<btBoxShape>(btVector3(20, 20, 20)));
- const btVector3 oscillatingBoxShapePosition(32, 32, 400);
- CollisionShapeInstance boderBox(std::make_unique<btBoxShape>(btVector3(50, 50, 50)));
+ const btVector3 oscillatingBoxShapePosition(288, 288, 400);
+ CollisionShapeInstance borderBox(std::make_unique<btBoxShape>(btVector3(50, 50, 50)));
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
- mNavigator->addObject(ObjectId(&oscillatingBox.shape()), ObjectShapes(oscillatingBox.instance()),
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
+ mNavigator->addObject(ObjectId(&oscillatingBox.shape()), ObjectShapes(oscillatingBox.instance(), mObjectTransform),
btTransform(btMatrix3x3::getIdentity(), oscillatingBoxShapePosition));
// add this box to make navmesh bound box independent from oscillatingBoxShape rotations
- mNavigator->addObject(ObjectId(&boderBox.shape()), ObjectShapes(boderBox.instance()),
+ mNavigator->addObject(ObjectId(&borderBox.shape()), ObjectShapes(borderBox.instance(), mObjectTransform),
btTransform(btMatrix3x3::getIdentity(), oscillatingBoxShapePosition + btVector3(0, 0, 200)));
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
+ const Version expectedVersion {1, 4};
+
const auto navMeshes = mNavigator->getNavMeshes();
ASSERT_EQ(navMeshes.size(), 1);
- {
- const auto navMesh = navMeshes.begin()->second->lockConst();
- ASSERT_EQ(navMesh->getGeneration(), 1);
- ASSERT_EQ(navMesh->getNavMeshRevision(), 4);
- }
+ ASSERT_EQ(navMeshes.begin()->second->lockConst()->getVersion(), expectedVersion);
for (int n = 0; n < 10; ++n)
{
const btTransform transform(btQuaternion(btVector3(0, 0, 1), n * 2 * osg::PI / 10),
oscillatingBoxShapePosition);
- mNavigator->updateObject(ObjectId(&oscillatingBox.shape()), ObjectShapes(oscillatingBox.instance()), transform);
+ mNavigator->updateObject(ObjectId(&oscillatingBox.shape()), ObjectShapes(oscillatingBox.instance(), mObjectTransform), transform);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
}
ASSERT_EQ(navMeshes.size(), 1);
- {
- const auto navMesh = navMeshes.begin()->second->lockConst();
- ASSERT_EQ(navMesh->getGeneration(), 1);
- ASSERT_EQ(navMesh->getNavMeshRevision(), 4);
- }
+ ASSERT_EQ(navMeshes.begin()->second->lockConst()->getVersion(), expectedVersion);
}
TEST_F(DetourNavigatorNavigatorTest, should_provide_path_over_flat_heightfield)
{
const HeightfieldPlane plane {100};
+ const int cellSize = mHeightfieldTileSize * 4;
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * 4, mShift, plane);
+ mNavigator->addHeightfield(mCellPosition, cellSize, plane);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::requiredTilesPresent);
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(-204, 204, 101.99999237060546875),
- Vec3fEq(-183.965301513671875, 183.965301513671875, 101.99999237060546875),
- Vec3fEq(-163.9306182861328125, 163.9306182861328125, 101.99999237060546875),
- Vec3fEq(-143.89593505859375, 143.89593505859375, 101.99999237060546875),
- Vec3fEq(-123.86124420166015625, 123.86124420166015625, 101.99999237060546875),
- Vec3fEq(-103.8265533447265625, 103.8265533447265625, 101.99999237060546875),
- Vec3fEq(-83.7918548583984375, 83.7918548583984375, 101.99999237060546875),
- Vec3fEq(-63.75716400146484375, 63.75716400146484375, 101.99999237060546875),
- Vec3fEq(-43.72247314453125, 43.72247314453125, 101.99999237060546875),
- Vec3fEq(-23.6877803802490234375, 23.6877803802490234375, 101.99999237060546875),
- Vec3fEq(-3.653090000152587890625, 3.653090000152587890625, 101.99999237060546875),
- Vec3fEq(16.3816013336181640625, -16.3816013336181640625, 101.99999237060546875),
- Vec3fEq(36.416290283203125, -36.416290283203125, 101.99999237060546875),
- Vec3fEq(56.450984954833984375, -56.450984954833984375, 101.99999237060546875),
- Vec3fEq(76.4856719970703125, -76.4856719970703125, 101.99999237060546875),
- Vec3fEq(96.52036285400390625, -96.52036285400390625, 101.99999237060546875),
- Vec3fEq(116.5550537109375, -116.5550537109375, 101.99999237060546875),
- Vec3fEq(136.5897369384765625, -136.5897369384765625, 101.99999237060546875),
- Vec3fEq(156.6244354248046875, -156.6244354248046875, 101.99999237060546875),
- Vec3fEq(176.6591339111328125, -176.6591339111328125, 101.99999237060546875),
- Vec3fEq(196.693817138671875, -196.693817138671875, 101.99999237060546875),
- Vec3fEq(204, -204, 101.99999237060546875)
+ Vec3fEq(56.66666412353515625, 460, 101.99999237060546875),
+ Vec3fEq(76.70135498046875, 439.965301513671875, 101.99999237060546875),
+ Vec3fEq(96.73604583740234375, 419.93060302734375, 101.99999237060546875),
+ Vec3fEq(116.770751953125, 399.89593505859375, 101.99999237060546875),
+ Vec3fEq(136.8054351806640625, 379.861236572265625, 101.99999237060546875),
+ Vec3fEq(156.840118408203125, 359.826568603515625, 101.99999237060546875),
+ Vec3fEq(176.8748016357421875, 339.7918701171875, 101.99999237060546875),
+ Vec3fEq(196.90948486328125, 319.757171630859375, 101.99999237060546875),
+ Vec3fEq(216.944183349609375, 299.722503662109375, 101.99999237060546875),
+ Vec3fEq(236.9788665771484375, 279.68780517578125, 101.99999237060546875),
+ Vec3fEq(257.0135498046875, 259.65313720703125, 101.99999237060546875),
+ Vec3fEq(277.048248291015625, 239.618438720703125, 101.99999237060546875),
+ Vec3fEq(297.082916259765625, 219.583740234375, 101.99999237060546875),
+ Vec3fEq(317.11761474609375, 199.549041748046875, 101.99999237060546875),
+ Vec3fEq(337.15228271484375, 179.5143585205078125, 101.99999237060546875),
+ Vec3fEq(357.186981201171875, 159.47967529296875, 101.99999237060546875),
+ Vec3fEq(377.221649169921875, 139.4449920654296875, 101.99999237060546875),
+ Vec3fEq(397.25634765625, 119.41030120849609375, 101.99999237060546875),
+ Vec3fEq(417.291046142578125, 99.3756103515625, 101.99999237060546875),
+ Vec3fEq(437.325714111328125, 79.340911865234375, 101.99999237060546875),
+ Vec3fEq(457.360443115234375, 59.3062286376953125, 101.99999237060546875),
+ Vec3fEq(460, 56.66666412353515625, 101.99999237060546875)
)) << mPath;
}
@@ -1058,38 +1055,32 @@ namespace
0, -25, -100, -100, -100,
}};
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
+ const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
CollisionShapeInstance compound(std::make_unique<btCompoundShape>());
compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(204, -204, 0)),
new btBoxShape(btVector3(200, 200, 1000)));
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
- mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), btTransform::getIdentity());
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
+ mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), mTransform);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
Status::PartialPath);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(-204, 204, -2.666739940643310546875),
- Vec3fEq(-193.730682373046875, 177.59320068359375, -3.9535934925079345703125),
- Vec3fEq(-183.4613800048828125, 151.1864166259765625, -5.240451335906982421875),
- Vec3fEq(-173.1920623779296875, 124.77962493896484375, -6.527309417724609375),
- Vec3fEq(-162.922760009765625, 98.37282562255859375, -7.814167022705078125),
- Vec3fEq(-152.6534423828125, 71.96602630615234375, -9.898590087890625),
- Vec3fEq(-142.384124755859375, 45.559230804443359375, -17.641445159912109375),
- Vec3fEq(-132.1148223876953125, 19.152431488037109375, -25.3843059539794921875),
- Vec3fEq(-121.8455047607421875, -7.254369258880615234375, -27.97742462158203125),
- Vec3fEq(-111.57619476318359375, -33.66117095947265625, -16.974590301513671875),
- Vec3fEq(-101.30689239501953125, -60.06797027587890625, -5.9717559814453125),
- Vec3fEq(-91.0375823974609375, -86.47476959228515625, -2.6667339801788330078125),
- Vec3fEq(-80.76827239990234375, -112.88156890869140625, -2.6667339801788330078125),
- Vec3fEq(-70.49897003173828125, -139.2883758544921875, -2.6667339801788330078125),
- Vec3fEq(-60.229663848876953125, -165.6951751708984375, -2.6667339801788330078125),
- Vec3fEq(-49.96035003662109375, -192.1019744873046875, -2.6667339801788330078125),
- Vec3fEq(-45.333343505859375, -204, -2.6667339801788330078125)
+ Vec3fEq(56.66664886474609375, 460, -2.5371043682098388671875),
+ Vec3fEq(76.42063140869140625, 439.6884765625, -2.9134314060211181640625),
+ Vec3fEq(96.17461395263671875, 419.376953125, -4.50826549530029296875),
+ Vec3fEq(115.9285888671875, 399.0654296875, -6.1030979156494140625),
+ Vec3fEq(135.6825714111328125, 378.753936767578125, -7.697928905487060546875),
+ Vec3fEq(155.436553955078125, 358.442413330078125, -20.9574985504150390625),
+ Vec3fEq(175.190521240234375, 338.130889892578125, -35.907512664794921875),
+ Vec3fEq(194.9445037841796875, 317.8193359375, -50.85752105712890625),
+ Vec3fEq(214.698486328125, 297.5078125, -65.807525634765625),
+ Vec3fEq(222.0001068115234375, 290.000091552734375, -71.333465576171875)
)) << mPath;
}
@@ -1103,42 +1094,42 @@ namespace
0, -25, -100, -100, -100,
}};
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
+ const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
CollisionShapeInstance compound(std::make_unique<btCompoundShape>());
compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(204, -204, 0)),
new btBoxShape(btVector3(100, 100, 1000)));
mNavigator->addAgent(mAgentHalfExtents);
- mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface);
- mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), btTransform::getIdentity());
+ mNavigator->addHeightfield(mCellPosition, cellSize, surface);
+ mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), mTransform);
mNavigator->update(mPlayerPosition);
mNavigator->wait(mListener, WaitConditionType::allJobsDone);
const float endTolerance = 1000.0f;
- EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, mOut),
+ EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(
- Vec3fEq(-204, 204, -2.666739940643310546875),
- Vec3fEq(-188.745635986328125, 180.1236114501953125, -4.578275203704833984375),
- Vec3fEq(-173.49127197265625, 156.247222900390625, -6.489814281463623046875),
- Vec3fEq(-158.2369232177734375, 132.370819091796875, -8.4013538360595703125),
- Vec3fEq(-142.9825592041015625, 108.49443817138671875, -10.31289386749267578125),
- Vec3fEq(-127.7281951904296875, 84.6180419921875, -17.4810466766357421875),
- Vec3fEq(-112.47383880615234375, 60.7416534423828125, -27.6026020050048828125),
- Vec3fEq(-97.21947479248046875, 36.865261077880859375, -37.724163055419921875),
- Vec3fEq(-81.965118408203125, 12.98886966705322265625, -47.84572601318359375),
- Vec3fEq(-66.71075439453125, -10.887523651123046875, -46.691577911376953125),
- Vec3fEq(-51.45639801025390625, -34.763916015625, -32.085445404052734375),
- Vec3fEq(-36.202037811279296875, -58.640308380126953125, -28.5217914581298828125),
- Vec3fEq(-20.947673797607421875, -82.5167083740234375, -32.16143035888671875),
- Vec3fEq(-5.693310260772705078125, -106.393096923828125, -35.8010711669921875),
- Vec3fEq(9.56105327606201171875, -130.2694854736328125, -29.6399688720703125),
- Vec3fEq(24.8154163360595703125, -154.1458740234375, -17.6428318023681640625),
- Vec3fEq(40.0697784423828125, -178.0222625732421875, -10.46006107330322265625),
- Vec3fEq(55.3241424560546875, -201.8986663818359375, -3.297139644622802734375),
- Vec3fEq(56.66666412353515625, -204, -2.6667373180389404296875)
+ Vec3fEq(56.66666412353515625, 460, -2.5371043682098388671875),
+ Vec3fEq(71.5649566650390625, 435.899810791015625, -5.817593097686767578125),
+ Vec3fEq(86.46324920654296875, 411.79962158203125, -9.66499996185302734375),
+ Vec3fEq(101.36154937744140625, 387.699462890625, -13.512401580810546875),
+ Vec3fEq(116.2598419189453125, 363.599273681640625, -17.359806060791015625),
+ Vec3fEq(131.1581268310546875, 339.499114990234375, -21.2072086334228515625),
+ Vec3fEq(146.056427001953125, 315.39892578125, -25.0546112060546875),
+ Vec3fEq(160.9547271728515625, 291.298736572265625, -28.9020137786865234375),
+ Vec3fEq(175.8530120849609375, 267.198577880859375, -32.749416351318359375),
+ Vec3fEq(190.751312255859375, 243.098388671875, -33.819454193115234375),
+ Vec3fEq(205.64959716796875, 218.9982147216796875, -31.020172119140625),
+ Vec3fEq(220.5478973388671875, 194.898040771484375, -26.844608306884765625),
+ Vec3fEq(235.446197509765625, 170.7978668212890625, -26.785541534423828125),
+ Vec3fEq(250.3444671630859375, 146.6976776123046875, -26.7264766693115234375),
+ Vec3fEq(265.242767333984375, 122.59751129150390625, -20.59339141845703125),
+ Vec3fEq(280.141021728515625, 98.4973297119140625, -14.040531158447265625),
+ Vec3fEq(295.039306640625, 74.39715576171875, -7.48766994476318359375),
+ Vec3fEq(306, 56.66666412353515625, -2.6667339801788330078125)
)) << mPath;
}
}
diff --git a/apps/openmw_test_suite/detournavigator/navmeshdb.cpp b/apps/openmw_test_suite/detournavigator/navmeshdb.cpp
new file mode 100644
index 0000000000..feadc2f59d
--- /dev/null
+++ b/apps/openmw_test_suite/detournavigator/navmeshdb.cpp
@@ -0,0 +1,112 @@
+#include "generate.hpp"
+
+#include <components/detournavigator/navmeshdb.hpp>
+#include <components/esm/cellid.hpp>
+
+#include <DetourAlloc.h>
+
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+
+#include <numeric>
+#include <random>
+
+namespace
+{
+ using namespace testing;
+ using namespace DetourNavigator;
+ using namespace DetourNavigator::Tests;
+
+ struct Tile
+ {
+ std::string mWorldspace;
+ TilePosition mTilePosition;
+ std::vector<std::byte> mInput;
+ std::vector<std::byte> mData;
+ };
+
+ struct DetourNavigatorNavMeshDbTest : Test
+ {
+ NavMeshDb mDb {":memory:"};
+ std::minstd_rand mRandom;
+
+ std::vector<std::byte> generateData()
+ {
+ std::vector<std::byte> data(32);
+ generateRange(data.begin(), data.end(), mRandom);
+ return data;
+ }
+
+ Tile insertTile(TileId tileId, TileVersion version)
+ {
+ std::string worldspace = "sys::default";
+ const TilePosition tilePosition {3, 4};
+ std::vector<std::byte> input = generateData();
+ std::vector<std::byte> data = generateData();
+ EXPECT_EQ(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), 1);
+ return {std::move(worldspace), tilePosition, std::move(input), std::move(data)};
+ }
+ };
+
+ TEST_F(DetourNavigatorNavMeshDbTest, get_max_tile_id_for_empty_db_should_return_zero)
+ {
+ EXPECT_EQ(mDb.getMaxTileId(), TileId {0});
+ }
+
+ TEST_F(DetourNavigatorNavMeshDbTest, inserted_tile_should_be_found_by_key)
+ {
+ const TileId tileId {146};
+ const TileVersion version {1};
+ const auto [worldspace, tilePosition, input, data] = insertTile(tileId, version);
+ const auto result = mDb.findTile(worldspace, tilePosition, input);
+ ASSERT_TRUE(result.has_value());
+ EXPECT_EQ(result->mTileId, tileId);
+ EXPECT_EQ(result->mVersion, version);
+ }
+
+ TEST_F(DetourNavigatorNavMeshDbTest, inserted_tile_should_change_max_tile_id)
+ {
+ insertTile(TileId {53}, TileVersion {1});
+ EXPECT_EQ(mDb.getMaxTileId(), TileId {53});
+ }
+
+ TEST_F(DetourNavigatorNavMeshDbTest, updated_tile_should_change_data)
+ {
+ const TileId tileId {13};
+ const TileVersion version {1};
+ auto [worldspace, tilePosition, input, data] = insertTile(tileId, version);
+ generateRange(data.begin(), data.end(), mRandom);
+ ASSERT_EQ(mDb.updateTile(tileId, version, data), 1);
+ const auto row = mDb.getTileData(worldspace, tilePosition, input);
+ ASSERT_TRUE(row.has_value());
+ EXPECT_EQ(row->mTileId, tileId);
+ EXPECT_EQ(row->mVersion, version);
+ ASSERT_FALSE(row->mData.empty());
+ EXPECT_EQ(row->mData, data);
+ }
+
+ TEST_F(DetourNavigatorNavMeshDbTest, on_inserted_duplicate_should_throw_exception)
+ {
+ const TileId tileId {53};
+ const TileVersion version {1};
+ const std::string worldspace = "sys::default";
+ const TilePosition tilePosition {3, 4};
+ const std::vector<std::byte> input = generateData();
+ const std::vector<std::byte> data = generateData();
+ ASSERT_EQ(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), 1);
+ EXPECT_THROW(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), std::runtime_error);
+ }
+
+ TEST_F(DetourNavigatorNavMeshDbTest, inserted_duplicate_leaves_db_in_correct_state)
+ {
+ const TileId tileId {53};
+ const TileVersion version {1};
+ const std::string worldspace = "sys::default";
+ const TilePosition tilePosition {3, 4};
+ const std::vector<std::byte> input = generateData();
+ const std::vector<std::byte> data = generateData();
+ ASSERT_EQ(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), 1);
+ EXPECT_THROW(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), std::runtime_error);
+ EXPECT_NO_THROW(insertTile(TileId {54}, version));
+ }
+}
diff --git a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp b/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp
index 309266142f..cbd68e0fe1 100644
--- a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp
+++ b/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp
@@ -1,4 +1,5 @@
#include "operators.hpp"
+#include "generate.hpp"
#include <components/detournavigator/navmeshtilescache.hpp>
#include <components/detournavigator/exceptions.hpp>
@@ -21,23 +22,17 @@ namespace
{
using namespace testing;
using namespace DetourNavigator;
+ using namespace DetourNavigator::Tests;
- void* permRecastAlloc(int size)
- {
- void* result = rcAlloc(static_cast<std::size_t>(size), RC_ALLOC_PERM);
- if (result == nullptr)
- throw std::bad_alloc();
- return result;
- }
-
- template <class T>
- void generate(T*& values, int size)
+ template <class T, class Random>
+ void generateRecastArray(T*& values, int size, Random& random)
{
values = static_cast<T*>(permRecastAlloc(size * sizeof(T)));
- std::generate_n(values, static_cast<std::size_t>(size), [] { return static_cast<T>(std::rand()); });
+ generateRange(values, values + static_cast<std::ptrdiff_t>(size), random);
}
- void generate(rcPolyMesh& value, int size)
+ template <class Random>
+ void generate(rcPolyMesh& value, int size, Random& random)
{
value.nverts = size;
value.maxpolys = size;
@@ -45,88 +40,49 @@ namespace
value.npolys = size;
rcVcopy(value.bmin, osg::Vec3f(-1, -2, -3).ptr());
rcVcopy(value.bmax, osg::Vec3f(3, 2, 1).ptr());
- value.cs = 1.0f / (std::rand() % 999 + 1);
- value.ch = 1.0f / (std::rand() % 999 + 1);
- value.borderSize = std::rand();
- value.maxEdgeError = 1.0f / (std::rand() % 999 + 1);
- generate(value.verts, getVertsLength(value));
- generate(value.polys, getPolysLength(value));
- generate(value.regs, getRegsLength(value));
- generate(value.flags, getFlagsLength(value));
- generate(value.areas, getAreasLength(value));
+ generateValue(value.cs, random);
+ generateValue(value.ch, random);
+ generateValue(value.borderSize, random);
+ generateValue(value.maxEdgeError, random);
+ generateRecastArray(value.verts, getVertsLength(value), random);
+ generateRecastArray(value.polys, getPolysLength(value), random);
+ generateRecastArray(value.regs, getRegsLength(value), random);
+ generateRecastArray(value.flags, getFlagsLength(value), random);
+ generateRecastArray(value.areas, getAreasLength(value), random);
}
- void generate(rcPolyMeshDetail& value, int size)
+ template <class Random>
+ void generate(rcPolyMeshDetail& value, int size, Random& random)
{
value.nmeshes = size;
value.nverts = size;
value.ntris = size;
- generate(value.meshes, getMeshesLength(value));
- generate(value.verts, getVertsLength(value));
- generate(value.tris, getTrisLength(value));
+ generateRecastArray(value.meshes, getMeshesLength(value), random);
+ generateRecastArray(value.verts, getVertsLength(value), random);
+ generateRecastArray(value.tris, getTrisLength(value), random);
}
- void generate(PreparedNavMeshData& value, int size)
+ template <class Random>
+ void generate(PreparedNavMeshData& value, int size, Random& random)
{
- value.mUserId = std::rand();
- value.mCellHeight = 1.0f / (std::rand() % 999 + 1);
- value.mCellSize = 1.0f / (std::rand() % 999 + 1);
- generate(value.mPolyMesh, size);
- generate(value.mPolyMeshDetail, size);
+ generateValue(value.mUserId, random);
+ generateValue(value.mCellHeight, random);
+ generateValue(value.mCellSize, random);
+ generate(value.mPolyMesh, size, random);
+ generate(value.mPolyMeshDetail, size, random);
}
std::unique_ptr<PreparedNavMeshData> makePeparedNavMeshData(int size)
{
+ std::minstd_rand random;
auto result = std::make_unique<PreparedNavMeshData>();
- generate(*result, size);
+ generate(*result, size, random);
return result;
}
- template <class T>
- void clone(const T* src, T*& dst, std::size_t size)
- {
- dst = static_cast<T*>(permRecastAlloc(static_cast<int>(size) * sizeof(T)));
- std::memcpy(dst, src, size * sizeof(T));
- }
-
- void clone(const rcPolyMesh& src, rcPolyMesh& dst)
- {
- dst.nverts = src.nverts;
- dst.npolys = src.npolys;
- dst.maxpolys = src.maxpolys;
- dst.nvp = src.nvp;
- rcVcopy(dst.bmin, src.bmin);
- rcVcopy(dst.bmax, src.bmax);
- dst.cs = src.cs;
- dst.ch = src.ch;
- dst.borderSize = src.borderSize;
- dst.maxEdgeError = src.maxEdgeError;
- clone(src.verts, dst.verts, getVertsLength(dst));
- clone(src.polys, dst.polys, getPolysLength(dst));
- clone(src.regs, dst.regs, getRegsLength(dst));
- clone(src.flags, dst.flags, getFlagsLength(dst));
- clone(src.areas, dst.areas, getAreasLength(dst));
- }
-
- void clone(const rcPolyMeshDetail& src, rcPolyMeshDetail& dst)
- {
- dst.nmeshes = src.nmeshes;
- dst.nverts = src.nverts;
- dst.ntris = src.ntris;
- clone(src.meshes, dst.meshes, getMeshesLength(dst));
- clone(src.verts, dst.verts, getVertsLength(dst));
- clone(src.tris, dst.tris, getTrisLength(dst));
- }
-
std::unique_ptr<PreparedNavMeshData> clone(const PreparedNavMeshData& value)
{
- auto result = std::make_unique<PreparedNavMeshData>();
- result->mUserId = value.mUserId;
- result->mCellHeight = value.mCellHeight;
- result->mCellSize = value.mCellSize;
- clone(value.mPolyMesh, result->mPolyMesh);
- clone(value.mPolyMeshDetail, result->mPolyMeshDetail);
- return result;
+ return std::make_unique<PreparedNavMeshData>(value);
}
Mesh makeMesh()
@@ -144,14 +100,15 @@ namespace
const std::size_t mGeneration = 0;
const std::size_t mRevision = 0;
const Mesh mMesh {makeMesh()};
- const std::vector<Cell> mWater {};
+ const std::vector<CellWater> mWater {};
const std::vector<Heightfield> mHeightfields {};
const std::vector<FlatHeightfield> mFlatHeightfields {};
- const RecastMesh mRecastMesh {mGeneration, mRevision, mMesh, mWater, mHeightfields, mFlatHeightfields};
+ const std::vector<MeshSource> mSources {};
+ const RecastMesh mRecastMesh {mGeneration, mRevision, mMesh, mWater, mHeightfields, mFlatHeightfields, mSources};
std::unique_ptr<PreparedNavMeshData> mPreparedNavMeshData {makePeparedNavMeshData(3)};
const std::size_t mRecastMeshSize = sizeof(mRecastMesh) + getSize(mRecastMesh);
- const std::size_t mRecastMeshWithWaterSize = mRecastMeshSize + sizeof(Cell);
+ const std::size_t mRecastMeshWithWaterSize = mRecastMeshSize + sizeof(CellWater);
const std::size_t mPreparedNavMeshDataSize = sizeof(*mPreparedNavMeshData) + getSize(*mPreparedNavMeshData);
};
@@ -234,8 +191,8 @@ namespace
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
- const std::vector<Cell> water {1, Cell {1, osg::Vec3f()}};
- const RecastMesh unexistentRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields};
+ const std::vector<CellWater> water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}});
+ const RecastMesh unexistentRecastMesh(mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields, mSources);
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData));
EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, unexistentRecastMesh));
@@ -246,8 +203,8 @@ namespace
const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize;
NavMeshTilesCache cache(maxSize);
- const std::vector<Cell> water {1, Cell {1, osg::Vec3f()}};
- const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields};
+ const std::vector<CellWater> water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}});
+ const RecastMesh anotherRecastMesh(mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields, mSources);
auto anotherPreparedNavMeshData = makePeparedNavMeshData(3);
const auto copy = clone(*anotherPreparedNavMeshData);
@@ -264,8 +221,8 @@ namespace
const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize;
NavMeshTilesCache cache(maxSize);
- const std::vector<Cell> water {1, Cell {1, osg::Vec3f()}};
- const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields};
+ const std::vector<CellWater> water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}});
+ const RecastMesh anotherRecastMesh(mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields, mSources);
auto anotherPreparedNavMeshData = makePeparedNavMeshData(3);
const auto value = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh,
@@ -280,14 +237,14 @@ namespace
NavMeshTilesCache cache(maxSize);
const auto copy = clone(*mPreparedNavMeshData);
- const std::vector<Cell> leastRecentlySetWater {1, Cell {1, osg::Vec3f()}};
- const RecastMesh leastRecentlySetRecastMesh {mGeneration, mRevision, mMesh, leastRecentlySetWater,
- mHeightfields, mFlatHeightfields};
+ const std::vector<CellWater> leastRecentlySetWater(1, CellWater {osg::Vec2i(), Water {1, 0.0f}});
+ const RecastMesh leastRecentlySetRecastMesh(mGeneration, mRevision, mMesh, leastRecentlySetWater,
+ mHeightfields, mFlatHeightfields, mSources);
auto leastRecentlySetData = makePeparedNavMeshData(3);
- const std::vector<Cell> mostRecentlySetWater {1, Cell {2, osg::Vec3f()}};
- const RecastMesh mostRecentlySetRecastMesh {mGeneration, mRevision, mMesh, mostRecentlySetWater,
- mHeightfields, mFlatHeightfields};
+ const std::vector<CellWater> mostRecentlySetWater(1, CellWater {osg::Vec2i(), Water {2, 0.0f}});
+ const RecastMesh mostRecentlySetRecastMesh(mGeneration, mRevision, mMesh, mostRecentlySetWater,
+ mHeightfields, mFlatHeightfields, mSources);
auto mostRecentlySetData = makePeparedNavMeshData(3);
ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, leastRecentlySetRecastMesh,
@@ -308,15 +265,15 @@ namespace
const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize);
NavMeshTilesCache cache(maxSize);
- const std::vector<Cell> leastRecentlyUsedWater {1, Cell {1, osg::Vec3f()}};
- const RecastMesh leastRecentlyUsedRecastMesh {mGeneration, mRevision, mMesh, leastRecentlyUsedWater,
- mHeightfields, mFlatHeightfields};
+ const std::vector<CellWater> leastRecentlyUsedWater(1, CellWater {osg::Vec2i(), Water {1, 0.0f}});
+ const RecastMesh leastRecentlyUsedRecastMesh(mGeneration, mRevision, mMesh, leastRecentlyUsedWater,
+ mHeightfields, mFlatHeightfields, mSources);
auto leastRecentlyUsedData = makePeparedNavMeshData(3);
const auto leastRecentlyUsedCopy = clone(*leastRecentlyUsedData);
- const std::vector<Cell> mostRecentlyUsedWater {1, Cell {2, osg::Vec3f()}};
- const RecastMesh mostRecentlyUsedRecastMesh {mGeneration, mRevision, mMesh, mostRecentlyUsedWater,
- mHeightfields, mFlatHeightfields};
+ const std::vector<CellWater> mostRecentlyUsedWater(1, CellWater {osg::Vec2i(), Water {2, 0.0f}});
+ const RecastMesh mostRecentlyUsedRecastMesh(mGeneration, mRevision, mMesh, mostRecentlyUsedWater,
+ mHeightfields, mFlatHeightfields, mSources);
auto mostRecentlyUsedData = makePeparedNavMeshData(3);
const auto mostRecentlyUsedCopy = clone(*mostRecentlyUsedData);
@@ -349,9 +306,9 @@ namespace
const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize);
NavMeshTilesCache cache(maxSize);
- const std::vector<Cell> water {1, Cell {1, osg::Vec3f()}};
- const RecastMesh tooLargeRecastMesh {mGeneration, mRevision, mMesh, water,
- mHeightfields, mFlatHeightfields};
+ const std::vector<CellWater> water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}});
+ const RecastMesh tooLargeRecastMesh(mGeneration, mRevision, mMesh, water,
+ mHeightfields, mFlatHeightfields, mSources);
auto tooLargeData = makePeparedNavMeshData(10);
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData));
@@ -364,14 +321,14 @@ namespace
const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize);
NavMeshTilesCache cache(maxSize);
- const std::vector<Cell> anotherWater {1, Cell {1, osg::Vec3f()}};
- const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, anotherWater,
- mHeightfields, mFlatHeightfields};
+ const std::vector<CellWater> anotherWater(1, CellWater {osg::Vec2i(), Water {1, 0.0f}});
+ const RecastMesh anotherRecastMesh(mGeneration, mRevision, mMesh, anotherWater,
+ mHeightfields, mFlatHeightfields, mSources);
auto anotherData = makePeparedNavMeshData(3);
- const std::vector<Cell> tooLargeWater {1, Cell {2, osg::Vec3f()}};
- const RecastMesh tooLargeRecastMesh {mGeneration, mRevision, mMesh, tooLargeWater,
- mHeightfields, mFlatHeightfields};
+ const std::vector<CellWater> tooLargeWater(1, CellWater {osg::Vec2i(), Water {2, 0.0f}});
+ const RecastMesh tooLargeRecastMesh(mGeneration, mRevision, mMesh, tooLargeWater,
+ mHeightfields, mFlatHeightfields, mSources);
auto tooLargeData = makePeparedNavMeshData(10);
const auto value = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh,
@@ -390,8 +347,8 @@ namespace
const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize;
NavMeshTilesCache cache(maxSize);
- const std::vector<Cell> water {1, Cell {1, osg::Vec3f()}};
- const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields};
+ const std::vector<CellWater> water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}});
+ const RecastMesh anotherRecastMesh(mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields, mSources);
auto anotherData = makePeparedNavMeshData(3);
const auto firstCopy = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData));
@@ -409,8 +366,8 @@ namespace
const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize;
NavMeshTilesCache cache(maxSize);
- const std::vector<Cell> water {1, Cell {1, osg::Vec3f()}};
- const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields};
+ const std::vector<CellWater> water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}});
+ const RecastMesh anotherRecastMesh(mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields, mSources);
auto anotherData = makePeparedNavMeshData(3);
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData));
diff --git a/apps/openmw_test_suite/detournavigator/operators.hpp b/apps/openmw_test_suite/detournavigator/operators.hpp
index 92740c65f1..fb6fcc5c39 100644
--- a/apps/openmw_test_suite/detournavigator/operators.hpp
+++ b/apps/openmw_test_suite/detournavigator/operators.hpp
@@ -48,7 +48,7 @@ namespace testing
template <>
inline testing::Message& Message::operator <<(const osg::Vec3f& value)
{
- return (*this) << "osg::Vec3f(" << std::setprecision(std::numeric_limits<float>::max_exponent10) << value.x()
+ return (*this) << "Vec3fEq(" << std::setprecision(std::numeric_limits<float>::max_exponent10) << value.x()
<< ", " << std::setprecision(std::numeric_limits<float>::max_exponent10) << value.y()
<< ", " << std::setprecision(std::numeric_limits<float>::max_exponent10) << value.z()
<< ')';
diff --git a/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp b/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp
index 73d86bd6ee..36e0287461 100644
--- a/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp
+++ b/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp
@@ -23,38 +23,30 @@
namespace DetourNavigator
{
- static inline bool operator ==(const Cell& lhs, const Cell& rhs)
+ static inline bool operator ==(const Water& lhs, const Water& rhs)
{
- return lhs.mSize == rhs.mSize && lhs.mShift == rhs.mShift;
+ const auto tie = [] (const Water& v) { return std::tie(v.mCellSize, v.mLevel); };
+ return tie(lhs) == tie(rhs);
}
- static inline bool operator==(const Heightfield& lhs, const Heightfield& rhs)
+ static inline bool operator ==(const CellWater& lhs, const CellWater& rhs)
{
- return makeTuple(lhs) == makeTuple(rhs);
+ const auto tie = [] (const CellWater& v) { return std::tie(v.mCellPosition, v.mWater); };
+ return tie(lhs) == tie(rhs);
}
- static inline bool operator==(const FlatHeightfield& lhs, const FlatHeightfield& rhs)
- {
- return std::tie(lhs.mBounds, lhs.mHeight) == std::tie(rhs.mBounds, rhs.mHeight);
- }
-
- static inline std::ostream& operator<<(std::ostream& s, const FlatHeightfield& v)
+ static inline bool operator==(const Heightfield& lhs, const Heightfield& rhs)
{
- return s << "FlatHeightfield {" << v.mBounds << ", " << v.mHeight << "}";
+ return makeTuple(lhs) == makeTuple(rhs);
}
- static inline std::ostream& operator<<(std::ostream& s, const Heightfield& v)
+ static inline bool operator==(const FlatHeightfield& lhs, const FlatHeightfield& rhs)
{
- s << "Heightfield {.mBounds=" << v.mBounds
- << ", .mLength=" << int(v.mLength)
- << ", .mMinHeight=" << v.mMinHeight
- << ", .mMaxHeight=" << v.mMaxHeight
- << ", .mShift=" << v.mShift
- << ", .mScale=" << v.mScale
- << ", .mHeights={";
- for (float h : v.mHeights)
- s << h << ", ";
- return s << "}}";
+ const auto tie = [] (const FlatHeightfield& v)
+ {
+ return std::tie(v.mCellPosition, v.mCellSize, v.mHeight);
+ };
+ return tie(lhs) == tie(rhs);
}
}
@@ -68,6 +60,8 @@ namespace
TileBounds mBounds;
const std::size_t mGeneration = 0;
const std::size_t mRevision = 0;
+ const osg::ref_ptr<const Resource::BulletShape> mSource {nullptr};
+ const ObjectTransform mObjectTransform {ESM::Position {{0, 0, 0}, {0, 0, 0}}, 0.0f};
DetourNavigatorRecastMeshBuilderTest()
{
@@ -94,7 +88,8 @@ namespace
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mBounds);
- builder.addObject(static_cast<const btCollisionShape&>(shape), btTransform::getIdentity(), AreaType_ground);
+ builder.addObject(static_cast<const btCollisionShape&>(shape), btTransform::getIdentity(),
+ AreaType_ground, mSource, mObjectTransform);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector<float>({
-1, -1, 0,
@@ -114,7 +109,7 @@ namespace
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)),
- AreaType_ground
+ AreaType_ground, mSource, mObjectTransform
);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector<float>({
@@ -131,7 +126,8 @@ namespace
const std::array<btScalar, 4> heightfieldData {{0, 0, 0, 0}};
btHeightfieldTerrainShape shape(2, 2, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
RecastMeshBuilder builder(mBounds);
- builder.addObject(static_cast<const btCollisionShape&>(shape), btTransform::getIdentity(), AreaType_ground);
+ builder.addObject(static_cast<const btCollisionShape&>(shape), btTransform::getIdentity(),
+ AreaType_ground, mSource, mObjectTransform);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector<float>({
-0.5, -0.5, 0,
@@ -147,7 +143,8 @@ namespace
{
btBoxShape shape(btVector3(1, 1, 2));
RecastMeshBuilder builder(mBounds);
- builder.addObject(static_cast<const btCollisionShape&>(shape), btTransform::getIdentity(), AreaType_ground);
+ builder.addObject(static_cast<const btCollisionShape&>(shape), btTransform::getIdentity(),
+ AreaType_ground, mSource, mObjectTransform);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector<float>({
-1, -1, -2,
@@ -193,7 +190,7 @@ namespace
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform::getIdentity(),
- AreaType_ground
+ AreaType_ground, mSource, mObjectTransform
);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector<float>({
@@ -240,7 +237,7 @@ namespace
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)),
- AreaType_ground
+ AreaType_ground, mSource, mObjectTransform
);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector<float>({
@@ -264,7 +261,7 @@ namespace
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)),
- AreaType_ground
+ AreaType_ground, mSource, mObjectTransform
);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector<float>({
@@ -286,7 +283,7 @@ namespace
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform::getIdentity(),
- AreaType_ground
+ AreaType_ground, mSource, mObjectTransform
);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector<float>({
@@ -313,7 +310,7 @@ namespace
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform::getIdentity(),
- AreaType_ground
+ AreaType_ground, mSource, mObjectTransform
);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector<float>({
@@ -338,7 +335,7 @@ namespace
static_cast<const btCollisionShape&>(shape),
btTransform(btQuaternion(btVector3(1, 0, 0),
static_cast<btScalar>(-osg::PI_4))),
- AreaType_ground
+ AreaType_ground, mSource, mObjectTransform
);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_THAT(recastMesh->getMesh().getVertices(), Pointwise(FloatNear(1e-5), std::vector<float>({
@@ -363,7 +360,7 @@ namespace
static_cast<const btCollisionShape&>(shape),
btTransform(btQuaternion(btVector3(0, 1, 0),
static_cast<btScalar>(osg::PI_4))),
- AreaType_ground
+ AreaType_ground, mSource, mObjectTransform
);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_THAT(recastMesh->getMesh().getVertices(), Pointwise(FloatNear(1e-5), std::vector<float>({
@@ -388,7 +385,7 @@ namespace
static_cast<const btCollisionShape&>(shape),
btTransform(btQuaternion(btVector3(0, 0, 1),
static_cast<btScalar>(osg::PI_4))),
- AreaType_ground
+ AreaType_ground, mSource, mObjectTransform
);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_THAT(recastMesh->getMesh().getVertices(), Pointwise(FloatNear(1e-5), std::vector<float>({
@@ -412,12 +409,12 @@ namespace
builder.addObject(
static_cast<const btCollisionShape&>(shape1),
btTransform::getIdentity(),
- AreaType_ground
+ AreaType_ground, mSource, mObjectTransform
);
builder.addObject(
static_cast<const btCollisionShape&>(shape2),
btTransform::getIdentity(),
- AreaType_null
+ AreaType_null, mSource, mObjectTransform
);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector<float>({
@@ -435,10 +432,10 @@ namespace
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_water_then_get_water_should_return_it)
{
RecastMeshBuilder builder(mBounds);
- builder.addWater(1000, osg::Vec3f(100, 200, 300));
+ builder.addWater(osg::Vec2i(1, 2), Water {1000, 300.0f});
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
- EXPECT_EQ(recastMesh->getWater(), std::vector<Cell>({
- Cell {1000, osg::Vec3f(100, 200, 300)}
+ EXPECT_EQ(recastMesh->getWater(), std::vector<CellWater>({
+ CellWater {osg::Vec2i(1, 2), Water {1000, 300.0f}}
}));
}
@@ -450,7 +447,7 @@ namespace
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mBounds);
- builder.addObject(static_cast<const btCollisionShape&>(shape), btTransform::getIdentity(), AreaType_ground);
+ builder.addObject(static_cast<const btCollisionShape&>(shape), btTransform::getIdentity(), AreaType_ground, mSource, mObjectTransform);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector<float>({
-1, -1, 0,
@@ -464,62 +461,111 @@ namespace
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_flat_heightfield_should_add_intersection)
{
- mBounds.mMin = osg::Vec2f(0, 0);
+ const osg::Vec2i cellPosition(0, 0);
+ const int cellSize = 1000;
+ const float height = 10;
+ mBounds.mMin = osg::Vec2f(100, 100);
RecastMeshBuilder builder(mBounds);
- builder.addHeightfield(1000, osg::Vec3f(1, 2, 3), 10);
+ builder.addHeightfield(cellPosition, cellSize, height);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
EXPECT_EQ(recastMesh->getFlatHeightfields(), std::vector<FlatHeightfield>({
- FlatHeightfield {TileBounds {osg::Vec2f(0, 0), osg::Vec2f(501, 502)}, 13},
+ FlatHeightfield {cellPosition, cellSize, height},
}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_inside_tile)
{
- constexpr std::array<float, 9> heights {{
+ constexpr std::size_t size = 3;
+ constexpr std::array<float, size * size> heights {{
0, 1, 2,
3, 4, 5,
6, 7, 8,
}};
+ const osg::Vec2i cellPosition(0, 0);
+ const int cellSize = 1000;
+ const float minHeight = 0;
+ const float maxHeight = 8;
RecastMeshBuilder builder(mBounds);
- builder.addHeightfield(1000, osg::Vec3f(1, 2, 3), heights.data(), 3, 0, 8);
+ builder.addHeightfield(cellPosition, cellSize, heights.data(), size, minHeight, maxHeight);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
Heightfield expected;
- expected.mBounds = TileBounds {osg::Vec2f(-499, -498), osg::Vec2f(501, 502)};
- expected.mLength = 3;
- expected.mMinHeight = 0;
- expected.mMaxHeight = 8;
- expected.mShift = osg::Vec3f(-499, -498, 3);
- expected.mScale = 500;
+ expected.mCellPosition = cellPosition;
+ expected.mCellSize = cellSize;
+ expected.mLength = size;
+ expected.mMinHeight = minHeight;
+ expected.mMaxHeight = maxHeight;
+ expected.mHeights = {
+ 0, 1, 2,
+ 3, 4, 5,
+ 6, 7, 8,
+ };
+ expected.mOriginalSize = 3;
+ expected.mMinX = 0;
+ expected.mMinY = 0;
+ EXPECT_EQ(recastMesh->getHeightfields(), std::vector<Heightfield>({expected}));
+ }
+
+ TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_to_shifted_cell_inside_tile)
+ {
+ constexpr std::size_t size = 3;
+ constexpr std::array<float, size * size> heights {{
+ 0, 1, 2,
+ 3, 4, 5,
+ 6, 7, 8,
+ }};
+ const osg::Vec2i cellPosition(1, 2);
+ const int cellSize = 1000;
+ const float minHeight = 0;
+ const float maxHeight = 8;
+ RecastMeshBuilder builder(maxCellTileBounds(cellPosition, cellSize));
+ builder.addHeightfield(cellPosition, cellSize, heights.data(), size, minHeight, maxHeight);
+ const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
+ Heightfield expected;
+ expected.mCellPosition = cellPosition;
+ expected.mCellSize = cellSize;
+ expected.mLength = size;
+ expected.mMinHeight = minHeight;
+ expected.mMaxHeight = maxHeight;
expected.mHeights = {
0, 1, 2,
3, 4, 5,
6, 7, 8,
};
+ expected.mOriginalSize = 3;
+ expected.mMinX = 0;
+ expected.mMinY = 0;
EXPECT_EQ(recastMesh->getHeightfields(), std::vector<Heightfield>({expected}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_should_add_intersection)
{
- constexpr std::array<float, 9> heights {{
+ constexpr std::size_t size = 3;
+ constexpr std::array<float, 3 * 3> heights {{
0, 1, 2,
3, 4, 5,
6, 7, 8,
}};
- mBounds.mMin = osg::Vec2f(250, 250);
+ const osg::Vec2i cellPosition(0, 0);
+ const int cellSize = 1000;
+ const float minHeight = 0;
+ const float maxHeight = 8;
+ mBounds.mMin = osg::Vec2f(750, 750);
RecastMeshBuilder builder(mBounds);
- builder.addHeightfield(1000, osg::Vec3f(-1, -2, 3), heights.data(), 3, 0, 8);
+ builder.addHeightfield(cellPosition, cellSize, heights.data(), size, minHeight, maxHeight);
const auto recastMesh = std::move(builder).create(mGeneration, mRevision);
Heightfield expected;
- expected.mBounds = TileBounds {osg::Vec2f(250, 250), osg::Vec2f(499, 498)};
+ expected.mCellPosition = cellPosition;
+ expected.mCellSize = cellSize;
expected.mLength = 2;
expected.mMinHeight = 0;
expected.mMaxHeight = 8;
- expected.mShift = osg::Vec3f(-1, -2, 3);
- expected.mScale = 500;
expected.mHeights = {
4, 5,
7, 8,
};
+ expected.mOriginalSize = 3;
+ expected.mMinX = 1;
+ expected.mMinY = 1;
EXPECT_EQ(recastMesh->getHeightfields(), std::vector<Heightfield>({expected}));
}
}
diff --git a/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp b/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp
index 7751d5220c..ff0d3e519c 100644
--- a/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp
+++ b/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp
@@ -1,6 +1,7 @@
#include "operators.hpp"
#include <components/detournavigator/recastmeshobject.hpp>
+#include <components/misc/convert.hpp>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
@@ -15,10 +16,11 @@ namespace
struct DetourNavigatorRecastMeshObjectTest : Test
{
btBoxShape mBoxShapeImpl {btVector3(1, 2, 3)};
- CollisionShape mBoxShape {nullptr, mBoxShapeImpl};
+ const ObjectTransform mObjectTransform {ESM::Position {{1, 2, 3}, {1, 2, 3}}, 0.5f};
+ CollisionShape mBoxShape {nullptr, mBoxShapeImpl, mObjectTransform};
btCompoundShape mCompoundShapeImpl {true};
- CollisionShape mCompoundShape {nullptr, mCompoundShapeImpl};
- btTransform mTransform {btQuaternion(btVector3(1, 2, 3), 1), btVector3(1, 2, 3)};
+ CollisionShape mCompoundShape {nullptr, mCompoundShapeImpl, mObjectTransform};
+ btTransform mTransform {Misc::Convert::makeBulletTransform(mObjectTransform.mPosition)};
DetourNavigatorRecastMeshObjectTest()
{
diff --git a/apps/openmw_test_suite/detournavigator/settings.hpp b/apps/openmw_test_suite/detournavigator/settings.hpp
new file mode 100644
index 0000000000..dc37dc7550
--- /dev/null
+++ b/apps/openmw_test_suite/detournavigator/settings.hpp
@@ -0,0 +1,50 @@
+#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_SETTINGS_H
+#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_SETTINGS_H
+
+#include <components/detournavigator/settings.hpp>
+
+#include <chrono>
+#include <limits>
+
+namespace DetourNavigator
+{
+ namespace Tests
+ {
+ inline Settings makeSettings()
+ {
+ Settings result;
+ result.mEnableWriteRecastMeshToFile = false;
+ result.mEnableWriteNavMeshToFile = false;
+ result.mEnableRecastMeshFileNameRevision = false;
+ result.mEnableNavMeshFileNameRevision = false;
+ result.mRecast.mBorderSize = 16;
+ result.mRecast.mCellHeight = 0.2f;
+ result.mRecast.mCellSize = 0.2f;
+ result.mRecast.mDetailSampleDist = 6;
+ result.mRecast.mDetailSampleMaxError = 1;
+ result.mRecast.mMaxClimb = 34;
+ result.mRecast.mMaxSimplificationError = 1.3f;
+ result.mRecast.mMaxSlope = 49;
+ result.mRecast.mRecastScaleFactor = 0.017647058823529415f;
+ result.mRecast.mSwimHeightScale = 0.89999997615814208984375f;
+ result.mRecast.mMaxEdgeLen = 12;
+ result.mDetour.mMaxNavMeshQueryNodes = 2048;
+ result.mRecast.mMaxVertsPerPoly = 6;
+ result.mRecast.mRegionMergeArea = 400;
+ result.mRecast.mRegionMinArea = 64;
+ result.mRecast.mTileSize = 64;
+ result.mWaitUntilMinDistanceToPlayer = std::numeric_limits<int>::max();
+ result.mAsyncNavMeshUpdaterThreads = 1;
+ result.mMaxNavMeshTilesCacheSize = 1024 * 1024;
+ result.mDetour.mMaxPolygonPathSize = 1024;
+ result.mDetour.mMaxSmoothPathSize = 1024;
+ result.mDetour.mMaxPolys = 4096;
+ result.mMaxTilesNumber = 512;
+ result.mMinUpdateInterval = std::chrono::milliseconds(50);
+ result.mWriteToNavMeshDb = true;
+ return result;
+ }
+ }
+}
+
+#endif
diff --git a/apps/openmw_test_suite/detournavigator/settingsutils.cpp b/apps/openmw_test_suite/detournavigator/settingsutils.cpp
index ffed64ab81..f06f3b3e32 100644
--- a/apps/openmw_test_suite/detournavigator/settingsutils.cpp
+++ b/apps/openmw_test_suite/detournavigator/settingsutils.cpp
@@ -11,7 +11,7 @@ namespace
struct DetourNavigatorGetTilePositionTest : Test
{
- Settings mSettings;
+ RecastSettings mSettings;
DetourNavigatorGetTilePositionTest()
{
@@ -47,7 +47,7 @@ namespace
struct DetourNavigatorMakeTileBoundsTest : Test
{
- Settings mSettings;
+ RecastSettings mSettings;
DetourNavigatorMakeTileBoundsTest()
{
diff --git a/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp b/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp
index 6209ec9c2a..c44ebc5155 100644
--- a/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp
+++ b/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp
@@ -15,8 +15,11 @@ namespace
struct DetourNavigatorTileCachedRecastMeshManagerTest : Test
{
- Settings mSettings;
+ RecastSettings mSettings;
std::vector<TilePosition> mChangedTiles;
+ const ObjectTransform mObjectTransform {ESM::Position {{0, 0, 0}, {0, 0, 0}}, 0.0f};
+ const osg::ref_ptr<const Resource::BulletShape> mShape = new Resource::BulletShape;
+ const osg::ref_ptr<const Resource::BulletShapeInstance> mInstance = new Resource::BulletShapeInstance(mShape);
DetourNavigatorTileCachedRecastMeshManagerTest()
{
@@ -35,7 +38,7 @@ namespace
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_empty_should_return_nullptr)
{
TileCachedRecastMeshManager manager(mSettings);
- EXPECT_EQ(manager.getMesh(TilePosition(0, 0)), nullptr);
+ EXPECT_EQ(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_revision_for_empty_should_return_zero)
@@ -56,7 +59,7 @@ namespace
{
TileCachedRecastMeshManager manager(mSettings);
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
EXPECT_TRUE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground));
}
@@ -64,7 +67,7 @@ namespace
{
TileCachedRecastMeshManager manager(mSettings);
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground);
EXPECT_FALSE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground));
}
@@ -72,12 +75,13 @@ namespace
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_object_should_add_tiles)
{
TileCachedRecastMeshManager manager(mSettings);
+ manager.setWorldspace("worldspace");
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
ASSERT_TRUE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground));
for (int x = -1; x < 1; ++x)
for (int y = -1; y < 1; ++y)
- ASSERT_NE(manager.getMesh(TilePosition(x, y)), nullptr);
+ ASSERT_NE(manager.getMesh("worldspace", TilePosition(x, y)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, update_object_for_changed_object_should_return_changed_tiles)
@@ -85,7 +89,7 @@ namespace
TileCachedRecastMeshManager manager(mSettings);
const btBoxShape boxShape(btVector3(20, 20, 100));
const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground);
EXPECT_TRUE(manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground,
[&] (const auto& v) { onChangedTile(v); }));
@@ -100,7 +104,7 @@ namespace
{
TileCachedRecastMeshManager manager(mSettings);
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground);
EXPECT_FALSE(manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground,
[&] (const auto& v) { onChangedTile(v); }));
@@ -110,90 +114,98 @@ namespace
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_after_add_object_should_return_recast_mesh_for_each_used_tile)
{
TileCachedRecastMeshManager manager(mSettings);
+ manager.setWorldspace("worldspace");
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground);
- EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(0, 0)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, -1)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_after_add_object_should_return_nullptr_for_unused_tile)
{
TileCachedRecastMeshManager manager(mSettings);
+ manager.setWorldspace("worldspace");
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground);
- EXPECT_EQ(manager.getMesh(TilePosition(1, 0)), nullptr);
+ EXPECT_EQ(manager.getMesh("worldspace", TilePosition(1, 0)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_moved_object_should_return_recast_mesh_for_each_used_tile)
{
TileCachedRecastMeshManager manager(mSettings);
+ manager.setWorldspace("worldspace");
+
const btBoxShape boxShape(btVector3(20, 20, 100));
const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground);
- EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(0, 0)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(1, 0)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(1, -1)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(1, 0)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(1, -1)), nullptr);
manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {});
- EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(0, 0)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, -1)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_moved_object_should_return_nullptr_for_unused_tile)
{
TileCachedRecastMeshManager manager(mSettings);
+ manager.setWorldspace("worldspace");
+
const btBoxShape boxShape(btVector3(20, 20, 100));
const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground);
- EXPECT_EQ(manager.getMesh(TilePosition(-1, -1)), nullptr);
- EXPECT_EQ(manager.getMesh(TilePosition(-1, 0)), nullptr);
+ EXPECT_EQ(manager.getMesh("worldspace", TilePosition(-1, -1)), nullptr);
+ EXPECT_EQ(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr);
manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {});
- EXPECT_EQ(manager.getMesh(TilePosition(1, 0)), nullptr);
- EXPECT_EQ(manager.getMesh(TilePosition(1, -1)), nullptr);
+ EXPECT_EQ(manager.getMesh("worldspace", TilePosition(1, 0)), nullptr);
+ EXPECT_EQ(manager.getMesh("worldspace", TilePosition(1, -1)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_removed_object_should_return_nullptr_for_all_previously_used_tiles)
{
TileCachedRecastMeshManager manager(mSettings);
+ manager.setWorldspace("worldspace");
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground);
manager.removeObject(ObjectId(&boxShape));
- EXPECT_EQ(manager.getMesh(TilePosition(-1, -1)), nullptr);
- EXPECT_EQ(manager.getMesh(TilePosition(-1, 0)), nullptr);
- EXPECT_EQ(manager.getMesh(TilePosition(0, -1)), nullptr);
- EXPECT_EQ(manager.getMesh(TilePosition(0, 0)), nullptr);
+ EXPECT_EQ(manager.getMesh("worldspace", TilePosition(-1, -1)), nullptr);
+ EXPECT_EQ(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr);
+ EXPECT_EQ(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr);
+ EXPECT_EQ(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_not_changed_object_after_update_should_return_recast_mesh_for_same_tiles)
{
TileCachedRecastMeshManager manager(mSettings);
+ manager.setWorldspace("worldspace");
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground);
- EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(0, 0)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, -1)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr);
manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {});
- EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr);
- EXPECT_NE(manager.getMesh(TilePosition(0, 0)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, -1)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr);
+ EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_revision_after_add_object_new_should_return_incremented_value)
@@ -201,7 +213,7 @@ namespace
TileCachedRecastMeshManager manager(mSettings);
const auto initialRevision = manager.getRevision();
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground);
EXPECT_EQ(manager.getRevision(), initialRevision + 1);
}
@@ -210,7 +222,7 @@ namespace
{
TileCachedRecastMeshManager manager(mSettings);
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground);
const auto beforeAddRevision = manager.getRevision();
manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground);
@@ -222,7 +234,7 @@ namespace
TileCachedRecastMeshManager manager(mSettings);
const btBoxShape boxShape(btVector3(20, 20, 100));
const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground);
const auto beforeUpdateRevision = manager.getRevision();
manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {});
@@ -232,8 +244,9 @@ namespace
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_revision_after_update_not_changed_object_should_return_same_value)
{
TileCachedRecastMeshManager manager(mSettings);
+ manager.setWorldspace("worldspace");
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground);
const auto beforeUpdateRevision = manager.getRevision();
manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {});
@@ -244,7 +257,7 @@ namespace
{
TileCachedRecastMeshManager manager(mSettings);
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground);
const auto beforeRemoveRevision = manager.getRevision();
manager.removeObject(ObjectId(&boxShape));
@@ -264,32 +277,34 @@ namespace
TileCachedRecastMeshManager manager(mSettings);
const osg::Vec2i cellPosition(0, 0);
const int cellSize = 8192;
- EXPECT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f()));
+ EXPECT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f));
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_water_for_not_max_int_should_add_new_tiles)
{
TileCachedRecastMeshManager manager(mSettings);
+ manager.setWorldspace("worldspace");
const osg::Vec2i cellPosition(0, 0);
const int cellSize = 8192;
- ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f()));
- for (int x = -6; x < 6; ++x)
- for (int y = -6; y < 6; ++y)
- ASSERT_NE(manager.getMesh(TilePosition(x, y)), nullptr);
+ ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f));
+ for (int x = -1; x < 12; ++x)
+ for (int y = -1; y < 12; ++y)
+ ASSERT_NE(manager.getMesh("worldspace", TilePosition(x, y)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_water_for_max_int_should_not_add_new_tiles)
{
TileCachedRecastMeshManager manager(mSettings);
+ manager.setWorldspace("worldspace");
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
ASSERT_TRUE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground));
const osg::Vec2i cellPosition(0, 0);
const int cellSize = std::numeric_limits<int>::max();
- ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f()));
+ ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f));
for (int x = -6; x < 6; ++x)
for (int y = -6; y < 6; ++y)
- ASSERT_EQ(manager.getMesh(TilePosition(x, y)) != nullptr, -1 <= x && x <= 0 && -1 <= y && y <= 0);
+ ASSERT_EQ(manager.getMesh("worldspace", TilePosition(x, y)) != nullptr, -1 <= x && x <= 0 && -1 <= y && y <= 0);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_absent_cell_should_return_nullopt)
@@ -303,51 +318,67 @@ namespace
TileCachedRecastMeshManager manager(mSettings);
const osg::Vec2i cellPosition(0, 0);
const int cellSize = 8192;
- ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f()));
+ ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f));
const auto result = manager.removeWater(cellPosition);
ASSERT_TRUE(result.has_value());
- EXPECT_EQ(result->mSize, cellSize);
+ EXPECT_EQ(result->mCellSize, cellSize);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_existing_cell_should_remove_empty_tiles)
{
TileCachedRecastMeshManager manager(mSettings);
+ manager.setWorldspace("worldspace");
const osg::Vec2i cellPosition(0, 0);
const int cellSize = 8192;
- ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f()));
+ ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f));
ASSERT_TRUE(manager.removeWater(cellPosition));
for (int x = -6; x < 6; ++x)
for (int y = -6; y < 6; ++y)
- ASSERT_EQ(manager.getMesh(TilePosition(x, y)), nullptr);
+ ASSERT_EQ(manager.getMesh("worldspace", TilePosition(x, y)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_existing_cell_should_leave_not_empty_tiles)
{
TileCachedRecastMeshManager manager(mSettings);
+ manager.setWorldspace("worldspace");
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
ASSERT_TRUE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground));
const osg::Vec2i cellPosition(0, 0);
const int cellSize = 8192;
- ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f()));
+ ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f));
ASSERT_TRUE(manager.removeWater(cellPosition));
for (int x = -6; x < 6; ++x)
for (int y = -6; y < 6; ++y)
- ASSERT_EQ(manager.getMesh(TilePosition(x, y)) != nullptr, -1 <= x && x <= 0 && -1 <= y && y <= 0);
+ ASSERT_EQ(manager.getMesh("worldspace", TilePosition(x, y)) != nullptr, -1 <= x && x <= 0 && -1 <= y && y <= 0);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_object_should_not_remove_tile_with_water)
{
TileCachedRecastMeshManager manager(mSettings);
+ manager.setWorldspace("worldspace");
const osg::Vec2i cellPosition(0, 0);
const int cellSize = 8192;
const btBoxShape boxShape(btVector3(20, 20, 100));
- const CollisionShape shape(nullptr, boxShape);
+ const CollisionShape shape(mInstance, boxShape, mObjectTransform);
ASSERT_TRUE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground));
- ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f()));
+ ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f));
ASSERT_TRUE(manager.removeObject(ObjectId(&boxShape)));
- for (int x = -6; x < 6; ++x)
- for (int y = -6; y < 6; ++y)
- ASSERT_NE(manager.getMesh(TilePosition(x, y)), nullptr);
+ for (int x = -1; x < 12; ++x)
+ for (int y = -1; y < 12; ++y)
+ ASSERT_NE(manager.getMesh("worldspace", TilePosition(x, y)), nullptr);
+ }
+
+ TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, set_new_worldspace_should_remove_tiles)
+ {
+ TileCachedRecastMeshManager manager(mSettings);
+ manager.setWorldspace("worldspace");
+ const btBoxShape boxShape(btVector3(20, 20, 100));
+ const CollisionShape shape(nullptr, boxShape, mObjectTransform);
+ ASSERT_TRUE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground));
+ manager.setWorldspace("other");
+ for (int x = -1; x < 1; ++x)
+ for (int y = -1; y < 1; ++y)
+ ASSERT_EQ(manager.getMesh("other", TilePosition(x, y)), nullptr);
}
}
diff --git a/apps/openmw_test_suite/esmloader/load.cpp b/apps/openmw_test_suite/esmloader/load.cpp
index 0f00847226..d6ed66f7a7 100644
--- a/apps/openmw_test_suite/esmloader/load.cpp
+++ b/apps/openmw_test_suite/esmloader/load.cpp
@@ -101,4 +101,27 @@ namespace
EXPECT_EQ(esmData.mLands.size(), 0);
EXPECT_EQ(esmData.mStatics.size(), 0);
}
+
+ TEST_F(EsmLoaderTest, loadEsmDataShouldSkipUnsupportedFormats)
+ {
+ Query query;
+ query.mLoadActivators = true;
+ query.mLoadCells = true;
+ query.mLoadContainers = true;
+ query.mLoadDoors = true;
+ query.mLoadGameSettings = true;
+ query.mLoadLands = true;
+ query.mLoadStatics = true;
+ const std::vector<std::string> contentFiles {{"script.omwscripts"}};
+ std::vector<ESM::ESMReader> readers(contentFiles.size());
+ ToUTF8::Utf8Encoder* const encoder = nullptr;
+ const EsmData esmData = loadEsmData(query, contentFiles, mFileCollections, readers, encoder);
+ EXPECT_EQ(esmData.mActivators.size(), 0);
+ EXPECT_EQ(esmData.mCells.size(), 0);
+ EXPECT_EQ(esmData.mContainers.size(), 0);
+ EXPECT_EQ(esmData.mDoors.size(), 0);
+ EXPECT_EQ(esmData.mGameSettings.size(), 0);
+ EXPECT_EQ(esmData.mLands.size(), 0);
+ EXPECT_EQ(esmData.mStatics.size(), 0);
+ }
}
diff --git a/apps/openmw_test_suite/files/hash.cpp b/apps/openmw_test_suite/files/hash.cpp
new file mode 100644
index 0000000000..e6dbc8f6cc
--- /dev/null
+++ b/apps/openmw_test_suite/files/hash.cpp
@@ -0,0 +1,55 @@
+#include <components/files/hash.hpp>
+#include <components/files/constrainedfilestream.hpp>
+
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+
+#include <algorithm>
+#include <fstream>
+#include <sstream>
+#include <string>
+
+namespace
+{
+ using namespace testing;
+ using namespace Files;
+
+ struct Params
+ {
+ std::size_t mSize;
+ std::array<std::uint64_t, 2> mHash;
+ };
+
+ struct FilesGetHash : TestWithParam<Params> {};
+
+ TEST_P(FilesGetHash, shouldReturnHashForStringStream)
+ {
+ const std::string fileName = "fileName";
+ std::string content;
+ std::fill_n(std::back_inserter(content), GetParam().mSize, 'a');
+ std::istringstream stream(content);
+ EXPECT_EQ(getHash(fileName, stream), GetParam().mHash);
+ }
+
+ TEST_P(FilesGetHash, shouldReturnHashForConstrainedFileStream)
+ {
+ std::string fileName(UnitTest::GetInstance()->current_test_info()->name());
+ std::replace(fileName.begin(), fileName.end(), '/', '_');
+ std::string content;
+ std::fill_n(std::back_inserter(content), GetParam().mSize, 'a');
+ std::fstream(fileName, std::ios_base::out | std::ios_base::binary)
+ .write(content.data(), static_cast<std::streamsize>(content.size()));
+ const auto stream = Files::openConstrainedFileStream(fileName.data(), 0, content.size());
+ EXPECT_EQ(getHash(fileName, *stream), GetParam().mHash);
+ }
+
+ INSTANTIATE_TEST_SUITE_P(Params, FilesGetHash, Values(
+ Params {0, {0, 0}},
+ Params {1, {9607679276477937801ull, 16624257681780017498ull}},
+ Params {128, {15287858148353394424ull, 16818615825966581310ull}},
+ Params {1000, {11018119256083894017ull, 6631144854802791578ull}},
+ Params {4096, {11972283295181039100ull, 16027670129106775155ull}},
+ Params {4097, {16717956291025443060ull, 12856404199748778153ull}},
+ Params {5000, {15775925571142117787ull, 10322955217889622896ull}}
+ ));
+}
diff --git a/apps/openmw_test_suite/lua/test_i18n.cpp b/apps/openmw_test_suite/lua/test_i18n.cpp
new file mode 100644
index 0000000000..427482be64
--- /dev/null
+++ b/apps/openmw_test_suite/lua/test_i18n.cpp
@@ -0,0 +1,110 @@
+#include "gmock/gmock.h"
+#include <gtest/gtest.h>
+
+#include <components/files/fixedpath.hpp>
+
+#include <components/lua/luastate.hpp>
+#include <components/lua/i18n.hpp>
+
+#include "testing_util.hpp"
+
+namespace
+{
+ using namespace testing;
+
+ TestFile invalidScript("not a script");
+ TestFile incorrectScript("return { incorrectSection = {}, engineHandlers = { incorrectHandler = function() end } }");
+ TestFile emptyScript("");
+
+ TestFile test1En(R"X(
+return {
+ good_morning = "Good morning.",
+ you_have_arrows = {
+ one = "You have one arrow.",
+ other = "You have %{count} arrows.",
+ },
+}
+)X");
+
+ TestFile test1De(R"X(
+return {
+ good_morning = "Guten Morgen.",
+ you_have_arrows = {
+ one = "Du hast ein Pfeil.",
+ other = "Du hast %{count} Pfeile.",
+ },
+ ["Hello %{name}!"] = "Hallo %{name}!",
+}
+)X");
+
+TestFile test2En(R"X(
+return {
+ good_morning = "Morning!",
+ you_have_arrows = "Arrows count: %{count}",
+}
+)X");
+
+ TestFile invalidTest2De(R"X(
+require('math')
+return {}
+)X");
+
+ struct LuaI18nTest : Test
+ {
+ std::unique_ptr<VFS::Manager> mVFS = createTestVFS({
+ {"i18n/Test1/en.lua", &test1En},
+ {"i18n/Test1/de.lua", &test1De},
+ {"i18n/Test2/en.lua", &test2En},
+ {"i18n/Test2/de.lua", &invalidTest2De},
+ });
+
+ LuaUtil::ScriptsConfiguration mCfg;
+ std::string mLibsPath = (Files::TargetPathType("openmw_test_suite").getLocalPath() / "resources" / "lua_libs").string();
+ };
+
+ TEST_F(LuaI18nTest, I18n)
+ {
+ internal::CaptureStdout();
+ LuaUtil::LuaState lua{mVFS.get(), &mCfg};
+ sol::state& l = lua.sol();
+ LuaUtil::I18nManager i18n(mVFS.get(), &lua);
+ lua.addInternalLibSearchPath(mLibsPath);
+ i18n.init();
+ i18n.setPreferredLanguages({"de", "en"});
+ EXPECT_THAT(internal::GetCapturedStdout(), "I18n preferred languages: de en\n");
+
+ internal::CaptureStdout();
+ l["t1"] = i18n.getContext("Test1");
+ EXPECT_THAT(internal::GetCapturedStdout(), "Language file \"i18n/Test1/de.lua\" is enabled\n");
+
+ internal::CaptureStdout();
+ l["t2"] = i18n.getContext("Test2");
+ {
+ std::string output = internal::GetCapturedStdout();
+ EXPECT_THAT(output, HasSubstr("Can not load i18n/Test2/de.lua"));
+ EXPECT_THAT(output, HasSubstr("Language file \"i18n/Test2/en.lua\" is enabled"));
+ }
+
+ EXPECT_EQ(get<std::string>(l, "t1('good_morning')"), "Guten Morgen.");
+ EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=1})"), "Du hast ein Pfeil.");
+ EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=5})"), "Du hast 5 Pfeile.");
+ EXPECT_EQ(get<std::string>(l, "t1('Hello %{name}!', {name='World'})"), "Hallo World!");
+ EXPECT_EQ(get<std::string>(l, "t2('good_morning')"), "Morning!");
+ EXPECT_EQ(get<std::string>(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3");
+
+ internal::CaptureStdout();
+ i18n.setPreferredLanguages({"en", "de"});
+ EXPECT_THAT(internal::GetCapturedStdout(),
+ "I18n preferred languages: en de\n"
+ "Language file \"i18n/Test1/en.lua\" is enabled\n"
+ "Language file \"i18n/Test2/en.lua\" is enabled\n");
+
+ EXPECT_EQ(get<std::string>(l, "t1('good_morning')"), "Good morning.");
+ EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=1})"), "You have one arrow.");
+ EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=5})"), "You have 5 arrows.");
+ EXPECT_EQ(get<std::string>(l, "t1('Hello %{name}!', {name='World'})"), "Hello World!");
+ EXPECT_EQ(get<std::string>(l, "t2('good_morning')"), "Morning!");
+ EXPECT_EQ(get<std::string>(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3");
+ }
+
+}
diff --git a/apps/openmw_test_suite/lua/test_lua.cpp b/apps/openmw_test_suite/lua/test_lua.cpp
index 4b3ecdcb2b..fe3cf14d25 100644
--- a/apps/openmw_test_suite/lua/test_lua.cpp
+++ b/apps/openmw_test_suite/lua/test_lua.cpp
@@ -106,7 +106,7 @@ return {
}
EXPECT_EQ(LuaUtil::call(script["useCounter"]).get<int>(), 45);
- EXPECT_ERROR(LuaUtil::call(script["incorrectRequire"]), "Resource 'counter.lua' not found");
+ EXPECT_ERROR(LuaUtil::call(script["incorrectRequire"]), "module not found: counter");
}
TEST_F(LuaStateTest, ReadOnly)
@@ -161,7 +161,7 @@ return {
sol::table script2 = lua.runInNewSandbox("bbb/tests.lua", "", {{"test.api", api2}});
- EXPECT_ERROR(LuaUtil::call(script1["sqr"], 3), "Resource 'sqrlib.lua' not found");
+ EXPECT_ERROR(LuaUtil::call(script1["sqr"], 3), "module not found: sqrlib");
EXPECT_EQ(LuaUtil::call(script2["sqr"], 3).get<int>(), 9);
EXPECT_EQ(LuaUtil::call(script1["apiName"]).get<std::string>(), "api1");
diff --git a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp
index 344fbb3c78..779debb4e6 100644
--- a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp
+++ b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp
@@ -364,7 +364,7 @@ return {
TEST_F(LuaScriptsContainerTest, Timers)
{
- using TimeUnit = LuaUtil::ScriptsContainer::TimeUnit;
+ using TimerType = LuaUtil::ScriptsContainer::TimerType;
LuaUtil::ScriptsContainer scripts(&mLua, "Test");
int test1Id = *mCfg.findId("test1.lua");
int test2Id = *mCfg.findId("test2.lua");
@@ -387,18 +387,18 @@ return {
scripts.processTimers(1, 2);
- scripts.setupSerializableTimer(TimeUnit::SECONDS, 10, test1Id, "B", sol::make_object(mLua.sol(), 3));
- scripts.setupSerializableTimer(TimeUnit::HOURS, 10, test2Id, "B", sol::make_object(mLua.sol(), 4));
- scripts.setupSerializableTimer(TimeUnit::SECONDS, 5, test1Id, "A", sol::make_object(mLua.sol(), 1));
- scripts.setupSerializableTimer(TimeUnit::HOURS, 5, test2Id, "A", sol::make_object(mLua.sol(), 2));
- scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, test1Id, "A", sol::make_object(mLua.sol(), 10));
- scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, test1Id, "B", sol::make_object(mLua.sol(), 20));
+ scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 10, test1Id, "B", sol::make_object(mLua.sol(), 3));
+ scripts.setupSerializableTimer(TimerType::GAME_TIME, 10, test2Id, "B", sol::make_object(mLua.sol(), 4));
+ scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 5, test1Id, "A", sol::make_object(mLua.sol(), 1));
+ scripts.setupSerializableTimer(TimerType::GAME_TIME, 5, test2Id, "A", sol::make_object(mLua.sol(), 2));
+ scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 15, test1Id, "A", sol::make_object(mLua.sol(), 10));
+ scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 15, test1Id, "B", sol::make_object(mLua.sol(), 20));
- scripts.setupUnsavableTimer(TimeUnit::SECONDS, 10, test2Id, fn2);
- scripts.setupUnsavableTimer(TimeUnit::HOURS, 10, test1Id, fn2);
- scripts.setupUnsavableTimer(TimeUnit::SECONDS, 5, test2Id, fn1);
- scripts.setupUnsavableTimer(TimeUnit::HOURS, 5, test1Id, fn1);
- scripts.setupUnsavableTimer(TimeUnit::SECONDS, 15, test2Id, fn1);
+ scripts.setupUnsavableTimer(TimerType::SIMULATION_TIME, 10, test2Id, fn2);
+ scripts.setupUnsavableTimer(TimerType::GAME_TIME, 10, test1Id, fn2);
+ scripts.setupUnsavableTimer(TimerType::SIMULATION_TIME, 5, test2Id, fn1);
+ scripts.setupUnsavableTimer(TimerType::GAME_TIME, 5, test1Id, fn1);
+ scripts.setupUnsavableTimer(TimerType::SIMULATION_TIME, 15, test2Id, fn1);
EXPECT_EQ(counter1, 0);
EXPECT_EQ(counter3, 0);
@@ -440,4 +440,24 @@ return {
EXPECT_EQ(counter4, 25);
}
+ TEST_F(LuaScriptsContainerTest, CallbackWrapper)
+ {
+ LuaUtil::Callback callback{mLua.sol()["print"], mLua.newTable()};
+ callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptDebugNameKey] = "some_script.lua";
+ callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptsContainer::ScriptId{nullptr, 0};
+
+ testing::internal::CaptureStdout();
+ callback(1.5);
+ EXPECT_EQ(internal::GetCapturedStdout(), "1.5\n");
+
+ testing::internal::CaptureStdout();
+ callback(1.5, 2.5);
+ EXPECT_EQ(internal::GetCapturedStdout(), "1.5\t2.5\n");
+
+ testing::internal::CaptureStdout();
+ callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = sol::nil;
+ callback(1.5, 2.5);
+ EXPECT_EQ(internal::GetCapturedStdout(), "Ignored callback to the removed script some_script.lua\n");
+ }
+
}
diff --git a/apps/openmw_test_suite/lua/test_serialization.cpp b/apps/openmw_test_suite/lua/test_serialization.cpp
index 1983daa158..1d664b06a4 100644
--- a/apps/openmw_test_suite/lua/test_serialization.cpp
+++ b/apps/openmw_test_suite/lua/test_serialization.cpp
@@ -1,10 +1,13 @@
#include "gmock/gmock.h"
#include <gtest/gtest.h>
+#include <osg/Matrixf>
+#include <osg/Quat>
#include <osg/Vec2f>
#include <osg/Vec3f>
#include <components/lua/serialization.hpp>
+#include <components/lua/utilpackage.hpp>
#include <components/misc/endianness.hpp>
@@ -90,20 +93,47 @@ namespace
{
std::string serialized = LuaUtil::serialize(sol::make_object(lua, vec2));
- EXPECT_EQ(serialized.size(), 10); // version, type, 2x float
+ EXPECT_EQ(serialized.size(), 18); // version, type, 2x double
sol::object value = LuaUtil::deserialize(lua, serialized);
ASSERT_TRUE(value.is<osg::Vec2f>());
EXPECT_EQ(value.as<osg::Vec2f>(), vec2);
}
{
std::string serialized = LuaUtil::serialize(sol::make_object(lua, vec3));
- EXPECT_EQ(serialized.size(), 14); // version, type, 3x float
+ EXPECT_EQ(serialized.size(), 26); // version, type, 3x double
sol::object value = LuaUtil::deserialize(lua, serialized);
ASSERT_TRUE(value.is<osg::Vec3f>());
EXPECT_EQ(value.as<osg::Vec3f>(), vec3);
}
}
+ TEST(LuaSerializationTest, Transform) {
+ sol::state lua;
+ osg::Matrixf matrix(1, 2, 3, 4,
+ 5, 6, 7, 8,
+ 9, 10, 11, 12,
+ 13, 14, 15, 16);
+ LuaUtil::TransformM transM = LuaUtil::asTransform(matrix);
+ osg::Quat quat(1, 2, 3, 4);
+ LuaUtil::TransformQ transQ = LuaUtil::asTransform(quat);
+
+ {
+ std::string serialized = LuaUtil::serialize(sol::make_object(lua, transM));
+ EXPECT_EQ(serialized.size(), 130); // version, type, 16x double
+ sol::object value = LuaUtil::deserialize(lua, serialized);
+ ASSERT_TRUE(value.is<LuaUtil::TransformM>());
+ EXPECT_EQ(value.as<LuaUtil::TransformM>().mM, transM.mM);
+ }
+ {
+ std::string serialized = LuaUtil::serialize(sol::make_object(lua, transQ));
+ EXPECT_EQ(serialized.size(), 34); // version, type, 4x double
+ sol::object value = LuaUtil::deserialize(lua, serialized);
+ ASSERT_TRUE(value.is<LuaUtil::TransformQ>());
+ EXPECT_EQ(value.as<LuaUtil::TransformQ>().mQ, transQ.mQ);
+ }
+
+ }
+
TEST(LuaSerializationTest, Table)
{
sol::state lua;
@@ -119,16 +149,27 @@ namespace
table[2] = osg::Vec2f(2, 1);
std::string serialized = LuaUtil::serialize(table);
- EXPECT_EQ(serialized.size(), 123);
+ EXPECT_EQ(serialized.size(), 139);
sol::table res_table = LuaUtil::deserialize(lua, serialized);
+ sol::table res_readonly_table = LuaUtil::deserialize(lua, serialized, nullptr, true);
+
+ for (auto t : {res_table, res_readonly_table})
+ {
+ EXPECT_EQ(t.get<int>("aa"), 1);
+ EXPECT_EQ(t.get<bool>("ab"), true);
+ EXPECT_EQ(t.get<sol::table>("nested").get<int>("aa"), 2);
+ EXPECT_EQ(t.get<sol::table>("nested").get<std::string>("bb"), "something");
+ EXPECT_FLOAT_EQ(t.get<sol::table>("nested").get<double>(5), -0.5);
+ EXPECT_EQ(t.get<osg::Vec2f>(1), osg::Vec2f(1, 2));
+ EXPECT_EQ(t.get<osg::Vec2f>(2), osg::Vec2f(2, 1));
+ }
- EXPECT_EQ(res_table.get<int>("aa"), 1);
- EXPECT_EQ(res_table.get<bool>("ab"), true);
- EXPECT_EQ(res_table.get<sol::table>("nested").get<int>("aa"), 2);
- EXPECT_EQ(res_table.get<sol::table>("nested").get<std::string>("bb"), "something");
- EXPECT_FLOAT_EQ(res_table.get<sol::table>("nested").get<double>(5), -0.5);
- EXPECT_EQ(res_table.get<osg::Vec2f>(1), osg::Vec2f(1, 2));
- EXPECT_EQ(res_table.get<osg::Vec2f>(2), osg::Vec2f(2, 1));
+ lua["t"] = res_table;
+ lua["ro_t"] = res_readonly_table;
+ EXPECT_NO_THROW(lua.safe_script("t.x = 5"));
+ EXPECT_NO_THROW(lua.safe_script("t.nested.x = 5"));
+ EXPECT_ERROR(lua.safe_script("ro_t.x = 5"), "userdata value");
+ EXPECT_ERROR(lua.safe_script("ro_t.nested.x = 5"), "userdata value");
}
struct TestStruct1 { double a, b; };
@@ -157,7 +198,7 @@ namespace
return false;
}
- bool deserialize(std::string_view typeName, std::string_view binaryData, sol::state& lua) const override
+ bool deserialize(std::string_view typeName, std::string_view binaryData, lua_State* lua) const override
{
if (typeName == "ts1")
{
diff --git a/apps/openmw_test_suite/lua/test_storage.cpp b/apps/openmw_test_suite/lua/test_storage.cpp
new file mode 100644
index 0000000000..fafba008a1
--- /dev/null
+++ b/apps/openmw_test_suite/lua/test_storage.cpp
@@ -0,0 +1,103 @@
+#include <filesystem>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <components/lua/storage.hpp>
+
+namespace
+{
+ using namespace testing;
+
+ template <typename T>
+ T get(sol::state& lua, std::string luaCode)
+ {
+ return lua.safe_script("return " + luaCode).get<T>();
+ }
+
+ TEST(LuaUtilStorageTest, Basic)
+ {
+ sol::state mLua;
+ LuaUtil::LuaStorage::initLuaBindings(mLua);
+ LuaUtil::LuaStorage storage(mLua);
+ mLua["mutable"] = storage.getMutableSection("test");
+ mLua["ro"] = storage.getReadOnlySection("test");
+
+ mLua.safe_script("mutable:set('x', 5)");
+ EXPECT_EQ(get<int>(mLua, "mutable:get('x')"), 5);
+ EXPECT_EQ(get<int>(mLua, "ro:get('x')"), 5);
+ EXPECT_FALSE(get<bool>(mLua, "mutable:wasChanged()"));
+ EXPECT_TRUE(get<bool>(mLua, "ro:wasChanged()"));
+ EXPECT_FALSE(get<bool>(mLua, "ro:wasChanged()"));
+
+ EXPECT_THROW(mLua.safe_script("ro:set('y', 3)"), std::exception);
+
+ mLua.safe_script("t1 = mutable:asTable()");
+ mLua.safe_script("t2 = ro:asTable()");
+ EXPECT_EQ(get<int>(mLua, "t1.x"), 5);
+ EXPECT_EQ(get<int>(mLua, "t2.x"), 5);
+
+ mLua.safe_script("mutable:reset()");
+ EXPECT_TRUE(get<bool>(mLua, "ro:get('x') == nil"));
+
+ mLua.safe_script("mutable:reset({x=4, y=7})");
+ EXPECT_EQ(get<int>(mLua, "ro:get('x')"), 4);
+ EXPECT_EQ(get<int>(mLua, "ro:get('y')"), 7);
+ EXPECT_FALSE(get<bool>(mLua, "mutable:wasChanged()"));
+ EXPECT_TRUE(get<bool>(mLua, "ro:wasChanged()"));
+ EXPECT_FALSE(get<bool>(mLua, "ro:wasChanged()"));
+ }
+
+ TEST(LuaUtilStorageTest, Table)
+ {
+ sol::state mLua;
+ LuaUtil::LuaStorage::initLuaBindings(mLua);
+ LuaUtil::LuaStorage storage(mLua);
+ mLua["mutable"] = storage.getMutableSection("test");
+ mLua["ro"] = storage.getReadOnlySection("test");
+
+ mLua.safe_script("mutable:set('x', { y = 'abc', z = 7 })");
+ EXPECT_EQ(get<int>(mLua, "mutable:get('x').z"), 7);
+ EXPECT_THROW(mLua.safe_script("mutable:get('x').z = 3"), std::exception);
+ EXPECT_NO_THROW(mLua.safe_script("mutable:getCopy('x').z = 3"));
+ EXPECT_EQ(get<int>(mLua, "mutable:get('x').z"), 7);
+ EXPECT_EQ(get<int>(mLua, "ro:get('x').z"), 7);
+ EXPECT_EQ(get<std::string>(mLua, "ro:get('x').y"), "abc");
+ }
+
+ TEST(LuaUtilStorageTest, Saving)
+ {
+ sol::state mLua;
+ LuaUtil::LuaStorage::initLuaBindings(mLua);
+ LuaUtil::LuaStorage storage(mLua);
+
+ mLua["permanent"] = storage.getMutableSection("permanent");
+ mLua["temporary"] = storage.getMutableSection("temporary");
+ mLua.safe_script("temporary:removeOnExit()");
+ mLua.safe_script("permanent:set('x', 1)");
+ mLua.safe_script("temporary:set('y', 2)");
+
+ std::string tmpFile = (std::filesystem::temp_directory_path() / "test_storage.bin").string();
+ storage.save(tmpFile);
+ EXPECT_EQ(get<int>(mLua, "permanent:get('x')"), 1);
+ EXPECT_EQ(get<int>(mLua, "temporary:get('y')"), 2);
+
+ storage.clearTemporary();
+ mLua["permanent"] = storage.getMutableSection("permanent");
+ mLua["temporary"] = storage.getMutableSection("temporary");
+ EXPECT_EQ(get<int>(mLua, "permanent:get('x')"), 1);
+ EXPECT_TRUE(get<bool>(mLua, "temporary:get('y') == nil"));
+
+ mLua.safe_script("permanent:set('x', 3)");
+ mLua.safe_script("permanent:set('z', 4)");
+
+ LuaUtil::LuaStorage storage2(mLua);
+ mLua["permanent"] = storage2.getMutableSection("permanent");
+ mLua["temporary"] = storage2.getMutableSection("temporary");
+
+ storage2.load(tmpFile);
+ EXPECT_EQ(get<int>(mLua, "permanent:get('x')"), 1);
+ EXPECT_TRUE(get<bool>(mLua, "permanent:get('z') == nil"));
+ EXPECT_TRUE(get<bool>(mLua, "temporary:get('y') == nil"));
+ }
+
+}
diff --git a/apps/openmw_test_suite/lua/test_ui_content.cpp b/apps/openmw_test_suite/lua/test_ui_content.cpp
new file mode 100644
index 0000000000..f478c618dc
--- /dev/null
+++ b/apps/openmw_test_suite/lua/test_ui_content.cpp
@@ -0,0 +1,97 @@
+#include <gtest/gtest.h>
+#include <sol/sol.hpp>
+
+#include <components/lua_ui/content.hpp>
+
+namespace
+{
+ using namespace testing;
+
+ sol::state state;
+
+ sol::table makeTable()
+ {
+ return sol::table(state, sol::create);
+ }
+
+ sol::table makeTable(std::string name)
+ {
+ auto result = makeTable();
+ result["name"] = name;
+ return result;
+ }
+
+ TEST(LuaUiContentTest, Create)
+ {
+ auto table = makeTable();
+ table.add(makeTable());
+ table.add(makeTable());
+ table.add(makeTable());
+ LuaUi::Content content(table);
+ EXPECT_EQ(content.size(), 3);
+ }
+
+ TEST(LuaUiContentTest, CreateWithHole)
+ {
+ auto table = makeTable();
+ table.add(makeTable());
+ table.add(makeTable());
+ table[4] = makeTable();
+ EXPECT_ANY_THROW(LuaUi::Content content(table));
+ }
+
+ TEST(LuaUiContentTest, WrongType)
+ {
+ auto table = makeTable();
+ table.add(makeTable());
+ table.add("a");
+ table.add(makeTable());
+ EXPECT_ANY_THROW(LuaUi::Content content(table));
+ }
+
+ TEST(LuaUiContentTest, NameAccess)
+ {
+ auto table = makeTable();
+ table.add(makeTable());
+ table.add(makeTable("a"));
+ LuaUi::Content content(table);
+ EXPECT_NO_THROW(content.at("a"));
+ content.remove("a");
+ content.assign(content.size(), makeTable("b"));
+ content.assign("b", makeTable());
+ EXPECT_ANY_THROW(content.at("b"));
+ EXPECT_EQ(content.size(), 2);
+ content.assign(content.size(), makeTable("c"));
+ content.assign(content.size(), makeTable("c"));
+ content.remove("c");
+ EXPECT_ANY_THROW(content.at("c"));
+ }
+
+ TEST(LuaUiContentTest, IndexOf)
+ {
+ auto table = makeTable();
+ table.add(makeTable());
+ table.add(makeTable());
+ table.add(makeTable());
+ LuaUi::Content content(table);
+ auto child = makeTable();
+ content.assign(2, child);
+ EXPECT_EQ(content.indexOf(child), 2);
+ EXPECT_EQ(content.indexOf(makeTable()), content.size());
+ }
+
+ TEST(LuaUiContentTest, BoundsChecks)
+ {
+ auto table = makeTable();
+ LuaUi::Content content(table);
+ EXPECT_ANY_THROW(content.at(0));
+ content.assign(content.size(), makeTable());
+ content.assign(content.size(), makeTable());
+ content.assign(content.size(), makeTable());
+ EXPECT_ANY_THROW(content.at(3));
+ EXPECT_ANY_THROW(content.remove(3));
+ EXPECT_NO_THROW(content.remove(1));
+ EXPECT_NO_THROW(content.at(1));
+ EXPECT_EQ(content.size(), 2);
+ }
+}
diff --git a/apps/openmw_test_suite/lua/test_utilpackage.cpp b/apps/openmw_test_suite/lua/test_utilpackage.cpp
index ead3cecc6f..953d5f50d3 100644
--- a/apps/openmw_test_suite/lua/test_utilpackage.cpp
+++ b/apps/openmw_test_suite/lua/test_utilpackage.cpp
@@ -10,12 +10,6 @@ namespace
{
using namespace testing;
- template <typename T>
- T get(sol::state& lua, std::string luaCode)
- {
- return lua.safe_script("return " + luaCode).get<T>();
- }
-
std::string getAsString(sol::state& lua, std::string luaCode)
{
return LuaUtil::toString(lua.safe_script("return " + luaCode));
@@ -93,13 +87,13 @@ namespace
EXPECT_EQ(getAsString(lua, "moveAndScale * v(300, 200, 100)"), "(156, 222, 68)");
EXPECT_THAT(getAsString(lua, "moveAndScale"), AllOf(StartsWith("TransformM{ move(6, 22, 18) scale(0.5, 1, 0.5) "), EndsWith(" }")));
EXPECT_EQ(getAsString(lua, "T.identity"), "TransformM{ }");
- lua.safe_script("rx = T.rotateX(math.pi / 2)");
- lua.safe_script("ry = T.rotateY(math.pi / 2)");
- lua.safe_script("rz = T.rotateZ(math.pi / 2)");
+ lua.safe_script("rx = T.rotateX(-math.pi / 2)");
+ lua.safe_script("ry = T.rotateY(-math.pi / 2)");
+ lua.safe_script("rz = T.rotateZ(-math.pi / 2)");
EXPECT_LT(get<float>(lua, "(rx * v(1, 2, 3) - v(1, -3, 2)):length()"), 1e-6);
EXPECT_LT(get<float>(lua, "(ry * v(1, 2, 3) - v(3, 2, -1)):length()"), 1e-6);
EXPECT_LT(get<float>(lua, "(rz * v(1, 2, 3) - v(-2, 1, 3)):length()"), 1e-6);
- lua.safe_script("rot = T.rotate(math.pi / 2, v(-1, -1, 0)) * T.rotateZ(-math.pi / 4)");
+ lua.safe_script("rot = T.rotate(math.pi / 2, v(-1, -1, 0)) * T.rotateZ(math.pi / 4)");
EXPECT_THAT(getAsString(lua, "rot"), HasSubstr("TransformQ"));
EXPECT_LT(get<float>(lua, "(rot * v(1, 0, 0) - v(0, 0, 1)):length()"), 1e-6);
EXPECT_LT(get<float>(lua, "(rot * rot:inverse() * v(1, 0, 0) - v(1, 0, 0)):length()"), 1e-6);
@@ -120,6 +114,9 @@ namespace
EXPECT_FLOAT_EQ(get<float>(lua, "util.clamp(0.1, 0, 1.5)"), 0.1);
EXPECT_FLOAT_EQ(get<float>(lua, "util.clamp(-0.1, 0, 1.5)"), 0);
EXPECT_FLOAT_EQ(get<float>(lua, "util.clamp(2.1, 0, 1.5)"), 1.5);
+ lua.safe_script("t = util.makeReadOnly({x = 1})");
+ EXPECT_FLOAT_EQ(get<float>(lua, "t.x"), 1);
+ EXPECT_ERROR(lua.safe_script("t.y = 2"), "userdata value");
}
}
diff --git a/apps/openmw_test_suite/lua/testing_util.hpp b/apps/openmw_test_suite/lua/testing_util.hpp
index 2f6810350f..ba4a418bfb 100644
--- a/apps/openmw_test_suite/lua/testing_util.hpp
+++ b/apps/openmw_test_suite/lua/testing_util.hpp
@@ -2,6 +2,7 @@
#define LUA_TESTING_UTIL_H
#include <sstream>
+#include <sol/sol.hpp>
#include <components/vfs/archive.hpp>
#include <components/vfs/manager.hpp>
@@ -9,6 +10,12 @@
namespace
{
+ template <typename T>
+ T get(sol::state& lua, const std::string& luaCode)
+ {
+ return lua.safe_script("return " + luaCode).get<T>();
+ }
+
class TestFile : public VFS::File
{
public:
diff --git a/apps/openmw_test_suite/misc/test_resourcehelpers.cpp b/apps/openmw_test_suite/misc/test_resourcehelpers.cpp
new file mode 100644
index 0000000000..ee062c6da8
--- /dev/null
+++ b/apps/openmw_test_suite/misc/test_resourcehelpers.cpp
@@ -0,0 +1,80 @@
+#include <gtest/gtest.h>
+#include "components/misc/resourcehelpers.hpp"
+#include "../lua/testing_util.hpp"
+
+namespace
+{
+ using namespace Misc::ResourceHelpers;
+ TEST(CorrectSoundPath, wav_files_not_overridden_with_mp3_in_vfs_are_not_corrected)
+ {
+ std::unique_ptr<VFS::Manager> mVFS = createTestVFS({
+ {"sound/bar.wav", nullptr}
+ });
+ EXPECT_EQ(correctSoundPath("sound/bar.wav", mVFS.get()), "sound/bar.wav");
+ }
+
+ TEST(CorrectSoundPath, wav_files_overridden_with_mp3_in_vfs_are_corrected)
+ {
+ std::unique_ptr<VFS::Manager> mVFS = createTestVFS({
+ {"sound/foo.mp3", nullptr}
+ });
+ EXPECT_EQ(correctSoundPath("sound/foo.wav", mVFS.get()), "sound/foo.mp3");
+ }
+
+ TEST(CorrectSoundPath, corrected_path_does_not_check_existence_in_vfs)
+ {
+ std::unique_ptr<VFS::Manager> mVFS = createTestVFS({
+ });
+ EXPECT_EQ(correctSoundPath("sound/foo.wav", mVFS.get()), "sound/foo.mp3");
+ }
+
+ TEST(CorrectSoundPath, correct_path_normalize_paths)
+ {
+ std::unique_ptr<VFS::Manager> mVFS = createTestVFS({
+ });
+ EXPECT_EQ(correctSoundPath("sound\\foo.wav", mVFS.get()), "sound/foo.mp3");
+ EXPECT_EQ(correctSoundPath("SOUND\\foo.WAV", mVFS.get()), "SOUND/foo.mp3");
+ }
+
+ namespace
+ {
+ std::string checkChangeExtensionToDds(std::string path)
+ {
+ changeExtensionToDds(path);
+ return path;
+ }
+ }
+
+ TEST(ChangeExtensionToDds, original_extension_with_same_size_as_dds)
+ {
+ EXPECT_EQ(checkChangeExtensionToDds("texture/bar.tga"), "texture/bar.dds");
+ }
+
+ TEST(ChangeExtensionToDds, original_extension_greater_than_dds)
+ {
+ EXPECT_EQ(checkChangeExtensionToDds("texture/bar.jpeg"), "texture/bar.dds");
+ }
+
+ TEST(ChangeExtensionToDds, original_extension_smaller_than_dds)
+ {
+ EXPECT_EQ(checkChangeExtensionToDds("texture/bar.xx"), "texture/bar.dds");
+ }
+
+ TEST(ChangeExtensionToDds, does_not_change_dds_extension)
+ {
+ std::string path = "texture/bar.dds";
+ EXPECT_FALSE(changeExtensionToDds(path));
+ }
+
+ TEST(ChangeExtensionToDds, does_not_change_when_no_extension)
+ {
+ std::string path = "texture/bar";
+ EXPECT_FALSE(changeExtensionToDds(path));
+ }
+
+ TEST(ChangeExtensionToDds, change_when_there_is_an_extension)
+ {
+ std::string path = "texture/bar.jpeg";
+ EXPECT_TRUE(changeExtensionToDds(path));
+ }
+}
diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp
index 79db3bb414..c04860afdf 100644
--- a/apps/openmw_test_suite/mwscript/test_scripts.cpp
+++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp
@@ -427,6 +427,16 @@ set 1 to 42
End)mwscript";
+ const std::string sIssue6380 = R"mwscript(,Begin,issue6380,
+
+,short,a
+
+,set,a,to,,,,(a,+1)
+
+messagebox,"this is a %g",a
+
+,End,)mwscript";
+
TEST_F(MWScriptTest, mwscript_test_invalid)
{
EXPECT_THROW(compile("this is not a valid script", true), Compiler::SourceException);
@@ -831,4 +841,9 @@ End)mwscript";
FAIL();
}
}
+
+ TEST_F(MWScriptTest, mwscript_test_6380)
+ {
+ EXPECT_FALSE(!compile(sIssue6380));
+ }
} \ No newline at end of file
diff --git a/apps/openmw_test_suite/mwworld/test_store.cpp b/apps/openmw_test_suite/mwworld/test_store.cpp
index 29240a1f7f..7ddab538c4 100644
--- a/apps/openmw_test_suite/mwworld/test_store.cpp
+++ b/apps/openmw_test_suite/mwworld/test_store.cpp
@@ -3,7 +3,6 @@
#include <boost/filesystem/fstream.hpp>
#include <components/files/configurationmanager.hpp>
-#include <components/files/escape.hpp>
#include <components/esm/esmreader.hpp>
#include <components/esm/esmwriter.hpp>
#include <components/loadinglistener/loadinglistener.hpp>
@@ -55,10 +54,10 @@ struct ContentFileTest : public ::testing::Test
boost::program_options::options_description desc("Allowed options");
desc.add_options()
- ("data", boost::program_options::value<Files::EscapePathContainer>()->default_value(Files::EscapePathContainer(), "data")->multitoken()->composing())
- ("content", boost::program_options::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
+ ("data", boost::program_options::value<Files::MaybeQuotedPathContainer>()->default_value(Files::MaybeQuotedPathContainer(), "data")->multitoken()->composing())
+ ("content", boost::program_options::value<std::vector<std::string>>()->default_value(std::vector<std::string>(), "")
->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon")
- ("data-local", boost::program_options::value<Files::EscapePath>()->default_value(Files::EscapePath(), ""));
+ ("data-local", boost::program_options::value<Files::MaybeQuotedPathContainer::value_type>()->default_value(Files::MaybeQuotedPathContainer::value_type(), ""));
boost::program_options::notify(variables);
@@ -66,10 +65,10 @@ struct ContentFileTest : public ::testing::Test
Files::PathContainer dataDirs, dataLocal;
if (!variables["data"].empty()) {
- dataDirs = Files::EscapePath::toPathContainer(variables["data"].as<Files::EscapePathContainer>());
+ dataDirs = asPathContainer(variables["data"].as<Files::MaybeQuotedPathContainer>());
}
- Files::PathContainer::value_type local(variables["data-local"].as<Files::EscapePath>().mPath);
+ Files::PathContainer::value_type local(variables["data-local"].as<Files::MaybeQuotedPathContainer::value_type>());
if (!local.empty()) {
dataLocal.push_back(local);
}
@@ -82,7 +81,7 @@ struct ContentFileTest : public ::testing::Test
Files::Collections collections (dataDirs, true);
- std::vector<std::string> contentFiles = variables["content"].as<Files::EscapeStringVector>().toStdStringVector();
+ std::vector<std::string> contentFiles = variables["content"].as<std::vector<std::string>>();
for (auto & contentFile : contentFiles)
{
if (!Misc::StringUtils::ciEndsWith(contentFile, ".omwscripts"))
diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp
index b37a8cd6c0..8fbf5c1b5b 100644
--- a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp
+++ b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp
@@ -335,6 +335,7 @@ namespace
MOCK_METHOD(void, setUseSkinning, (bool), (override));
MOCK_METHOD(bool, getUseSkinning, (), (const, override));
MOCK_METHOD(std::string, getFilename, (), (const, override));
+ MOCK_METHOD(std::string, getHash, (), (const, override));
MOCK_METHOD(unsigned int, getVersion, (), (const, override));
MOCK_METHOD(unsigned int, getUserVersion, (), (const, override));
MOCK_METHOD(unsigned int, getBethVersion, (), (const, override));
@@ -381,6 +382,7 @@ namespace
),
btVector3(4, 8, 12)
};
+ const std::string mHash = "hash";
TestBulletNifLoader()
{
@@ -411,6 +413,8 @@ namespace
mNiTriStripsData.vertices = {osg::Vec3f(0, 0, 0), osg::Vec3f(1, 0, 0), osg::Vec3f(1, 1, 0), osg::Vec3f(0, 1, 0)};
mNiTriStripsData.strips = {{0, 1, 2, 3}};
mNiTriStrips.data = Nif::NiGeometryDataPtr(&mNiTriStripsData);
+
+ EXPECT_CALL(mNifFile, getHash()).WillOnce(Return(mHash));
}
};
@@ -423,6 +427,8 @@ namespace
Resource::BulletShape expected;
EXPECT_EQ(*result, expected);
+ EXPECT_EQ(result->mFileName, "test.nif");
+ EXPECT_EQ(result->mFileHash, mHash);
}
TEST_F(TestBulletNifLoader, should_ignore_nullptr_root)
diff --git a/apps/openmw_test_suite/openmw/options.cpp b/apps/openmw_test_suite/openmw/options.cpp
index ebb956b10c..04c209e8a4 100644
--- a/apps/openmw_test_suite/openmw/options.cpp
+++ b/apps/openmw_test_suite/openmw/options.cpp
@@ -1,6 +1,5 @@
#include <apps/openmw/options.hpp>
#include <components/files/configurationmanager.hpp>
-#include <components/files/escape.hpp>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
@@ -22,17 +21,12 @@ namespace
{
std::vector<std::string> result = std::move(base);
for (int i = 1; i <= std::numeric_limits<char>::max(); ++i)
- if (i != '&' && i != '"' && i != ' ' && i != '@' && i != '\n')
+ if (i != '&' && i != '"' && i != ' ' && i != '\n')
result.push_back(std::string(1, i));
return result;
}
- constexpr std::array supportedAtSignEscapings {
- std::pair {'a', '@'},
- std::pair {'h', '#'},
- };
-
- MATCHER_P(IsEscapePath, v, "") { return arg.mPath.string() == v; }
+ MATCHER_P(IsPath, v, "") { return arg.string() == v; }
template <class T>
void parseArgs(const T& arguments, bpo::variables_map& variables, bpo::options_description& description)
@@ -46,7 +40,7 @@ namespace
const std::array arguments {"openmw", "--load-savegame=save.omwsave"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "save.omwsave");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "save.omwsave");
}
TEST(OpenMWOptionsFromArguments, should_support_single_word_load_savegame_path)
@@ -55,7 +49,7 @@ namespace
const std::array arguments {"openmw", "--load-savegame", "save.omwsave"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "save.omwsave");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "save.omwsave");
}
TEST(OpenMWOptionsFromArguments, should_support_multi_component_load_savegame_path)
@@ -64,7 +58,7 @@ namespace
const std::array arguments {"openmw", "--load-savegame", "/home/user/openmw/save.omwsave"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "/home/user/openmw/save.omwsave");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "/home/user/openmw/save.omwsave");
}
TEST(OpenMWOptionsFromArguments, should_support_windows_multi_component_load_savegame_path)
@@ -73,7 +67,7 @@ namespace
const std::array arguments {"openmw", "--load-savegame", R"(C:\OpenMW\save.omwsave)"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), R"(C:\OpenMW\save.omwsave)");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), R"(C:\OpenMW\save.omwsave)");
}
TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_spaces)
@@ -82,17 +76,16 @@ namespace
const std::array arguments {"openmw", "--load-savegame", "my save.omwsave"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "my");
-// EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "my save.omwsave");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "my save.omwsave");
}
- TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_number_sign)
+ TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_octothorpe)
{
bpo::options_description description = makeOptionsDescription();
const std::array arguments {"openmw", "--load-savegame", "my#save.omwsave"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "my#save.omwsave");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "my#save.omwsave");
}
TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_at_sign)
@@ -101,8 +94,7 @@ namespace
const std::array arguments {"openmw", "--load-savegame", "my@save.omwsave"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "my?ave.omwsave");
-// EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "my@save.omwsave");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "my@save.omwsave");
}
TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_quote)
@@ -111,17 +103,16 @@ namespace
const std::array arguments {"openmw", "--load-savegame", R"(my"save.omwsave)"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), R"(my"save.omwsave)");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), R"(my"save.omwsave)");
}
- TEST(OpenMWOptionsFromArguments, should_support_quted_load_savegame_path)
+ TEST(OpenMWOptionsFromArguments, should_support_quoted_load_savegame_path)
{
bpo::options_description description = makeOptionsDescription();
const std::array arguments {"openmw", "--load-savegame", R"("save".omwsave)"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), R"(save)");
-// EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), R"("save".omwsave)");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), R"(save)");
}
TEST(OpenMWOptionsFromArguments, should_support_quoted_load_savegame_path_with_escaped_quote_by_ampersand)
@@ -130,7 +121,7 @@ namespace
const std::array arguments {"openmw", "--load-savegame", R"("save&".omwsave")"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), R"(save".omwsave)");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), R"(save".omwsave)");
}
TEST(OpenMWOptionsFromArguments, should_support_quoted_load_savegame_path_with_escaped_ampersand)
@@ -139,7 +130,7 @@ namespace
const std::array arguments {"openmw", "--load-savegame", R"("save.omwsave&&")"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "save.omwsave&");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "save.omwsave&");
}
TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_ampersand)
@@ -148,7 +139,7 @@ namespace
const std::array arguments {"openmw", "--load-savegame", "save&.omwsave"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "save&.omwsave");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "save&.omwsave");
}
TEST(OpenMWOptionsFromArguments, should_support_load_savegame_path_with_multiple_quotes)
@@ -157,7 +148,7 @@ namespace
const std::array arguments {"openmw", "--load-savegame", R"(my"save".omwsave)"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), R"(my"save".omwsave)");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), R"(my"save".omwsave)");
}
TEST(OpenMWOptionsFromArguments, should_compose_data)
@@ -166,7 +157,7 @@ namespace
const std::array arguments {"openmw", "--data", "1", "--data", "2"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_THAT(variables["data"].as<Files::EscapePathContainer>(), ElementsAre(IsEscapePath("1"), IsEscapePath("2")));
+ EXPECT_THAT(variables["data"].as<Files::MaybeQuotedPathContainer>(), ElementsAre(IsPath("1"), IsPath("2")));
}
TEST(OpenMWOptionsFromArguments, should_compose_data_from_single_flag)
@@ -175,7 +166,7 @@ namespace
const std::array arguments {"openmw", "--data", "1", "2"};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_THAT(variables["data"].as<Files::EscapePathContainer>(), ElementsAre(IsEscapePath("1"), IsEscapePath("2")));
+ EXPECT_THAT(variables["data"].as<Files::MaybeQuotedPathContainer>(), ElementsAre(IsPath("1"), IsPath("2")));
}
TEST(OpenMWOptionsFromArguments, should_throw_on_multiple_load_savegame)
@@ -196,7 +187,7 @@ namespace
const std::array arguments {"openmw", "--load-savegame", pathArgument.c_str()};
bpo::variables_map variables;
parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), path);
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), path);
}
INSTANTIATE_TEST_SUITE_P(
@@ -205,32 +196,13 @@ namespace
ValuesIn(generateSupportedCharacters({u8"👍", u8"Ъ", u8"Ǽ", "\n"}))
);
- struct OpenMWOptionsFromArgumentsEscapings : TestWithParam<std::pair<char, char>> {};
-
- TEST_P(OpenMWOptionsFromArgumentsEscapings, should_support_escaping_with_at_sign_in_load_savegame_path)
- {
- bpo::options_description description = makeOptionsDescription();
- const std::string path = "save_@" + std::string(1, GetParam().first) + ".omwsave";
- const std::array arguments {"openmw", "--load-savegame", path.c_str()};
- bpo::variables_map variables;
- parseArgs(arguments, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(),
- "save_" + std::string(1, GetParam().second) + ".omwsave");
- }
-
- INSTANTIATE_TEST_SUITE_P(
- SupportedEscapingsWithAtSign,
- OpenMWOptionsFromArgumentsEscapings,
- ValuesIn(supportedAtSignEscapings)
- );
-
TEST(OpenMWOptionsFromConfig, should_support_single_word_load_savegame_path)
{
bpo::options_description description = makeOptionsDescription();
std::istringstream stream("load-savegame=save.omwsave");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "save.omwsave");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "save.omwsave");
}
TEST(OpenMWOptionsFromConfig, should_strip_quotes_from_load_savegame_path)
@@ -239,7 +211,7 @@ namespace
std::istringstream stream(R"(load-savegame="save.omwsave")");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "save.omwsave");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "save.omwsave");
}
TEST(OpenMWOptionsFromConfig, should_strip_outer_quotes_from_load_savegame_path)
@@ -248,8 +220,7 @@ namespace
std::istringstream stream(R"(load-savegame=""save".omwsave")");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "");
-// EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), R"(""save".omwsave")");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "");
}
TEST(OpenMWOptionsFromConfig, should_strip_quotes_from_load_savegame_path_with_space)
@@ -258,16 +229,16 @@ namespace
std::istringstream stream(R"(load-savegame="my save.omwsave")");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "my save.omwsave");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "my save.omwsave");
}
- TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_number_sign)
+ TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_octothorpe)
{
bpo::options_description description = makeOptionsDescription();
std::istringstream stream("load-savegame=save#.omwsave");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "save#.omwsave");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "save#.omwsave");
}
TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_at_sign)
@@ -276,7 +247,7 @@ namespace
std::istringstream stream("load-savegame=save@.omwsave");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "save@.omwsave");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "save@.omwsave");
}
TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_quote)
@@ -285,7 +256,25 @@ namespace
std::istringstream stream(R"(load-savegame=save".omwsave)");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), R"(save".omwsave)");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), R"(save".omwsave)");
+ }
+
+ TEST(OpenMWOptionsFromConfig, should_support_confusing_savegame_path_with_lots_going_on)
+ {
+ bpo::options_description description = makeOptionsDescription();
+ std::istringstream stream(R"(load-savegame="one &"two"three".omwsave")");
+ bpo::variables_map variables;
+ Files::parseConfig(stream, variables, description);
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), R"(one "two)");
+ }
+
+ TEST(OpenMWOptionsFromConfig, should_support_confusing_savegame_path_with_even_more_going_on)
+ {
+ bpo::options_description description = makeOptionsDescription();
+ std::istringstream stream(R"(load-savegame="one &"two"three ".omwsave")");
+ bpo::variables_map variables;
+ Files::parseConfig(stream, variables, description);
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), R"(one "two)");
}
TEST(OpenMWOptionsFromConfig, should_ignore_commented_option)
@@ -294,7 +283,25 @@ namespace
std::istringstream stream("#load-savegame=save.omwsave");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "");
+ }
+
+ TEST(OpenMWOptionsFromConfig, should_ignore_whitespace_prefixed_commented_option)
+ {
+ bpo::options_description description = makeOptionsDescription();
+ std::istringstream stream(" \t#load-savegame=save.omwsave");
+ bpo::variables_map variables;
+ Files::parseConfig(stream, variables, description);
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "");
+ }
+
+ TEST(OpenMWOptionsFromConfig, should_support_whitespace_around_option)
+ {
+ bpo::options_description description = makeOptionsDescription();
+ std::istringstream stream(" load-savegame = save.omwsave ");
+ bpo::variables_map variables;
+ Files::parseConfig(stream, variables, description);
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "save.omwsave");
}
TEST(OpenMWOptionsFromConfig, should_throw_on_multiple_load_savegame)
@@ -311,7 +318,7 @@ namespace
std::istringstream stream("load-savegame=/home/user/openmw/save.omwsave");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "/home/user/openmw/save.omwsave");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "/home/user/openmw/save.omwsave");
}
TEST(OpenMWOptionsFromConfig, should_support_windows_multi_component_load_savegame_path)
@@ -320,7 +327,7 @@ namespace
std::istringstream stream(R"(load-savegame=C:\OpenMW\save.omwsave)");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), R"(C:\OpenMW\save.omwsave)");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), R"(C:\OpenMW\save.omwsave)");
}
TEST(OpenMWOptionsFromConfig, should_compose_data)
@@ -329,7 +336,7 @@ namespace
std::istringstream stream("data=1\ndata=2");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_THAT(variables["data"].as<Files::EscapePathContainer>(), ElementsAre(IsEscapePath("1"), IsEscapePath("2")));
+ EXPECT_THAT(variables["data"].as<Files::MaybeQuotedPathContainer>(), ElementsAre(IsPath("1"), IsPath("2")));
}
TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_escaped_quote_by_ampersand)
@@ -338,7 +345,7 @@ namespace
std::istringstream stream(R"(load-savegame="save&".omwsave")");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), R"(save".omwsave)");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), R"(save".omwsave)");
}
TEST(OpenMWOptionsFromConfig, should_support_quoted_load_savegame_path_with_escaped_ampersand)
@@ -347,7 +354,7 @@ namespace
std::istringstream stream(R"(load-savegame="save.omwsave&&")");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "save.omwsave&");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "save.omwsave&");
}
TEST(OpenMWOptionsFromConfig, should_support_load_savegame_path_with_ampersand)
@@ -356,7 +363,7 @@ namespace
std::istringstream stream("load-savegame=save&.omwsave");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), "save&.omwsave");
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), "save&.omwsave");
}
struct OpenMWOptionsFromConfigStrings : TestWithParam<std::string> {};
@@ -368,7 +375,7 @@ namespace
std::istringstream stream("load-savegame=\"" + path + "\"");
bpo::variables_map variables;
Files::parseConfig(stream, variables, description);
- EXPECT_EQ(variables["load-savegame"].as<Files::EscapePath>().mPath.string(), path);
+ EXPECT_EQ(variables["load-savegame"].as<Files::MaybeQuotedPath>().string(), path);
}
INSTANTIATE_TEST_SUITE_P(
diff --git a/apps/openmw_test_suite/detournavigator/serialization/binaryreader.cpp b/apps/openmw_test_suite/serialization/binaryreader.cpp
index d071326cf5..cb4f5c57bd 100644
--- a/apps/openmw_test_suite/detournavigator/serialization/binaryreader.cpp
+++ b/apps/openmw_test_suite/serialization/binaryreader.cpp
@@ -1,6 +1,6 @@
#include "format.hpp"
-#include <components/detournavigator/serialization/binaryreader.hpp>
+#include <components/serialization/binaryreader.hpp>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
@@ -12,8 +12,8 @@
namespace
{
using namespace testing;
- using namespace DetourNavigator::Serialization;
- using namespace DetourNavigator::SerializationTesting;
+ using namespace Serialization;
+ using namespace SerializationTesting;
TEST(DetourNavigatorSerializationBinaryReaderTest, shouldReadArithmeticTypeValue)
{
diff --git a/apps/openmw_test_suite/detournavigator/serialization/binarywriter.cpp b/apps/openmw_test_suite/serialization/binarywriter.cpp
index fccc2be3da..cb0f29ba85 100644
--- a/apps/openmw_test_suite/detournavigator/serialization/binarywriter.cpp
+++ b/apps/openmw_test_suite/serialization/binarywriter.cpp
@@ -1,6 +1,6 @@
#include "format.hpp"
-#include <components/detournavigator/serialization/binarywriter.hpp>
+#include <components/serialization/binarywriter.hpp>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
@@ -12,8 +12,8 @@
namespace
{
using namespace testing;
- using namespace DetourNavigator::Serialization;
- using namespace DetourNavigator::SerializationTesting;
+ using namespace Serialization;
+ using namespace SerializationTesting;
TEST(DetourNavigatorSerializationBinaryWriterTest, shouldWriteArithmeticTypeValue)
{
diff --git a/apps/openmw_test_suite/detournavigator/serialization/format.hpp b/apps/openmw_test_suite/serialization/format.hpp
index 7c5e26a0be..8f61838fde 100644
--- a/apps/openmw_test_suite/detournavigator/serialization/format.hpp
+++ b/apps/openmw_test_suite/serialization/format.hpp
@@ -1,12 +1,12 @@
-#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H
-#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H
+#ifndef OPENMW_TEST_SUITE_SERIALIZATION_FORMAT_H
+#define OPENMW_TEST_SUITE_SERIALIZATION_FORMAT_H
-#include <components/detournavigator/serialization/format.hpp>
+#include <components/serialization/format.hpp>
#include <utility>
#include <type_traits>
-namespace DetourNavigator::SerializationTesting
+namespace SerializationTesting
{
struct Pod
{
diff --git a/apps/openmw_test_suite/detournavigator/serialization/integration.cpp b/apps/openmw_test_suite/serialization/integration.cpp
index e7e8eacc20..cb8c711c67 100644
--- a/apps/openmw_test_suite/detournavigator/serialization/integration.cpp
+++ b/apps/openmw_test_suite/serialization/integration.cpp
@@ -1,8 +1,8 @@
#include "format.hpp"
-#include <components/detournavigator/serialization/sizeaccumulator.hpp>
-#include <components/detournavigator/serialization/binarywriter.hpp>
-#include <components/detournavigator/serialization/binaryreader.hpp>
+#include <components/serialization/sizeaccumulator.hpp>
+#include <components/serialization/binarywriter.hpp>
+#include <components/serialization/binaryreader.hpp>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
@@ -12,8 +12,8 @@
namespace
{
using namespace testing;
- using namespace DetourNavigator::Serialization;
- using namespace DetourNavigator::SerializationTesting;
+ using namespace Serialization;
+ using namespace SerializationTesting;
struct DetourNavigatorSerializationIntegrationTest : Test
{
diff --git a/apps/openmw_test_suite/detournavigator/serialization/sizeaccumulator.cpp b/apps/openmw_test_suite/serialization/sizeaccumulator.cpp
index 39b7ea8646..dce148468a 100644
--- a/apps/openmw_test_suite/detournavigator/serialization/sizeaccumulator.cpp
+++ b/apps/openmw_test_suite/serialization/sizeaccumulator.cpp
@@ -1,6 +1,6 @@
#include "format.hpp"
-#include <components/detournavigator/serialization/sizeaccumulator.hpp>
+#include <components/serialization/sizeaccumulator.hpp>
#include <gtest/gtest.h>
@@ -12,8 +12,8 @@
namespace
{
using namespace testing;
- using namespace DetourNavigator::Serialization;
- using namespace DetourNavigator::SerializationTesting;
+ using namespace Serialization;
+ using namespace SerializationTesting;
TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldProvideSizeForArithmeticType)
{
diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp
index e9cec12f5f..198d1df6e7 100644
--- a/apps/wizard/inisettings.cpp
+++ b/apps/wizard/inisettings.cpp
@@ -122,7 +122,7 @@ bool Wizard::IniSettings::writeFile(const QString &path, QTextStream &stream)
QString section(fullKey.at(0));
section.prepend(QLatin1Char('['));
section.append(QLatin1Char(']'));
- QString key(fullKey.at(1));
+ const QString& key(fullKey.at(1));
int index = buffer.lastIndexOf(section);
if (index == -1) {
diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp
index d92f5b029c..064f0813d8 100644
--- a/apps/wizard/mainwizard.cpp
+++ b/apps/wizard/mainwizard.cpp
@@ -88,8 +88,9 @@ void Wizard::MainWizard::setupLog()
msgBox.setIcon(QMessageBox::Critical);
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.setText(mLogError.arg(file.fileName()));
+ connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection);
msgBox.exec();
- return qApp->quit();
+ return;
}
addLogText(QString("Started OpenMW Wizard on %1").arg(QDateTime::currentDateTime().toString()));
@@ -110,8 +111,9 @@ void Wizard::MainWizard::addLogText(const QString &text)
msgBox.setIcon(QMessageBox::Critical);
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.setText(mLogError.arg(file.fileName()));
+ connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection);
msgBox.exec();
- return qApp->quit();
+ return;
}
if (!file.isSequential())
@@ -148,8 +150,9 @@ void Wizard::MainWizard::setupGameSettings()
msgBox.setIcon(QMessageBox::Critical);
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.setText(message.arg(file.fileName()));
+ connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection);
msgBox.exec();
- return qApp->quit();
+ return;
}
QTextStream stream(&file);
stream.setCodec(QTextCodec::codecForName("UTF-8"));
@@ -177,8 +180,9 @@ void Wizard::MainWizard::setupGameSettings()
msgBox.setIcon(QMessageBox::Critical);
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.setText(message.arg(file.fileName()));
-
- return qApp->quit();
+ connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection);
+ msgBox.exec();
+ return;
}
QTextStream stream(&file);
stream.setCodec(QTextCodec::codecForName("UTF-8"));
@@ -210,8 +214,9 @@ void Wizard::MainWizard::setupLauncherSettings()
msgBox.setIcon(QMessageBox::Critical);
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.setText(message.arg(file.fileName()));
+ connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection);
msgBox.exec();
- return qApp->quit();
+ return;
}
QTextStream stream(&file);
stream.setCodec(QTextCodec::codecForName("UTF-8"));
@@ -394,8 +399,9 @@ void Wizard::MainWizard::writeSettings()
msgBox.setText(tr("<html><head/><body><p><b>Could not create %1</b></p> \
<p>Please make sure you have the right permissions \
and try again.</p></body></html>").arg(userPath));
+ connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection);
msgBox.exec();
- return qApp->quit();
+ return;
}
}
@@ -411,8 +417,9 @@ void Wizard::MainWizard::writeSettings()
msgBox.setText(tr("<html><head/><body><p><b>Could not open %1 for writing</b></p> \
<p>Please make sure you have the right permissions \
and try again.</p></body></html>").arg(file.fileName()));
+ connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection);
msgBox.exec();
- return qApp->quit();
+ return;
}
QTextStream stream(&file);
@@ -433,8 +440,9 @@ void Wizard::MainWizard::writeSettings()
msgBox.setText(tr("<html><head/><body><p><b>Could not open %1 for writing</b></p> \
<p>Please make sure you have the right permissions \
and try again.</p></body></html>").arg(file.fileName()));
+ connect(&msgBox, &QDialog::finished, qApp, &QApplication::quit, Qt::QueuedConnection);
msgBox.exec();
- return qApp->quit();
+ return;
}
stream.setDevice(&file);
diff --git a/cmake/FindGMock.cmake b/cmake/FindGMock.cmake
index 502910a9df..bc3410969f 100644
--- a/cmake/FindGMock.cmake
+++ b/cmake/FindGMock.cmake
@@ -164,8 +164,16 @@ find_dependency(Threads)
set_target_properties(GMock::GMock PROPERTIES
INTERFACE_LINK_LIBRARIES "Threads::Threads"
- IMPORTED_LINK_INTERFACE_LANGUAGES "CXX"
- IMPORTED_LOCATION "${GMOCK_LIBRARY}")
+ IMPORTED_LINK_INTERFACE_LANGUAGES "CXX")
+
+if(EXISTS "${GMOCK_LIBRARY}")
+ set_target_properties(GMock::GMock PROPERTIES
+ IMPORTED_LOCATION "${GMOCK_LIBRARY}")
+endif()
+if(EXISTS "${GMOCK_LIBRARY_DEBUG}")
+ set_target_properties(GMock::GMock PROPERTIES
+ IMPORTED_LOCATION_DEBUG "${GMOCK_LIBRARY_DEBUG}")
+endif()
if(GMOCK_INCLUDE_DIR)
set_target_properties(GMock::GMock PROPERTIES
@@ -182,8 +190,16 @@ endif()
set_target_properties(GMock::Main PROPERTIES
INTERFACE_LINK_LIBRARIES "GMock::GMock"
- IMPORTED_LINK_INTERFACE_LANGUAGES "CXX"
- IMPORTED_LOCATION "${GMOCK_MAIN_LIBRARY}")
+ IMPORTED_LINK_INTERFACE_LANGUAGES "CXX")
+
+if(EXISTS "${GMOCK_MAIN_LIBRARY}")
+ set_target_properties(GMock::Main PROPERTIES
+ IMPORTED_LOCATION "${GMOCK_MAIN_LIBRARY}")
+endif()
+if(EXISTS "${GMOCK_MAIN_LIBRARY_DEBUG}")
+ set_target_properties(GMock::Main PROPERTIES
+ IMPORTED_LOCATION "${GMOCK_MAIN_LIBRARY_DEBUG}")
+endif()
if(GMOCK_FOUND)
set(GMOCK_INCLUDE_DIRS ${GMOCK_INCLUDE_DIR})
diff --git a/cmake/FindLZ4.cmake b/cmake/FindLZ4.cmake
index ec854c6b18..5b148cb64e 100644
--- a/cmake/FindLZ4.cmake
+++ b/cmake/FindLZ4.cmake
@@ -95,6 +95,8 @@ include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(LZ4 DEFAULT_MSG
LZ4_LIBRARY LZ4_INCLUDE_DIR LZ4_VERSION)
+set(LZ4_INCLUDE_DIR ${LZ4_INCLUDE_DIR} CACHE PATH "LZ4 include dir hint")
+set(LZ4_LIBRARY ${LZ4_LIBRARY} CACHE FILEPATH "LZ4 library path hint")
mark_as_advanced(LZ4_INCLUDE_DIR LZ4_LIBRARY)
set(LZ4_LIBRARIES ${LZ4_LIBRARY})
diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt
index a3f77d86bf..f7382db1c7 100644
--- a/components/CMakeLists.txt
+++ b/components/CMakeLists.txt
@@ -29,7 +29,7 @@ endif (GIT_CHECKOUT)
# source files
add_component_dir (lua
- luastate scriptscontainer utilpackage serialization configuration
+ luastate scriptscontainer utilpackage serialization configuration i18n storage
)
add_component_dir (settings
@@ -37,7 +37,7 @@ add_component_dir (settings
)
add_component_dir (bsa
- bsa_file compressedbsafile memorystream
+ bsa_file compressedbsafile
)
add_component_dir (vfs
@@ -57,11 +57,11 @@ add_component_dir (sceneutil
clone attach visitor util statesetupdater controller skeleton riggeometry morphgeometry lightcontroller
lightmanager lightutil positionattitudetransform workqueue pathgridutil waterutil writescene serialize optimizer
actorutil detourdebugdraw navmesh agentpath shadow mwshadowtechnique recastmesh shadowsbin osgacontroller rtt
- screencapture
+ screencapture depth
)
add_component_dir (nif
- controlled effect niftypes record controller extra node record_ptr data niffile property nifkey base nifstream
+ controlled effect niftypes record controller extra node record_ptr data niffile property nifkey base nifstream physics
)
add_component_dir (nifosg
@@ -93,7 +93,7 @@ add_component_dir (esmterrain
add_component_dir (misc
constants utf8stream stringops resourcehelpers rng messageformatparser weakcache thread
- compression
+ compression osguservalues errorMarker
)
add_component_dir (debug
@@ -105,8 +105,8 @@ IF(NOT WIN32 AND NOT APPLE)
add_definitions(-DGLOBAL_CONFIG_PATH="${GLOBAL_CONFIG_PATH}")
ENDIF()
add_component_dir (files
- linuxpath androidpath windowspath macospath fixedpath multidircollection collections configurationmanager escape
- lowlevelfile constrainedfilestream memorystream
+ linuxpath androidpath windowspath macospath fixedpath multidircollection collections configurationmanager
+ lowlevelfile constrainedfilestream memorystream hash configfileparser
)
add_component_dir (compiler
@@ -146,7 +146,7 @@ add_component_dir (fontloader
)
add_component_dir (sdlutil
- gl4es_init sdlgraphicswindow imagetosurface sdlinputwrapper sdlvideowrapper events sdlcursormanager
+ gl4es_init sdlgraphicswindow imagetosurface sdlinputwrapper sdlvideowrapper events sdlcursormanager sdlmappings
)
add_component_dir (version
@@ -160,6 +160,12 @@ add_component_dir (fallback
add_component_dir (queries
query luabindings
)
+
+add_component_dir (lua_ui
+ widget widgetlist element layers content
+ text textedit window
+ )
+
if(WIN32)
add_component_dir (crashcatcher
@@ -196,6 +202,12 @@ add_component_dir(detournavigator
offmeshconnectionsmanager
preparednavmeshdata
navmeshcacheitem
+ navigatorutils
+ generatenavmeshtile
+ navmeshdb
+ serialization
+ navmeshdbutils
+ recast
)
add_component_dir(loadinglistener
@@ -276,6 +288,7 @@ target_link_libraries(components
${SDL2_LIBRARIES}
${OPENGL_gl_LIBRARY}
${MyGUI_LIBRARIES}
+ ${LUA_LIBRARIES}
LZ4::LZ4
RecastNavigation::DebugUtils
RecastNavigation::Detour
@@ -283,6 +296,7 @@ target_link_libraries(components
Base64
SQLite::SQLite3
+ smhasher
)
target_link_libraries(components ${BULLET_LIBRARIES})
@@ -363,3 +377,7 @@ endif(OSG_STATIC)
if(USE_QT)
set_property(TARGET components_qt PROPERTY AUTOMOC ON)
endif(USE_QT)
+
+if (MSVC AND CMAKE_VERSION VERSION_GREATER_EQUAL 3.16)
+ target_precompile_headers(components PRIVATE ${SOL_INCLUDE_DIR}/sol/sol.hpp)
+endif ()
diff --git a/components/bsa/memorystream.cpp b/components/bsa/memorystream.cpp
deleted file mode 100644
index 34e98e6b68..0000000000
--- a/components/bsa/memorystream.cpp
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- OpenMW - The completely unofficial reimplementation of Morrowind
- Copyright (C) 2008-2010 Nicolay Korslund
- Email: < korslund@gmail.com >
- WWW: http://openmw.sourceforge.net/
-
- This file (memorystream.cpp) is part of the OpenMW package.
-
- OpenMW is distributed as free software: you can redistribute it
- and/or modify it under the terms of the GNU General Public License
- version 3, as published by the Free Software Foundation.
-
- This program is distributed in the hope that it will be useful, but
- WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- version 3 along with this program. If not, see
- http://www.gnu.org/licenses/ .
-
- Compressed BSA upgrade added by Azdul 2019
-
- */
-#include "memorystream.hpp"
-
-
-namespace Bsa
-{
-MemoryInputStreamBuf::MemoryInputStreamBuf(size_t bufferSize) : mBufferPtr(bufferSize)
-{
- this->setg(mBufferPtr.data(), mBufferPtr.data(), mBufferPtr.data() + bufferSize);
-}
-
-char* MemoryInputStreamBuf::getRawData() {
- return mBufferPtr.data();
-}
-
-MemoryInputStream::MemoryInputStream(size_t bufferSize) :
- MemoryInputStreamBuf(bufferSize),
- std::istream(static_cast<std::streambuf*>(this)) {
-
-}
-
-char* MemoryInputStream::getRawData() {
- return MemoryInputStreamBuf::getRawData();
-}
-}
diff --git a/components/bsa/memorystream.hpp b/components/bsa/memorystream.hpp
index d168e93d65..5aae448299 100644
--- a/components/bsa/memorystream.hpp
+++ b/components/bsa/memorystream.hpp
@@ -28,23 +28,11 @@
#include <vector>
#include <iostream>
+#include <components/files/memorystream.hpp>
namespace Bsa
{
/**
-Class used internally by MemoryInputStream.
-*/
-class MemoryInputStreamBuf : public std::streambuf {
-
-public:
- explicit MemoryInputStreamBuf(size_t bufferSize);
- virtual char* getRawData();
-private:
- //correct call to delete [] on C++ 11
- std::vector<char> mBufferPtr;
-};
-
-/**
Class replaces Ogre memory streams without introducing any new external dependencies
beyond standard library.
@@ -52,10 +40,18 @@ private:
Memory buffer is freed once the class instance is destroyed.
*/
-class MemoryInputStream : virtual MemoryInputStreamBuf, std::istream {
+class MemoryInputStream : private std::vector<char>, public virtual Files::MemBuf, public std::istream {
public:
- explicit MemoryInputStream(size_t bufferSize);
- char* getRawData() override;
+ explicit MemoryInputStream(size_t bufferSize)
+ : std::vector<char>(bufferSize)
+ , Files::MemBuf(this->data(), this->size())
+ , std::istream(static_cast<std::streambuf*>(this))
+ {}
+
+ char* getRawData()
+ {
+ return this->data();
+ }
};
}
diff --git a/components/compiler/discardparser.cpp b/components/compiler/discardparser.cpp
index 0e7c4718cb..0a714d4eb6 100644
--- a/components/compiler/discardparser.cpp
+++ b/components/compiler/discardparser.cpp
@@ -12,7 +12,7 @@ namespace Compiler
bool DiscardParser::parseInt (int value, const TokenLoc& loc, Scanner& scanner)
{
- if (mState==StartState || mState==CommaState || mState==MinusState)
+ if (mState==StartState || mState==MinusState)
{
if (isEmpty())
mTokenLoc = loc;
@@ -26,7 +26,7 @@ namespace Compiler
bool DiscardParser::parseFloat (float value, const TokenLoc& loc, Scanner& scanner)
{
- if (mState==StartState || mState==CommaState || mState==MinusState)
+ if (mState==StartState || mState==MinusState)
{
if (isEmpty())
mTokenLoc = loc;
@@ -41,7 +41,7 @@ namespace Compiler
bool DiscardParser::parseName (const std::string& name, const TokenLoc& loc,
Scanner& scanner)
{
- if (mState==StartState || mState==CommaState)
+ if (mState==StartState)
{
if (isEmpty())
mTokenLoc = loc;
@@ -55,18 +55,7 @@ namespace Compiler
bool DiscardParser::parseSpecial (int code, const TokenLoc& loc, Scanner& scanner)
{
- if (code==Scanner::S_comma && mState==StartState)
- {
- if (isEmpty())
- mTokenLoc = loc;
-
- start();
-
- mState = CommaState;
- return true;
- }
-
- if (code==Scanner::S_minus && (mState==StartState || mState==CommaState))
+ if (code==Scanner::S_minus && mState==StartState)
{
if (isEmpty())
mTokenLoc = loc;
diff --git a/components/compiler/discardparser.hpp b/components/compiler/discardparser.hpp
index 15e06756e2..5286676654 100644
--- a/components/compiler/discardparser.hpp
+++ b/components/compiler/discardparser.hpp
@@ -11,7 +11,7 @@ namespace Compiler
{
enum State
{
- StartState, CommaState, MinusState
+ StartState, MinusState
};
State mState;
diff --git a/components/compiler/exprparser.cpp b/components/compiler/exprparser.cpp
index 1aedc8dc59..668946f839 100644
--- a/components/compiler/exprparser.cpp
+++ b/components/compiler/exprparser.cpp
@@ -244,7 +244,6 @@ namespace Compiler
}
else
{
- // no comma was used between arguments
scanner.putbackInt (value, loc);
return false;
}
@@ -267,7 +266,6 @@ namespace Compiler
}
else
{
- // no comma was used between arguments
scanner.putbackFloat (value, loc);
return false;
}
@@ -343,7 +341,6 @@ namespace Compiler
}
else
{
- // no comma was used between arguments
scanner.putbackName (name, loc);
return false;
}
@@ -452,7 +449,6 @@ namespace Compiler
}
else
{
- // no comma was used between arguments
scanner.putbackKeyword (keyword, loc);
return false;
}
@@ -487,22 +483,6 @@ namespace Compiler
return Parser::parseSpecial (code, loc, scanner);
}
- if (code==Scanner::S_comma)
- {
- mTokenLoc = loc;
-
- if (mFirst)
- {
- // leading comma
- mFirst = false;
- return true;
- }
-
- // end marker
- scanner.putbackSpecial (code, loc);
- return false;
- }
-
mFirst = false;
if (code==Scanner::S_newline)
@@ -539,7 +519,6 @@ namespace Compiler
}
else
{
- // no comma was used between arguments
scanner.putbackSpecial (code, loc);
return false;
}
diff --git a/components/compiler/fileparser.cpp b/components/compiler/fileparser.cpp
index c7459c2ae7..9d5c02785e 100644
--- a/components/compiler/fileparser.cpp
+++ b/components/compiler/fileparser.cpp
@@ -121,11 +121,6 @@ namespace Compiler
return false;
}
}
- else if (code==Scanner::S_comma && (mState==NameState || mState==EndNameState))
- {
- // ignoring comma (for now)
- return true;
- }
return Parser::parseSpecial (code, loc, scanner);
}
diff --git a/components/compiler/lineparser.cpp b/components/compiler/lineparser.cpp
index ec90812ec7..3ad8c5bbe2 100644
--- a/components/compiler/lineparser.cpp
+++ b/components/compiler/lineparser.cpp
@@ -12,7 +12,6 @@
#include "generator.hpp"
#include "extensions.hpp"
#include "declarationparser.hpp"
-#include "exception.hpp"
namespace Compiler
{
@@ -136,7 +135,7 @@ namespace Compiler
return false;
}
- if (mState==MessageState || mState==MessageCommaState)
+ if (mState==MessageState)
{
GetArgumentsFromMessageFormat processor;
processor.process(name);
@@ -155,7 +154,7 @@ namespace Compiler
return true;
}
- if (mState==MessageButtonState || mState==MessageButtonCommaState)
+ if (mState==MessageButtonState)
{
Generator::pushString (mCode, mLiterals, name);
mState = MessageButtonState;
@@ -198,7 +197,7 @@ namespace Compiler
bool LineParser::parseKeyword (int keyword, const TokenLoc& loc, Scanner& scanner)
{
- if (mState==MessageState || mState==MessageCommaState)
+ if (mState==MessageState)
{
if (const Extensions *extensions = getContext().getExtensions())
{
@@ -259,33 +258,11 @@ namespace Compiler
mExplicit.clear();
}
- try
- {
- // workaround for broken positioncell instructions.
- /// \todo add option to disable this
- std::unique_ptr<ErrorDowngrade> errorDowngrade (nullptr);
- if (Misc::StringUtils::lowerCase (loc.mLiteral)=="positioncell")
- errorDowngrade = std::make_unique<ErrorDowngrade> (getErrorHandler());
-
- std::vector<Interpreter::Type_Code> code;
- int optionals = mExprParser.parseArguments (argumentType, scanner, code, keyword);
- mCode.insert (mCode.end(), code.begin(), code.end());
- extensions->generateInstructionCode (keyword, mCode, mLiterals,
- mExplicit, optionals);
- }
- catch (const SourceException&)
- {
- // Ignore argument exceptions for positioncell.
- /// \todo add option to disable this
- if (Misc::StringUtils::lowerCase (loc.mLiteral)=="positioncell")
- {
- SkipParser skip (getErrorHandler(), getContext());
- scanner.scan (skip);
- return false;
- }
-
- throw;
- }
+ std::vector<Interpreter::Type_Code> code;
+ int optionals = mExprParser.parseArguments (argumentType, scanner, code, keyword);
+ mCode.insert (mCode.end(), code.begin(), code.end());
+ extensions->generateInstructionCode (keyword, mCode, mLiterals,
+ mExplicit, optionals);
mState = EndState;
return true;
@@ -446,12 +423,6 @@ namespace Compiler
if (code==Scanner::S_newline && (mState==EndState || mState==BeginState))
return false;
- if (code==Scanner::S_comma && mState==MessageState)
- {
- mState = MessageCommaState;
- return true;
- }
-
if (code==Scanner::S_ref && mState==SetPotentialMemberVarState)
{
getErrorHandler().warning ("Stray explicit reference", loc);
@@ -479,12 +450,6 @@ namespace Compiler
return false;
}
- if (code==Scanner::S_comma && mState==MessageButtonState)
- {
- mState = MessageButtonCommaState;
- return true;
- }
-
if (code==Scanner::S_member && mState==SetPotentialMemberVarState)
{
mState = SetMemberVarState;
diff --git a/components/compiler/lineparser.hpp b/components/compiler/lineparser.hpp
index c434792d18..2a0e5d6630 100644
--- a/components/compiler/lineparser.hpp
+++ b/components/compiler/lineparser.hpp
@@ -24,7 +24,7 @@ namespace Compiler
BeginState,
SetState, SetLocalVarState, SetGlobalVarState, SetPotentialMemberVarState,
SetMemberVarState, SetMemberVarState2,
- MessageState, MessageCommaState, MessageButtonState, MessageButtonCommaState,
+ MessageState, MessageButtonState,
EndState, PotentialExplicitState, ExplicitState, MemberState
};
diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp
index 0e2b76cb23..6cf2d6e7c0 100644
--- a/components/compiler/scanner.cpp
+++ b/components/compiler/scanner.cpp
@@ -531,8 +531,6 @@ namespace Compiler
else
special = S_cmpGT;
}
- else if (c==',')
- special = S_comma;
else if (c=='+')
special = S_plus;
else if (c=='*')
@@ -552,8 +550,6 @@ namespace Compiler
mTolerantNames = tolerant;
return out;
}
- else if (expectName && special == S_comma)
- mExpectName = true;
TokenLoc loc (mLoc);
mLoc.mLiteral.clear();
diff --git a/components/compiler/scanner.hpp b/components/compiler/scanner.hpp
index 8ee2672132..7a77811f2a 100644
--- a/components/compiler/scanner.hpp
+++ b/components/compiler/scanner.hpp
@@ -63,7 +63,7 @@ namespace Compiler
bool isWhitespace()
{
- return (mData[0]==' ' || mData[0]=='\t') && mData[1]==0 && mData[2]==0 && mData[3]==0;
+ return (mData[0]==' ' || mData[0]=='\t' || mData[0]==',') && mData[1]==0 && mData[2]==0 && mData[3]==0;
}
bool isDigit()
@@ -214,7 +214,6 @@ namespace Compiler
S_open, S_close,
S_cmpEQ, S_cmpNE, S_cmpLT, S_cmpLE, S_cmpGT, S_cmpGE,
S_plus, S_minus, S_mult, S_div,
- S_comma,
S_ref,
S_member
};
diff --git a/components/compiler/stringparser.cpp b/components/compiler/stringparser.cpp
index 4e0114e0a1..a6423211c2 100644
--- a/components/compiler/stringparser.cpp
+++ b/components/compiler/stringparser.cpp
@@ -13,7 +13,7 @@
namespace Compiler
{
StringParser::StringParser (ErrorHandler& errorHandler, const Context& context, Literals& literals)
- : Parser (errorHandler, context), mLiterals (literals), mState (StartState), mSmashCase (false), mDiscard (false)
+ : Parser (errorHandler, context), mLiterals (literals), mSmashCase (false), mDiscard (false)
{
}
@@ -21,23 +21,18 @@ namespace Compiler
bool StringParser::parseName (const std::string& name, const TokenLoc& loc,
Scanner& scanner)
{
- if (mState==StartState || mState==CommaState)
- {
- start();
- mTokenLoc = loc;
-
- if (!mDiscard)
- {
- if (mSmashCase)
- Generator::pushString (mCode, mLiterals, Misc::StringUtils::lowerCase (name));
- else
- Generator::pushString (mCode, mLiterals, name);
- }
+ start();
+ mTokenLoc = loc;
- return false;
+ if (!mDiscard)
+ {
+ if (mSmashCase)
+ Generator::pushString (mCode, mLiterals, Misc::StringUtils::lowerCase (name));
+ else
+ Generator::pushString (mCode, mLiterals, name);
}
- return Parser::parseName (name, loc, scanner);
+ return false;
}
bool StringParser::parseKeyword (int keyword, const TokenLoc& loc, Scanner& scanner)
@@ -75,17 +70,6 @@ namespace Compiler
return Parser::parseKeyword (keyword, loc, scanner);
}
- bool StringParser::parseSpecial (int code, const TokenLoc& loc, Scanner& scanner)
- {
- if (code==Scanner::S_comma && mState==StartState)
- {
- mState = CommaState;
- return true;
- }
-
- return Parser::parseSpecial (code, loc, scanner);
- }
-
bool StringParser::parseInt (int value, const TokenLoc& loc, Scanner& scanner)
{
reportWarning("Treating integer argument as a string", loc);
@@ -99,7 +83,6 @@ namespace Compiler
void StringParser::reset()
{
- mState = StartState;
mCode.clear();
mSmashCase = false;
mTokenLoc = TokenLoc();
diff --git a/components/compiler/stringparser.hpp b/components/compiler/stringparser.hpp
index 07b61d8fda..cdc7c676a7 100644
--- a/components/compiler/stringparser.hpp
+++ b/components/compiler/stringparser.hpp
@@ -14,13 +14,7 @@ namespace Compiler
class StringParser : public Parser
{
- enum State
- {
- StartState, CommaState
- };
-
Literals& mLiterals;
- State mState;
std::vector<Interpreter::Type_Code> mCode;
bool mSmashCase;
TokenLoc mTokenLoc;
@@ -39,10 +33,6 @@ namespace Compiler
///< Handle a keyword token.
/// \return fetch another token?
- bool parseSpecial (int code, const TokenLoc& loc, Scanner& scanner) override;
- ///< Handle a special character token.
- /// \return fetch another token?
-
bool parseInt (int value, const TokenLoc& loc, Scanner& scanner) override;
///< Handle an int token.
/// \return fetch another token?
diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp
index 208b1315f3..199799025a 100644
--- a/components/contentselector/model/contentmodel.cpp
+++ b/components/contentselector/model/contentmodel.cpp
@@ -2,6 +2,7 @@
#include "esmfile.hpp"
#include <stdexcept>
+#include <unordered_set>
#include <QDir>
#include <QTextCodec>
@@ -420,6 +421,7 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path)
if (mShowOMWScripts)
filters << "*.omwscripts";
dir.setNameFilters(filters);
+ dir.setSorting(QDir::Name);
for (const QString &path2 : dir.entryList())
{
@@ -428,6 +430,10 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path)
if (item(info.fileName()))
continue;
+ // Enabled by default in system openmw.cfg; shouldn't be shown in content list.
+ if (info.fileName().compare("builtin.omwscripts", Qt::CaseInsensitive) == 0)
+ continue;
+
if (info.fileName().endsWith(".omwscripts", Qt::CaseInsensitive))
{
EsmFile *file = new EsmFile(path2);
@@ -474,8 +480,6 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path)
}
}
-
- sortFiles();
}
void ContentSelectorModel::ContentModel::clearFiles()
@@ -504,41 +508,37 @@ QStringList ContentSelectorModel::ContentModel::gameFiles() const
void ContentSelectorModel::ContentModel::sortFiles()
{
- //first, sort the model such that all dependencies are ordered upstream (gamefile) first.
- bool movedFiles = true;
- int fileCount = mFiles.size();
-
+ emit layoutAboutToBeChanged();
//Dependency sort
- //iterate until no sorting of files occurs
- while (movedFiles)
+ std::unordered_set<const EsmFile*> moved;
+ for(int i = mFiles.size() - 1; i > 0;)
{
- movedFiles = false;
- //iterate each file, obtaining a reference to it's gamefiles list
- for (int i = 0; i < fileCount; i++)
+ const auto file = mFiles.at(i);
+ if(moved.find(file) == moved.end())
{
- QModelIndex idx1 = index (i, 0, QModelIndex());
- const QStringList &gamefiles = mFiles.at(i)->gameFiles();
- //iterate each file after the current file, verifying that none of it's
- //dependencies appear.
- for (int j = i + 1; j < fileCount; j++)
+ int index = -1;
+ for(int j = 0; j < i; ++j)
{
- if (gamefiles.contains(mFiles.at(j)->fileName(), Qt::CaseInsensitive)
- || (!mFiles.at(i)->isGameFile() && gamefiles.isEmpty()
- && mFiles.at(j)->fileName().compare("Morrowind.esm", Qt::CaseInsensitive) == 0)) // Hack: implicit dependency on Morrowind.esm for dependency-less files
+ const QStringList& gameFiles = mFiles.at(j)->gameFiles();
+ if(gameFiles.contains(file->fileName(), Qt::CaseInsensitive)
+ || (!mFiles.at(j)->isGameFile() && gameFiles.isEmpty()
+ && file->fileName().compare("Morrowind.esm", Qt::CaseInsensitive) == 0)) // Hack: implicit dependency on Morrowind.esm for dependency-less files
{
- mFiles.move(j, i);
-
- QModelIndex idx2 = index (j, 0, QModelIndex());
-
- emit dataChanged (idx1, idx2);
-
- movedFiles = true;
+ index = j;
+ break;
}
}
- if (movedFiles)
- break;
+ if(index >= 0)
+ {
+ mFiles.move(i, index);
+ moved.insert(file);
+ continue;
+ }
}
+ --i;
+ moved.clear();
}
+ emit layoutChanged();
}
bool ContentSelectorModel::ContentModel::isChecked(const QString& filepath) const
diff --git a/components/contentselector/model/contentmodel.hpp b/components/contentselector/model/contentmodel.hpp
index f8130e3649..4bbe73b427 100644
--- a/components/contentselector/model/contentmodel.hpp
+++ b/components/contentselector/model/contentmodel.hpp
@@ -44,6 +44,7 @@ namespace ContentSelectorModel
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
void addFiles(const QString &path);
+ void sortFiles();
void clearFiles();
QModelIndex indexFromItem(const EsmFile *item) const;
@@ -68,8 +69,6 @@ namespace ContentSelectorModel
void addFile(EsmFile *file);
- void sortFiles();
-
/// Checks a specific plug-in for load order errors
/// \return all errors found for specific plug-in
QList<LoadOrderError> checkForLoadOrderErrors(const EsmFile *file, int row) const;
diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp
index f18e80dd0a..ef925148ab 100644
--- a/components/contentselector/view/contentselector.cpp
+++ b/components/contentselector/view/contentselector.cpp
@@ -173,6 +173,11 @@ void ContentSelectorView::ContentSelector::addFiles(const QString &path)
mContentModel->checkForLoadOrderErrors();
}
+void ContentSelectorView::ContentSelector::sortFiles()
+{
+ mContentModel->sortFiles();
+}
+
void ContentSelectorView::ContentSelector::clearFiles()
{
mContentModel->clearFiles();
diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp
index 4a9983c1bf..b40675bedc 100644
--- a/components/contentselector/view/contentselector.hpp
+++ b/components/contentselector/view/contentselector.hpp
@@ -28,6 +28,7 @@ namespace ContentSelectorView
QString currentFile() const;
void addFiles(const QString &path);
+ void sortFiles();
void clearFiles();
void setProfileContent (const QStringList &fileList);
diff --git a/components/crashcatcher/crashcatcher.cpp b/components/crashcatcher/crashcatcher.cpp
index 86571e1e3a..c828e1ca81 100644
--- a/components/crashcatcher/crashcatcher.cpp
+++ b/components/crashcatcher/crashcatcher.cpp
@@ -56,8 +56,6 @@ static const char exec_err[] = "!!! Failed to exec debug process\n";
static char argv0[PATH_MAX];
-static char altstack[SIGSTKSZ];
-
static struct {
int signum;
@@ -475,9 +473,10 @@ int crashCatcherInstallHandlers(int argc, char **argv, int num_signals, int *sig
/* Set an alternate signal stack so SIGSEGVs caused by stack overflows
* still run */
+ static char* altstack = new char [SIGSTKSZ];
altss.ss_sp = altstack;
altss.ss_flags = 0;
- altss.ss_size = sizeof(altstack);
+ altss.ss_size = SIGSTKSZ;
sigaltstack(&altss, nullptr);
memset(&sa, 0, sizeof(sa));
diff --git a/components/crashcatcher/windows_crashcatcher.cpp b/components/crashcatcher/windows_crashcatcher.cpp
index 39ac86d7b8..e9eb60e845 100644
--- a/components/crashcatcher/windows_crashcatcher.cpp
+++ b/components/crashcatcher/windows_crashcatcher.cpp
@@ -1,10 +1,11 @@
+#include "windows_crashcatcher.hpp"
+
#include <cassert>
#include <cwchar>
#include <iostream>
#include <sstream>
#include <thread>
-#include "windows_crashcatcher.hpp"
#include "windows_crashmonitor.hpp"
#include "windows_crashshm.hpp"
#include <SDL_messagebox.h>
@@ -144,6 +145,7 @@ namespace Crash
mShm->mEvent = CrashSHM::Event::Startup;
mShm->mStartup.mShmMutex = duplicateHandle(mShmMutex);
mShm->mStartup.mAppProcessHandle = duplicateHandle(GetCurrentProcess());
+ mShm->mStartup.mAppMainThreadId = GetThreadId(GetCurrentThread());
mShm->mStartup.mSignalApp = duplicateHandle(mSignalAppEvent);
mShm->mStartup.mSignalMonitor = duplicateHandle(mSignalMonitorEvent);
@@ -196,7 +198,7 @@ namespace Crash
// must remain until monitor has finished
waitMonitor();
- std::string message = "OpenMW has encountered a fatal error.\nCrash log saved to '" + std::string(mShm->mStartup.mLogFilePath) + "'.\n Please report this to https://gitlab.com/OpenMW/openmw/issues !";
+ std::string message = "OpenMW has encountered a fatal error.\nCrash log saved to '" + std::string(mShm->mStartup.mLogFilePath) + "'.\nPlease report this to https://gitlab.com/OpenMW/openmw/issues !";
SDL_ShowSimpleMessageBox(0, "Fatal Error", message.c_str(), nullptr);
}
diff --git a/components/crashcatcher/windows_crashmonitor.cpp b/components/crashcatcher/windows_crashmonitor.cpp
index 8976deb2ea..50d3fc08a1 100644
--- a/components/crashcatcher/windows_crashmonitor.cpp
+++ b/components/crashcatcher/windows_crashmonitor.cpp
@@ -1,3 +1,5 @@
+#include "windows_crashmonitor.hpp"
+
#undef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
@@ -9,13 +11,15 @@
#include <memory>
#include <sstream>
+#include <SDL_messagebox.h>
+
#include "windows_crashcatcher.hpp"
-#include "windows_crashmonitor.hpp"
#include "windows_crashshm.hpp"
#include <components/debug/debuglog.hpp>
namespace Crash
{
+ std::unordered_map<HWINEVENTHOOK, CrashMonitor*> CrashMonitor::smEventHookOwners{};
CrashMonitor::CrashMonitor(HANDLE shmHandle)
: mShmHandle(shmHandle)
@@ -28,6 +32,7 @@ namespace Crash
mShmMutex = mShm->mStartup.mShmMutex;
mAppProcessHandle = mShm->mStartup.mAppProcessHandle;
+ mAppMainThreadId = mShm->mStartup.mAppMainThreadId;
mSignalAppEvent = mShm->mStartup.mSignalApp;
mSignalMonitorEvent = mShm->mStartup.mSignalMonitor;
}
@@ -80,6 +85,61 @@ namespace Crash
return code == STILL_ACTIVE;
}
+ bool CrashMonitor::isAppFrozen()
+ {
+ MSG message;
+ // Allow the event hook callback to run
+ PeekMessage(&message, nullptr, 0, 0, PM_NOREMOVE);
+
+ if (!mAppWindowHandle)
+ {
+ EnumWindows([](HWND handle, LPARAM param) -> BOOL {
+ CrashMonitor& crashMonitor = *(CrashMonitor*)param;
+ DWORD processId;
+ if (GetWindowThreadProcessId(handle, &processId) == crashMonitor.mAppMainThreadId && processId == GetProcessId(crashMonitor.mAppProcessHandle))
+ {
+ if (GetWindow(handle, GW_OWNER) == 0)
+ {
+ crashMonitor.mAppWindowHandle = handle;
+ return false;
+ }
+ }
+ return true;
+ }, (LPARAM)this);
+ if (mAppWindowHandle)
+ {
+ DWORD processId;
+ GetWindowThreadProcessId(mAppWindowHandle, &processId);
+ HWINEVENTHOOK eventHookHandle = SetWinEventHook(EVENT_OBJECT_DESTROY, EVENT_OBJECT_DESTROY, nullptr,
+ [](HWINEVENTHOOK hWinEventHook, DWORD event, HWND windowHandle, LONG objectId, LONG childId, DWORD eventThread, DWORD eventTime)
+ {
+ CrashMonitor& crashMonitor = *smEventHookOwners[hWinEventHook];
+ if (event == EVENT_OBJECT_DESTROY && windowHandle == crashMonitor.mAppWindowHandle && objectId == OBJID_WINDOW && childId == INDEXID_CONTAINER)
+ {
+ crashMonitor.mAppWindowHandle = nullptr;
+ smEventHookOwners.erase(hWinEventHook);
+ UnhookWinEvent(hWinEventHook);
+ }
+ }, processId, mAppMainThreadId, WINEVENT_OUTOFCONTEXT);
+ smEventHookOwners[eventHookHandle] = this;
+ }
+ else
+ return false;
+ }
+ if (IsHungAppWindow)
+ return IsHungAppWindow(mAppWindowHandle);
+ else
+ {
+ BOOL debuggerPresent;
+
+ if (CheckRemoteDebuggerPresent(mAppProcessHandle, &debuggerPresent) && debuggerPresent)
+ return false;
+ if (SendMessageTimeoutA(mAppWindowHandle, WM_NULL, 0, 0, 0, 5000, nullptr) == 0)
+ return GetLastError() == ERROR_TIMEOUT;
+ }
+ return false;
+ }
+
void CrashMonitor::run()
{
try
@@ -88,9 +148,24 @@ namespace Crash
signalApp();
bool running = true;
- while (isAppAlive() && running)
+ bool frozen = false;
+ while (isAppAlive() && running && !mFreezeAbort)
{
- if (waitApp())
+ if (isAppFrozen())
+ {
+ if (!frozen)
+ {
+ showFreezeMessageBox();
+ frozen = true;
+ }
+ }
+ else if (frozen)
+ {
+ hideFreezeMessageBox();
+ frozen = false;
+ }
+
+ if (!mFreezeAbort && waitApp())
{
shmLock();
@@ -113,6 +188,16 @@ namespace Crash
}
}
+ if (frozen)
+ hideFreezeMessageBox();
+
+ if (mFreezeAbort)
+ {
+ TerminateProcess(mAppProcessHandle, 0xDEAD);
+ std::string message = "OpenMW appears to have frozen.\nCrash log saved to '" + std::string(mShm->mStartup.mLogFilePath) + "'.\nPlease report this to https://gitlab.com/OpenMW/openmw/issues !";
+ SDL_ShowSimpleMessageBox(0, "Fatal Error", message.c_str(), nullptr);
+ }
+
}
catch (...)
{
@@ -185,4 +270,43 @@ namespace Crash
}
}
+ void CrashMonitor::showFreezeMessageBox()
+ {
+ std::thread messageBoxThread([&]() {
+ SDL_MessageBoxButtonData button = { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 0, "Abort" };
+ SDL_MessageBoxData messageBoxData = {
+ SDL_MESSAGEBOX_ERROR,
+ nullptr,
+ "OpenMW appears to have frozen",
+ "OpenMW appears to have frozen. Press Abort to terminate it and generate a crash dump.\nIf OpenMW hasn't actually frozen, this message box will disappear a within a few seconds of it becoming responsive.",
+ 1,
+ &button,
+ nullptr
+ };
+
+ int buttonId;
+ if (SDL_ShowMessageBox(&messageBoxData, &buttonId) == 0 && buttonId == 0)
+ mFreezeAbort = true;
+ });
+
+ mFreezeMessageBoxThreadId = GetThreadId(messageBoxThread.native_handle());
+ messageBoxThread.detach();
+ }
+
+ void CrashMonitor::hideFreezeMessageBox()
+ {
+ if (!mFreezeMessageBoxThreadId)
+ return;
+
+ EnumWindows([](HWND handle, LPARAM param) -> BOOL {
+ CrashMonitor& crashMonitor = *(CrashMonitor*)param;
+ DWORD processId;
+ if (GetWindowThreadProcessId(handle, &processId) == crashMonitor.mFreezeMessageBoxThreadId && processId == GetCurrentProcessId())
+ PostMessage(handle, WM_CLOSE, 0, 0);
+ return true;
+ }, (LPARAM)this);
+
+ mFreezeMessageBoxThreadId = 0;
+ }
+
} // namespace Crash
diff --git a/components/crashcatcher/windows_crashmonitor.hpp b/components/crashcatcher/windows_crashmonitor.hpp
index 678d38435c..eaf908bf66 100644
--- a/components/crashcatcher/windows_crashmonitor.hpp
+++ b/components/crashcatcher/windows_crashmonitor.hpp
@@ -1,7 +1,12 @@
#ifndef WINDOWS_CRASHMONITOR_HPP
#define WINDOWS_CRASHMONITOR_HPP
-#include <windef.h>
+#undef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#include <Windows.h>
+
+#include <atomic>
+#include <unordered_map>
namespace Crash
{
@@ -21,6 +26,8 @@ public:
private:
HANDLE mAppProcessHandle = nullptr;
+ DWORD mAppMainThreadId = 0;
+ HWND mAppWindowHandle = nullptr;
// triggered when the monitor process wants to wake the parent process (received via SHM)
HANDLE mSignalAppEvent = nullptr;
@@ -31,17 +38,28 @@ private:
HANDLE mShmHandle = nullptr;
HANDLE mShmMutex = nullptr;
+ DWORD mFreezeMessageBoxThreadId = 0;
+ std::atomic_bool mFreezeAbort;
+
+ static std::unordered_map<HWINEVENTHOOK, CrashMonitor*> smEventHookOwners;
+
void signalApp() const;
bool waitApp() const;
bool isAppAlive() const;
+ bool isAppFrozen();
+
void shmLock();
void shmUnlock();
void handleCrash();
+
+ void showFreezeMessageBox();
+
+ void hideFreezeMessageBox();
};
} // namespace Crash
diff --git a/components/crashcatcher/windows_crashshm.hpp b/components/crashcatcher/windows_crashshm.hpp
index 47929a45fe..a474600f94 100644
--- a/components/crashcatcher/windows_crashshm.hpp
+++ b/components/crashcatcher/windows_crashshm.hpp
@@ -26,6 +26,7 @@ namespace Crash
struct Startup
{
HANDLE mAppProcessHandle;
+ DWORD mAppMainThreadId;
HANDLE mSignalApp;
HANDLE mSignalMonitor;
HANDLE mShmMutex;
diff --git a/components/debug/debuglog.hpp b/components/debug/debuglog.hpp
index 0da5b9cbdd..96b9d798e9 100644
--- a/components/debug/debuglog.hpp
+++ b/components/debug/debuglog.hpp
@@ -4,8 +4,6 @@
#include <mutex>
#include <iostream>
-#include <osg/io_utils>
-
namespace Debug
{
enum Level
@@ -29,25 +27,29 @@ class Log
std::unique_lock<std::mutex> mLock;
public:
- // Locks a global lock while the object is alive
- Log(Debug::Level level) :
- mLock(sLock),
- mLevel(level)
+ explicit Log(Debug::Level level)
+ : mShouldLog(level <= Debug::CurrentDebugLevel)
{
+ // No need to hold the lock if there will be no logging anyway
+ if (!mShouldLog)
+ return;
+
+ // Locks a global lock while the object is alive
+ mLock = std::unique_lock<std::mutex>(sLock);
+
// If the app has no logging system enabled, log level is not specified.
// Show all messages without marker - we just use the plain cout in this case.
if (Debug::CurrentDebugLevel == Debug::NoLevel)
return;
- if (mLevel <= Debug::CurrentDebugLevel)
- std::cout << static_cast<unsigned char>(mLevel);
+ std::cout << static_cast<unsigned char>(level);
}
// Perfect forwarding wrappers to give the chain of objects to cout
template<typename T>
Log& operator<<(T&& rhs)
{
- if (mLevel <= Debug::CurrentDebugLevel)
+ if (mShouldLog)
std::cout << std::forward<T>(rhs);
return *this;
@@ -55,12 +57,12 @@ public:
~Log()
{
- if (mLevel <= Debug::CurrentDebugLevel)
+ if (mShouldLog)
std::cout << std::endl;
}
private:
- Debug::Level mLevel;
+ const bool mShouldLog;
};
#endif
diff --git a/components/detournavigator/areatype.hpp b/components/detournavigator/areatype.hpp
index 9d99421af0..125b7ed7b9 100644
--- a/components/detournavigator/areatype.hpp
+++ b/components/detournavigator/areatype.hpp
@@ -3,6 +3,8 @@
#include <Recast.h>
+#include <ostream>
+
namespace DetourNavigator
{
enum AreaType : unsigned char
@@ -21,6 +23,19 @@ namespace DetourNavigator
float mPathgrid = 1.0f;
float mGround = 1.0f;
};
+
+ inline std::ostream& operator<<(std::ostream& stream, AreaType value)
+ {
+ switch (value)
+ {
+ case AreaType_null: return stream << "null";
+ case AreaType_water: return stream << "water";
+ case AreaType_door: return stream << "door";
+ case AreaType_pathgrid: return stream << "pathgrid";
+ case AreaType_ground: return stream << "ground";
+ }
+ return stream << "unknown area type (" << static_cast<std::underlying_type_t<AreaType>>(value) << ")";
+ }
}
#endif
diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp
index 966e07bc5a..657befd9d7 100644
--- a/components/detournavigator/asyncnavmeshupdater.cpp
+++ b/components/detournavigator/asyncnavmeshupdater.cpp
@@ -3,6 +3,9 @@
#include "makenavmesh.hpp"
#include "settings.hpp"
#include "version.hpp"
+#include "serialization.hpp"
+#include "navmeshdbutils.hpp"
+#include "dbrefgeometryobject.hpp"
#include <components/debug/debuglog.hpp>
#include <components/misc/thread.hpp>
@@ -15,71 +18,105 @@
#include <algorithm>
#include <numeric>
#include <set>
+#include <type_traits>
-namespace
+namespace DetourNavigator
{
- using DetourNavigator::ChangeType;
- using DetourNavigator::TilePosition;
- using DetourNavigator::UpdateType;
- using DetourNavigator::ChangeType;
- using DetourNavigator::Job;
- using DetourNavigator::JobIt;
-
- int getManhattanDistance(const TilePosition& lhs, const TilePosition& rhs)
+ namespace
{
- return std::abs(lhs.x() - rhs.x()) + std::abs(lhs.y() - rhs.y());
- }
+ int getManhattanDistance(const TilePosition& lhs, const TilePosition& rhs)
+ {
+ return std::abs(lhs.x() - rhs.x()) + std::abs(lhs.y() - rhs.y());
+ }
- int getMinDistanceTo(const TilePosition& position, int maxDistance,
- const std::set<std::tuple<osg::Vec3f, TilePosition>>& pushedTiles,
- const std::set<std::tuple<osg::Vec3f, TilePosition>>& presentTiles)
- {
- int result = maxDistance;
- for (const auto& [halfExtents, tile] : pushedTiles)
- if (presentTiles.find(std::tie(halfExtents, tile)) == presentTiles.end())
- result = std::min(result, getManhattanDistance(position, tile));
- return result;
- }
+ int getMinDistanceTo(const TilePosition& position, int maxDistance,
+ const std::set<std::tuple<osg::Vec3f, TilePosition>>& pushedTiles,
+ const std::set<std::tuple<osg::Vec3f, TilePosition>>& presentTiles)
+ {
+ int result = maxDistance;
+ for (const auto& [halfExtents, tile] : pushedTiles)
+ if (presentTiles.find(std::tie(halfExtents, tile)) == presentTiles.end())
+ result = std::min(result, getManhattanDistance(position, tile));
+ return result;
+ }
- UpdateType getUpdateType(ChangeType changeType) noexcept
- {
- if (changeType == ChangeType::update)
- return UpdateType::Temporary;
- return UpdateType::Persistent;
- }
+ auto getPriority(const Job& job) noexcept
+ {
+ return std::make_tuple(-static_cast<std::underlying_type_t<JobState>>(job.mState), job.mProcessTime,
+ job.mChangeType, job.mTryNumber, job.mDistanceToPlayer, job.mDistanceToOrigin);
+ }
- auto getPriority(const Job& job) noexcept
- {
- return std::make_tuple(job.mProcessTime, job.mChangeType, job.mTryNumber, job.mDistanceToPlayer, job.mDistanceToOrigin);
- }
+ struct LessByJobPriority
+ {
+ bool operator()(JobIt lhs, JobIt rhs) const noexcept
+ {
+ return getPriority(*lhs) < getPriority(*rhs);
+ }
+ };
- struct LessByJobPriority
- {
- bool operator()(JobIt lhs, JobIt rhs) const noexcept
+ void insertPrioritizedJob(JobIt job, std::deque<JobIt>& queue)
{
- return getPriority(*lhs) < getPriority(*rhs);
+ const auto it = std::upper_bound(queue.begin(), queue.end(), job, LessByJobPriority {});
+ queue.insert(it, job);
}
- };
- void insertPrioritizedJob(JobIt job, std::deque<JobIt>& queue)
- {
- const auto it = std::upper_bound(queue.begin(), queue.end(), job, LessByJobPriority {});
- queue.insert(it, job);
- }
+ auto getDbPriority(const Job& job) noexcept
+ {
+ return std::make_tuple(static_cast<std::underlying_type_t<JobState>>(job.mState),
+ job.mChangeType, job.mDistanceToPlayer, job.mDistanceToOrigin);
+ }
- auto getAgentAndTile(const Job& job) noexcept
- {
- return std::make_tuple(job.mAgentHalfExtents, job.mChangedTile);
+ struct LessByJobDbPriority
+ {
+ bool operator()(JobIt lhs, JobIt rhs) const noexcept
+ {
+ return getDbPriority(*lhs) < getDbPriority(*rhs);
+ }
+ };
+
+ void insertPrioritizedDbJob(JobIt job, std::deque<JobIt>& queue)
+ {
+ const auto it = std::upper_bound(queue.begin(), queue.end(), job, LessByJobDbPriority {});
+ queue.insert(it, job);
+ }
+
+ auto getAgentAndTile(const Job& job) noexcept
+ {
+ return std::make_tuple(job.mAgentHalfExtents, job.mChangedTile);
+ }
+
+ std::unique_ptr<DbWorker> makeDbWorker(AsyncNavMeshUpdater& updater, std::unique_ptr<NavMeshDb>&& db, const Settings& settings)
+ {
+ if (db == nullptr)
+ return nullptr;
+ return std::make_unique<DbWorker>(updater, std::move(db), TileVersion(settings.mNavMeshVersion),
+ settings.mRecast, settings.mWriteToNavMeshDb);
+ }
+
+ void updateJobs(std::deque<JobIt>& jobs, TilePosition playerTile, int maxTiles)
+ {
+ for (JobIt job : jobs)
+ {
+ job->mDistanceToPlayer = getManhattanDistance(job->mChangedTile, playerTile);
+ if (!shouldAddTile(job->mChangedTile, playerTile, maxTiles))
+ job->mChangeType = ChangeType::remove;
+ }
+ }
+
+ std::size_t getNextJobId()
+ {
+ static std::atomic_size_t nextJobId {1};
+ return nextJobId.fetch_add(1);
+ }
}
-}
-namespace DetourNavigator
-{
Job::Job(const osg::Vec3f& agentHalfExtents, std::weak_ptr<GuardedNavMeshCacheItem> navMeshCacheItem,
- const TilePosition& changedTile, ChangeType changeType, int distanceToPlayer,
+ std::string_view worldspace, const TilePosition& changedTile, ChangeType changeType, int distanceToPlayer,
std::chrono::steady_clock::time_point processTime)
- : mAgentHalfExtents(agentHalfExtents)
+ : mId(getNextJobId())
+ , mAgentHalfExtents(agentHalfExtents)
, mNavMeshCacheItem(std::move(navMeshCacheItem))
+ , mWorldspace(worldspace)
, mChangedTile(changedTile)
, mProcessTime(processTime)
, mChangeType(changeType)
@@ -89,12 +126,13 @@ namespace DetourNavigator
}
AsyncNavMeshUpdater::AsyncNavMeshUpdater(const Settings& settings, TileCachedRecastMeshManager& recastMeshManager,
- OffMeshConnectionsManager& offMeshConnectionsManager)
+ OffMeshConnectionsManager& offMeshConnectionsManager, std::unique_ptr<NavMeshDb>&& db)
: mSettings(settings)
, mRecastMeshManager(recastMeshManager)
, mOffMeshConnectionsManager(offMeshConnectionsManager)
, mShouldStop()
, mNavMeshTilesCache(settings.mMaxNavMeshTilesCacheSize)
+ , mDbWorker(makeDbWorker(*this, std::move(db), mSettings))
{
for (std::size_t i = 0; i < mSettings.get().mAsyncNavMeshUpdaterThreads; ++i)
mThreads.emplace_back([&] { process(); });
@@ -103,6 +141,8 @@ namespace DetourNavigator
AsyncNavMeshUpdater::~AsyncNavMeshUpdater()
{
mShouldStop = true;
+ if (mDbWorker != nullptr)
+ mDbWorker->stop();
std::unique_lock<std::mutex> lock(mMutex);
mWaiting.clear();
mHasJob.notify_all();
@@ -111,8 +151,8 @@ namespace DetourNavigator
thread.join();
}
- void AsyncNavMeshUpdater::post(const osg::Vec3f& agentHalfExtents,
- const SharedNavMeshCacheItem& navMeshCacheItem, const TilePosition& playerTile,
+ void AsyncNavMeshUpdater::post(const osg::Vec3f& agentHalfExtents, const SharedNavMeshCacheItem& navMeshCacheItem,
+ const TilePosition& playerTile, std::string_view worldspace,
const std::map<TilePosition, ChangeType>& changedTiles)
{
bool playerTileChanged = false;
@@ -126,18 +166,12 @@ namespace DetourNavigator
return;
const dtNavMeshParams params = *navMeshCacheItem->lockConst()->getImpl().getParams();
+ const int maxTiles = std::min(mSettings.get().mMaxTilesNumber, params.maxTiles);
- const std::lock_guard<std::mutex> lock(mMutex);
+ std::unique_lock lock(mMutex);
if (playerTileChanged)
- {
- for (JobIt job : mWaiting)
- {
- job->mDistanceToPlayer = getManhattanDistance(job->mChangedTile, playerTile);
- if (!shouldAddTile(job->mChangedTile, playerTile, std::min(mSettings.get().mMaxTilesNumber, params.maxTiles)))
- job->mChangeType = ChangeType::remove;
- }
- }
+ updateJobs(mWaiting, playerTile, maxTiles);
for (const auto& [changedTile, changeType] : changedTiles)
{
@@ -147,8 +181,11 @@ namespace DetourNavigator
? mLastUpdates[std::tie(agentHalfExtents, changedTile)] + mSettings.get().mMinUpdateInterval
: std::chrono::steady_clock::time_point();
- const JobIt it = mJobs.emplace(mJobs.end(), agentHalfExtents, navMeshCacheItem, changedTile,
- changeType, getManhattanDistance(changedTile, playerTile), processTime);
+ const JobIt it = mJobs.emplace(mJobs.end(), agentHalfExtents, navMeshCacheItem, worldspace,
+ changedTile, changeType, getManhattanDistance(changedTile, playerTile), processTime);
+
+ Log(Debug::Debug) << "Post job " << it->mId << " for agent=(" << it->mAgentHalfExtents << ")"
+ << " changedTile=(" << it->mChangedTile << ")";
if (playerTileChanged)
mWaiting.push_back(it);
@@ -164,6 +201,11 @@ namespace DetourNavigator
if (!mWaiting.empty())
mHasJob.notify_all();
+
+ lock.unlock();
+
+ if (playerTileChanged && mDbWorker != nullptr)
+ mDbWorker->updateJobs(playerTile, maxTiles);
}
void AsyncNavMeshUpdater::wait(Loading::Listener& listener, WaitConditionType waitConditionType)
@@ -241,25 +283,40 @@ namespace DetourNavigator
mProcessingTiles.wait(mProcessed, [] (const auto& v) { return v.empty(); });
}
- void AsyncNavMeshUpdater::reportStats(unsigned int frameNumber, osg::Stats& stats) const
+ AsyncNavMeshUpdater::Stats AsyncNavMeshUpdater::getStats() const
{
- std::size_t jobs = 0;
- std::size_t waiting = 0;
- std::size_t pushed = 0;
-
+ Stats result;
{
const std::lock_guard<std::mutex> lock(mMutex);
- jobs = mJobs.size();
- waiting = mWaiting.size();
- pushed = mPushed.size();
+ result.mJobs = mJobs.size();
+ result.mWaiting = mWaiting.size();
+ result.mPushed = mPushed.size();
}
+ result.mProcessing = mProcessingTiles.lockConst()->size();
+ if (mDbWorker != nullptr)
+ result.mDb = mDbWorker->getStats();
+ result.mCache = mNavMeshTilesCache.getStats();
+ result.mDbGetTileHits = mDbGetTileHits.load(std::memory_order_relaxed);
+ return result;
+ }
+
+ void reportStats(const AsyncNavMeshUpdater::Stats& stats, unsigned int frameNumber, osg::Stats& out)
+ {
+ out.setAttribute(frameNumber, "NavMesh Jobs", static_cast<double>(stats.mJobs));
+ out.setAttribute(frameNumber, "NavMesh Waiting", static_cast<double>(stats.mWaiting));
+ out.setAttribute(frameNumber, "NavMesh Pushed", static_cast<double>(stats.mPushed));
+ out.setAttribute(frameNumber, "NavMesh Processing", static_cast<double>(stats.mProcessing));
- stats.setAttribute(frameNumber, "NavMesh Jobs", jobs);
- stats.setAttribute(frameNumber, "NavMesh Waiting", waiting);
- stats.setAttribute(frameNumber, "NavMesh Pushed", pushed);
- stats.setAttribute(frameNumber, "NavMesh Processing", mProcessingTiles.lockConst()->size());
+ if (stats.mDb.has_value())
+ {
+ out.setAttribute(frameNumber, "NavMesh DbJobs", static_cast<double>(stats.mDb->mJobs));
- mNavMeshTilesCache.reportStats(frameNumber, stats);
+ if (stats.mDb->mGetTileCount > 0)
+ out.setAttribute(frameNumber, "NavMesh DbCacheHitRate", static_cast<double>(stats.mDbGetTileHits)
+ / static_cast<double>(stats.mDb->mGetTileCount) * 100.0);
+ }
+
+ reportStats(stats.mCache, frameNumber, out);
}
void AsyncNavMeshUpdater::process() noexcept
@@ -272,12 +329,26 @@ namespace DetourNavigator
{
if (JobIt job = getNextJob(); job != mJobs.end())
{
- const auto processed = processJob(*job);
- unlockTile(job->mAgentHalfExtents, job->mChangedTile);
- if (processed)
- removeJob(job);
- else
- repost(job);
+ const JobStatus status = processJob(*job);
+ Log(Debug::Debug) << "Processed job " << job->mId << " with status=" << status;
+ switch (status)
+ {
+ case JobStatus::Done:
+ unlockTile(job->mAgentHalfExtents, job->mChangedTile);
+ if (job->mGeneratedNavMeshData != nullptr)
+ mDbWorker->enqueueJob(job);
+ else
+ removeJob(job);
+ break;
+ case JobStatus::Fail:
+ repost(job);
+ break;
+ case JobStatus::MemoryCacheMiss:
+ {
+ mDbWorker->enqueueJob(job);
+ break;
+ }
+ }
}
else
cleanupLastUpdates();
@@ -290,38 +361,156 @@ namespace DetourNavigator
Log(Debug::Debug) << "Stop navigator jobs processing by thread=" << std::this_thread::get_id();
}
- bool AsyncNavMeshUpdater::processJob(const Job& job)
+ JobStatus AsyncNavMeshUpdater::processJob(Job& job)
{
- Log(Debug::Debug) << "Process job for agent=(" << std::fixed << std::setprecision(2) << job.mAgentHalfExtents << ")"
- " by thread=" << std::this_thread::get_id();
-
- const auto start = std::chrono::steady_clock::now();
+ Log(Debug::Debug) << "Processing job " << job.mId << " by thread=" << std::this_thread::get_id();
const auto navMeshCacheItem = job.mNavMeshCacheItem.lock();
if (!navMeshCacheItem)
- return true;
+ return JobStatus::Done;
- const auto recastMesh = mRecastMeshManager.get().getMesh(job.mChangedTile);
const auto playerTile = *mPlayerTile.lockConst();
- const auto offMeshConnections = mOffMeshConnectionsManager.get().get(job.mChangedTile);
+ const auto params = *navMeshCacheItem->lockConst()->getImpl().getParams();
+
+ if (!shouldAddTile(job.mChangedTile, playerTile, std::min(mSettings.get().mMaxTilesNumber, params.maxTiles)))
+ {
+ Log(Debug::Debug) << "Ignore add tile by job " << job.mId << ": too far from player";
+ navMeshCacheItem->lock()->removeTile(job.mChangedTile);
+ return JobStatus::Done;
+ }
- const auto status = updateNavMesh(job.mAgentHalfExtents, recastMesh.get(), job.mChangedTile, playerTile,
- offMeshConnections, mSettings, navMeshCacheItem, mNavMeshTilesCache, getUpdateType(job.mChangeType));
+ switch (job.mState)
+ {
+ case JobState::Initial:
+ return processInitialJob(job, *navMeshCacheItem);
+ case JobState::WithDbResult:
+ return processJobWithDbResult(job, *navMeshCacheItem);
+ }
+
+ return JobStatus::Done;
+ }
- if (recastMesh != nullptr)
+ JobStatus AsyncNavMeshUpdater::processInitialJob(Job& job, GuardedNavMeshCacheItem& navMeshCacheItem)
+ {
+ Log(Debug::Debug) << "Processing initial job " << job.mId;
+
+ std::shared_ptr<RecastMesh> recastMesh = mRecastMeshManager.get().getMesh(job.mWorldspace, job.mChangedTile);
+
+ if (recastMesh == nullptr)
+ {
+ Log(Debug::Debug) << "Null recast mesh for job " << job.mId;
+ navMeshCacheItem.lock()->markAsEmpty(job.mChangedTile);
+ return JobStatus::Done;
+ }
+
+ if (isEmpty(*recastMesh))
+ {
+ Log(Debug::Debug) << "Empty bounds for job " << job.mId;
+ navMeshCacheItem.lock()->markAsEmpty(job.mChangedTile);
+ return JobStatus::Done;
+ }
+
+ NavMeshTilesCache::Value cachedNavMeshData = mNavMeshTilesCache.get(job.mAgentHalfExtents, job.mChangedTile, *recastMesh);
+ std::unique_ptr<PreparedNavMeshData> preparedNavMeshData;
+ const PreparedNavMeshData* preparedNavMeshDataPtr = nullptr;
+
+ if (cachedNavMeshData)
+ {
+ preparedNavMeshDataPtr = &cachedNavMeshData.get();
+ }
+ else
{
- Version navMeshVersion;
+ if (job.mChangeType != ChangeType::update && mDbWorker != nullptr)
+ {
+ job.mRecastMesh = std::move(recastMesh);
+ return JobStatus::MemoryCacheMiss;
+ }
+
+ preparedNavMeshData = prepareNavMeshTileData(*recastMesh, job.mChangedTile, job.mAgentHalfExtents, mSettings.get().mRecast);
+
+ if (preparedNavMeshData == nullptr)
+ {
+ Log(Debug::Debug) << "Null navmesh data for job " << job.mId;
+ navMeshCacheItem.lock()->markAsEmpty(job.mChangedTile);
+ return JobStatus::Done;
+ }
+
+ if (job.mChangeType == ChangeType::update)
{
- const auto locked = navMeshCacheItem->lockConst();
- navMeshVersion.mGeneration = locked->getGeneration();
- navMeshVersion.mRevision = locked->getNavMeshRevision();
+ preparedNavMeshDataPtr = preparedNavMeshData.get();
}
- mRecastMeshManager.get().reportNavMeshChange(job.mChangedTile,
- Version {recastMesh->getGeneration(), recastMesh->getRevision()},
- navMeshVersion);
+ else
+ {
+ cachedNavMeshData = mNavMeshTilesCache.set(job.mAgentHalfExtents, job.mChangedTile,
+ *recastMesh, std::move(preparedNavMeshData));
+ preparedNavMeshDataPtr = cachedNavMeshData ? &cachedNavMeshData.get() : preparedNavMeshData.get();
+ }
+ }
+
+ const auto offMeshConnections = mOffMeshConnectionsManager.get().get(job.mChangedTile);
+
+ const UpdateNavMeshStatus status = navMeshCacheItem.lock()->updateTile(job.mChangedTile, std::move(cachedNavMeshData),
+ makeNavMeshTileData(*preparedNavMeshDataPtr, offMeshConnections, job.mAgentHalfExtents, job.mChangedTile, mSettings.get().mRecast));
+
+ return handleUpdateNavMeshStatus(status, job, navMeshCacheItem, *recastMesh);
+ }
+
+ JobStatus AsyncNavMeshUpdater::processJobWithDbResult(Job& job, GuardedNavMeshCacheItem& navMeshCacheItem)
+ {
+ Log(Debug::Debug) << "Processing job with db result " << job.mId;
+
+ std::unique_ptr<PreparedNavMeshData> preparedNavMeshData;
+ bool generatedNavMeshData = false;
+
+ if (job.mCachedTileData.has_value() && job.mCachedTileData->mVersion == mSettings.get().mNavMeshVersion)
+ {
+ preparedNavMeshData = std::make_unique<PreparedNavMeshData>();
+ if (deserialize(job.mCachedTileData->mData, *preparedNavMeshData))
+ ++mDbGetTileHits;
+ else
+ preparedNavMeshData = nullptr;
}
+ if (preparedNavMeshData == nullptr)
+ {
+ preparedNavMeshData = prepareNavMeshTileData(*job.mRecastMesh, job.mChangedTile, job.mAgentHalfExtents, mSettings.get().mRecast);
+ generatedNavMeshData = true;
+ }
+
+ if (preparedNavMeshData == nullptr)
+ {
+ Log(Debug::Debug) << "Null navmesh data for job " << job.mId;
+ navMeshCacheItem.lock()->markAsEmpty(job.mChangedTile);
+ return JobStatus::Done;
+ }
+
+ auto cachedNavMeshData = mNavMeshTilesCache.set(job.mAgentHalfExtents, job.mChangedTile, *job.mRecastMesh,
+ std::move(preparedNavMeshData));
+
+ const auto offMeshConnections = mOffMeshConnectionsManager.get().get(job.mChangedTile);
+
+ const PreparedNavMeshData* preparedNavMeshDataPtr = cachedNavMeshData ? &cachedNavMeshData.get() : preparedNavMeshData.get();
+ const UpdateNavMeshStatus status = navMeshCacheItem.lock()->updateTile(job.mChangedTile, std::move(cachedNavMeshData),
+ makeNavMeshTileData(*preparedNavMeshDataPtr, offMeshConnections, job.mAgentHalfExtents, job.mChangedTile, mSettings.get().mRecast));
+
+ const JobStatus result = handleUpdateNavMeshStatus(status, job, navMeshCacheItem, *job.mRecastMesh);
+
+ if (result == JobStatus::Done && job.mChangeType != ChangeType::update
+ && mDbWorker != nullptr && mSettings.get().mWriteToNavMeshDb && generatedNavMeshData)
+ job.mGeneratedNavMeshData = std::make_unique<PreparedNavMeshData>(*preparedNavMeshDataPtr);
+
+ return result;
+ }
+
+ JobStatus AsyncNavMeshUpdater::handleUpdateNavMeshStatus(UpdateNavMeshStatus status,
+ const Job& job, const GuardedNavMeshCacheItem& navMeshCacheItem, const RecastMesh& recastMesh)
+ {
+ const Version navMeshVersion = navMeshCacheItem.lockConst()->getVersion();
+ mRecastMeshManager.get().reportNavMeshChange(job.mChangedTile,
+ Version {recastMesh.getGeneration(), recastMesh.getRevision()},
+ navMeshVersion);
+
if (status == UpdateNavMeshStatus::removed || status == UpdateNavMeshStatus::lost)
{
const std::scoped_lock lock(mMutex);
@@ -333,23 +522,9 @@ namespace DetourNavigator
mPresentTiles.insert(std::make_tuple(job.mAgentHalfExtents, job.mChangedTile));
}
- const auto finish = std::chrono::steady_clock::now();
-
- writeDebugFiles(job, recastMesh.get());
-
- using FloatMs = std::chrono::duration<float, std::milli>;
+ writeDebugFiles(job, &recastMesh);
- const auto locked = navMeshCacheItem->lockConst();
- Log(Debug::Debug) << std::fixed << std::setprecision(2) <<
- "Cache updated for agent=(" << job.mAgentHalfExtents << ")" <<
- " tile=" << job.mChangedTile <<
- " status=" << status <<
- " generation=" << locked->getGeneration() <<
- " revision=" << locked->getNavMeshRevision() <<
- " time=" << std::chrono::duration_cast<FloatMs>(finish - start).count() << "ms" <<
- " thread=" << std::this_thread::get_id();
-
- return isSuccess(status);
+ return isSuccess(status) ? JobStatus::Done : JobStatus::Fail;
}
JobIt AsyncNavMeshUpdater::getNextJob()
@@ -378,8 +553,12 @@ namespace DetourNavigator
mWaiting.pop_front();
+ if (job->mRecastMesh != nullptr)
+ return job;
+
if (!lockTile(job->mAgentHalfExtents, job->mChangedTile))
{
+ Log(Debug::Debug) << "Failed to lock tile by " << job->mId;
++job->mTryNumber;
insertPrioritizedJob(job, mWaiting);
return mJobs.end();
@@ -409,7 +588,7 @@ namespace DetourNavigator
}
if (recastMesh && mSettings.get().mEnableWriteRecastMeshToFile)
writeToFile(*recastMesh, mSettings.get().mRecastMeshPathPrefix + std::to_string(job.mChangedTile.x())
- + "_" + std::to_string(job.mChangedTile.y()) + "_", recastMeshRevision, mSettings);
+ + "_" + std::to_string(job.mChangedTile.y()) + "_", recastMeshRevision, mSettings.get().mRecast);
if (mSettings.get().mEnableWriteNavMeshToFile)
if (const auto shared = job.mNavMeshCacheItem.lock())
writeToFile(shared->lockConst()->getImpl(), mSettings.get().mNavMeshPathPrefix, navMeshRevision);
@@ -417,6 +596,8 @@ namespace DetourNavigator
void AsyncNavMeshUpdater::repost(JobIt job)
{
+ unlockTile(job->mAgentHalfExtents, job->mChangedTile);
+
if (mShouldStop || job->mTryNumber > 2)
return;
@@ -435,17 +616,15 @@ namespace DetourNavigator
bool AsyncNavMeshUpdater::lockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile)
{
- if (mSettings.get().mAsyncNavMeshUpdaterThreads <= 1)
- return true;
+ Log(Debug::Debug) << "Locking tile agent=(" << agentHalfExtents << ") changedTile=(" << changedTile << ")";
return mProcessingTiles.lock()->emplace(agentHalfExtents, changedTile).second;
}
void AsyncNavMeshUpdater::unlockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile)
{
- if (mSettings.get().mAsyncNavMeshUpdaterThreads <= 1)
- return;
auto locked = mProcessingTiles.lock();
locked->erase(std::tie(agentHalfExtents, changedTile));
+ Log(Debug::Debug) << "Unlocked tile agent=(" << agentHalfExtents << ") changedTile=(" << changedTile << ")";
if (locked->empty())
mProcessed.notify_all();
}
@@ -471,9 +650,213 @@ namespace DetourNavigator
}
}
+ void AsyncNavMeshUpdater::enqueueJob(JobIt job)
+ {
+ Log(Debug::Debug) << "Enqueueing job " << job->mId << " by thread=" << std::this_thread::get_id();
+ const std::lock_guard lock(mMutex);
+ insertPrioritizedJob(job, mWaiting);
+ mHasJob.notify_all();
+ }
+
void AsyncNavMeshUpdater::removeJob(JobIt job)
{
+ Log(Debug::Debug) << "Removing job " << job->mId << " by thread=" << std::this_thread::get_id();
const std::lock_guard lock(mMutex);
mJobs.erase(job);
}
+
+ void DbJobQueue::push(JobIt job)
+ {
+ const std::lock_guard lock(mMutex);
+ insertPrioritizedDbJob(job, mJobs);
+ mHasJob.notify_all();
+ }
+
+ std::optional<JobIt> DbJobQueue::pop()
+ {
+ std::unique_lock lock(mMutex);
+ mHasJob.wait(lock, [&] { return mShouldStop || !mJobs.empty(); });
+ if (mJobs.empty())
+ return std::nullopt;
+ const JobIt job = mJobs.front();
+ mJobs.pop_front();
+ return job;
+ }
+
+ void DbJobQueue::update(TilePosition playerTile, int maxTiles)
+ {
+ const std::lock_guard lock(mMutex);
+ updateJobs(mJobs, playerTile, maxTiles);
+ std::sort(mJobs.begin(), mJobs.end(), LessByJobDbPriority {});
+ }
+
+ void DbJobQueue::stop()
+ {
+ const std::lock_guard lock(mMutex);
+ mJobs.clear();
+ mShouldStop = true;
+ mHasJob.notify_all();
+ }
+
+ std::size_t DbJobQueue::size() const
+ {
+ const std::lock_guard lock(mMutex);
+ return mJobs.size();
+ }
+
+ DbWorker::DbWorker(AsyncNavMeshUpdater& updater, std::unique_ptr<NavMeshDb>&& db,
+ TileVersion version, const RecastSettings& recastSettings, bool writeToDb)
+ : mUpdater(updater)
+ , mRecastSettings(recastSettings)
+ , mDb(std::move(db))
+ , mVersion(version)
+ , mWriteToDb(writeToDb)
+ , mNextTileId(mDb->getMaxTileId() + 1)
+ , mNextShapeId(mDb->getMaxShapeId() + 1)
+ , mThread([this] { run(); })
+ {
+ }
+
+ DbWorker::~DbWorker()
+ {
+ stop();
+ mThread.join();
+ }
+
+ void DbWorker::enqueueJob(JobIt job)
+ {
+ Log(Debug::Debug) << "Enqueueing db job " << job->mId << " by thread=" << std::this_thread::get_id();
+ mQueue.push(job);
+ }
+
+ DbWorker::Stats DbWorker::getStats() const
+ {
+ Stats result;
+ result.mJobs = mQueue.size();
+ result.mGetTileCount = mGetTileCount.load(std::memory_order::memory_order_relaxed);
+ return result;
+ }
+
+ void DbWorker::stop()
+ {
+ mShouldStop = true;
+ mQueue.stop();
+ }
+
+ void DbWorker::run() noexcept
+ {
+ constexpr std::size_t writesPerTransaction = 100;
+ auto transaction = mDb->startTransaction();
+ while (!mShouldStop)
+ {
+ try
+ {
+ if (const auto job = mQueue.pop())
+ processJob(*job);
+ if (mWrites > writesPerTransaction)
+ {
+ mWrites = 0;
+ transaction.commit();
+ transaction = mDb->startTransaction();
+ }
+ }
+ catch (const std::exception& e)
+ {
+ Log(Debug::Error) << "DbWorker exception: " << e.what();
+ }
+ }
+ transaction.commit();
+ }
+
+ void DbWorker::processJob(JobIt job)
+ {
+ const auto process = [&] (auto f)
+ {
+ try
+ {
+ f(job);
+ }
+ catch (const std::exception& e)
+ {
+ Log(Debug::Error) << "DbWorker exception while processing job " << job->mId << ": " << e.what();
+ }
+ };
+
+ if (job->mGeneratedNavMeshData != nullptr)
+ {
+ process([&] (JobIt job) { processWritingJob(job); });
+ mUpdater.removeJob(job);
+ return;
+ }
+
+ process([&] (JobIt job) { processReadingJob(job); });
+ job->mState = JobState::WithDbResult;
+ mUpdater.enqueueJob(job);
+ }
+
+ void DbWorker::processReadingJob(JobIt job)
+ {
+ Log(Debug::Debug) << "Processing db read job " << job->mId;
+
+ if (job->mInput.empty())
+ {
+ Log(Debug::Debug) << "Serializing input for job " << job->mId;
+ if (mWriteToDb)
+ {
+ const ShapeId shapeId = mNextShapeId;
+ const auto objects = makeDbRefGeometryObjects(job->mRecastMesh->getMeshSources(),
+ [&] (const MeshSource& v) { return resolveMeshSource(*mDb, v, mNextShapeId); });
+ if (shapeId != mNextShapeId)
+ ++mWrites;
+ job->mInput = serialize(mRecastSettings, *job->mRecastMesh, objects);
+ }
+ else
+ {
+ const auto objects = makeDbRefGeometryObjects(job->mRecastMesh->getMeshSources(),
+ [&] (const MeshSource& v) { return resolveMeshSource(*mDb, v); });
+ if (!objects.has_value())
+ return;
+ job->mInput = serialize(mRecastSettings, *job->mRecastMesh, *objects);
+ }
+ }
+
+ job->mCachedTileData = mDb->getTileData(job->mWorldspace, job->mChangedTile, job->mInput);
+ ++mGetTileCount;
+ }
+
+ void DbWorker::processWritingJob(JobIt job)
+ {
+ ++mWrites;
+
+ Log(Debug::Debug) << "Processing db write job " << job->mId;
+
+ if (job->mInput.empty())
+ {
+ Log(Debug::Debug) << "Serializing input for job " << job->mId;
+ const std::vector<DbRefGeometryObject> objects = makeDbRefGeometryObjects(job->mRecastMesh->getMeshSources(),
+ [&] (const MeshSource& v) { return resolveMeshSource(*mDb, v, mNextShapeId); });
+ job->mInput = serialize(mRecastSettings, *job->mRecastMesh, objects);
+ }
+
+ if (const auto& cachedTileData = job->mCachedTileData)
+ {
+ Log(Debug::Debug) << "Update db tile by job " << job->mId;
+ job->mGeneratedNavMeshData->mUserId = cachedTileData->mTileId;
+ mDb->updateTile(cachedTileData->mTileId, mVersion, serialize(*job->mGeneratedNavMeshData));
+ return;
+ }
+
+ const auto cached = mDb->findTile(job->mWorldspace, job->mChangedTile, job->mInput);
+ if (cached.has_value() && cached->mVersion == mVersion)
+ {
+ Log(Debug::Debug) << "Ignore existing db tile by job " << job->mId;
+ return;
+ }
+
+ job->mGeneratedNavMeshData->mUserId = mNextTileId;
+ Log(Debug::Debug) << "Insert db tile by job " << job->mId;
+ mDb->insertTile(mNextTileId, job->mWorldspace, job->mChangedTile,
+ mVersion, job->mInput, serialize(*job->mGeneratedNavMeshData));
+ ++mNextTileId.t;
+ }
}
diff --git a/components/detournavigator/asyncnavmeshupdater.hpp b/components/detournavigator/asyncnavmeshupdater.hpp
index 2d915ad434..31778f8c26 100644
--- a/components/detournavigator/asyncnavmeshupdater.hpp
+++ b/components/detournavigator/asyncnavmeshupdater.hpp
@@ -7,6 +7,7 @@
#include "tileposition.hpp"
#include "navmeshtilescache.hpp"
#include "waitconditiontype.hpp"
+#include "navmeshdb.hpp"
#include <osg/Vec3f>
@@ -20,6 +21,7 @@
#include <thread>
#include <tuple>
#include <list>
+#include <optional>
class dtNavMesh;
@@ -53,37 +55,151 @@ namespace DetourNavigator
return stream << "ChangeType::" << static_cast<int>(value);
}
+ enum class JobState
+ {
+ Initial,
+ WithDbResult,
+ };
+
struct Job
{
+ const std::size_t mId;
const osg::Vec3f mAgentHalfExtents;
const std::weak_ptr<GuardedNavMeshCacheItem> mNavMeshCacheItem;
+ const std::string mWorldspace;
const TilePosition mChangedTile;
const std::chrono::steady_clock::time_point mProcessTime;
unsigned mTryNumber = 0;
ChangeType mChangeType;
int mDistanceToPlayer;
const int mDistanceToOrigin;
+ JobState mState = JobState::Initial;
+ std::vector<std::byte> mInput;
+ std::shared_ptr<RecastMesh> mRecastMesh;
+ std::optional<TileData> mCachedTileData;
+ std::unique_ptr<PreparedNavMeshData> mGeneratedNavMeshData;
Job(const osg::Vec3f& agentHalfExtents, std::weak_ptr<GuardedNavMeshCacheItem> navMeshCacheItem,
- const TilePosition& changedTile, ChangeType changeType, int distanceToPlayer,
+ std::string_view worldspace, const TilePosition& changedTile, ChangeType changeType, int distanceToPlayer,
std::chrono::steady_clock::time_point processTime);
};
using JobIt = std::list<Job>::iterator;
+ enum class JobStatus
+ {
+ Done,
+ Fail,
+ MemoryCacheMiss,
+ };
+
+ inline std::ostream& operator<<(std::ostream& stream, JobStatus value)
+ {
+ switch (value)
+ {
+ case JobStatus::Done: return stream << "JobStatus::Done";
+ case JobStatus::Fail: return stream << "JobStatus::Fail";
+ case JobStatus::MemoryCacheMiss: return stream << "JobStatus::MemoryCacheMiss";
+ }
+ return stream << "JobStatus::" << static_cast<std::underlying_type_t<JobState>>(value);
+ }
+
+ class DbJobQueue
+ {
+ public:
+ void push(JobIt job);
+
+ std::optional<JobIt> pop();
+
+ void update(TilePosition playerTile, int maxTiles);
+
+ void stop();
+
+ std::size_t size() const;
+
+ private:
+ mutable std::mutex mMutex;
+ std::condition_variable mHasJob;
+ std::deque<JobIt> mJobs;
+ bool mShouldStop = false;
+ };
+
+ class AsyncNavMeshUpdater;
+
+ class DbWorker
+ {
+ public:
+ struct Stats
+ {
+ std::size_t mJobs = 0;
+ std::size_t mGetTileCount = 0;
+ };
+
+ DbWorker(AsyncNavMeshUpdater& updater, std::unique_ptr<NavMeshDb>&& db,
+ TileVersion version, const RecastSettings& recastSettings, bool writeToDb);
+
+ ~DbWorker();
+
+ Stats getStats() const;
+
+ void enqueueJob(JobIt job);
+
+ void updateJobs(TilePosition playerTile, int maxTiles) { mQueue.update(playerTile, maxTiles); }
+
+ void stop();
+
+ private:
+ AsyncNavMeshUpdater& mUpdater;
+ const RecastSettings& mRecastSettings;
+ const std::unique_ptr<NavMeshDb> mDb;
+ const TileVersion mVersion;
+ const bool mWriteToDb;
+ TileId mNextTileId;
+ ShapeId mNextShapeId;
+ DbJobQueue mQueue;
+ std::atomic_bool mShouldStop {false};
+ std::atomic_size_t mGetTileCount {0};
+ std::size_t mWrites = 0;
+ std::thread mThread;
+
+ inline void run() noexcept;
+
+ inline void processJob(JobIt job);
+
+ inline void processReadingJob(JobIt job);
+
+ inline void processWritingJob(JobIt job);
+ };
+
class AsyncNavMeshUpdater
{
public:
+ struct Stats
+ {
+ std::size_t mJobs = 0;
+ std::size_t mWaiting = 0;
+ std::size_t mPushed = 0;
+ std::size_t mProcessing = 0;
+ std::size_t mDbGetTileHits = 0;
+ std::optional<DbWorker::Stats> mDb;
+ NavMeshTilesCache::Stats mCache;
+ };
+
AsyncNavMeshUpdater(const Settings& settings, TileCachedRecastMeshManager& recastMeshManager,
- OffMeshConnectionsManager& offMeshConnectionsManager);
+ OffMeshConnectionsManager& offMeshConnectionsManager, std::unique_ptr<NavMeshDb>&& db);
~AsyncNavMeshUpdater();
- void post(const osg::Vec3f& agentHalfExtents, const SharedNavMeshCacheItem& mNavMeshCacheItem,
- const TilePosition& playerTile, const std::map<TilePosition, ChangeType>& changedTiles);
+ void post(const osg::Vec3f& agentHalfExtents, const SharedNavMeshCacheItem& navMeshCacheItem,
+ const TilePosition& playerTile, std::string_view worldspace,
+ const std::map<TilePosition, ChangeType>& changedTiles);
void wait(Loading::Listener& listener, WaitConditionType waitConditionType);
- void reportStats(unsigned int frameNumber, osg::Stats& stats) const;
+ Stats getStats() const;
+
+ void enqueueJob(JobIt job);
+
+ void removeJob(JobIt job);
private:
std::reference_wrapper<const Settings> mSettings;
@@ -103,14 +219,21 @@ namespace DetourNavigator
std::map<std::tuple<osg::Vec3f, TilePosition>, std::chrono::steady_clock::time_point> mLastUpdates;
std::set<std::tuple<osg::Vec3f, TilePosition>> mPresentTiles;
std::vector<std::thread> mThreads;
+ std::unique_ptr<DbWorker> mDbWorker;
+ std::atomic_size_t mDbGetTileHits {0};
void process() noexcept;
- bool processJob(const Job& job);
+ JobStatus processJob(Job& job);
- JobIt getNextJob();
+ inline JobStatus processInitialJob(Job& job, GuardedNavMeshCacheItem& navMeshCacheItem);
+
+ inline JobStatus processJobWithDbResult(Job& job, GuardedNavMeshCacheItem& navMeshCacheItem);
+
+ inline JobStatus handleUpdateNavMeshStatus(UpdateNavMeshStatus status, const Job& job,
+ const GuardedNavMeshCacheItem& navMeshCacheItem, const RecastMesh& recastMesh);
- JobIt getJob(std::deque<JobIt>& jobs, bool changeLastUpdate);
+ JobIt getNextJob();
void postThreadJob(JobIt job, std::deque<JobIt>& queue);
@@ -129,9 +252,9 @@ namespace DetourNavigator
int waitUntilJobsDoneForNotPresentTiles(const std::size_t initialJobsLeft, std::size_t& maxJobsLeft, Loading::Listener& listener);
void waitUntilAllJobsDone();
-
- inline void removeJob(JobIt job);
};
+
+ void reportStats(const AsyncNavMeshUpdater::Stats& stats, unsigned int frameNumber, osg::Stats& out);
}
#endif
diff --git a/components/detournavigator/cachedrecastmeshmanager.cpp b/components/detournavigator/cachedrecastmeshmanager.cpp
index d82310dc38..e350c5591f 100644
--- a/components/detournavigator/cachedrecastmeshmanager.cpp
+++ b/components/detournavigator/cachedrecastmeshmanager.cpp
@@ -3,9 +3,8 @@
namespace DetourNavigator
{
- CachedRecastMeshManager::CachedRecastMeshManager(const Settings& settings, const TileBounds& bounds,
- std::size_t generation)
- : mImpl(settings, bounds, generation)
+ CachedRecastMeshManager::CachedRecastMeshManager(const TileBounds& bounds, std::size_t generation)
+ : mImpl(bounds, generation)
{}
bool CachedRecastMeshManager::addObject(const ObjectId id, const CollisionShape& shape,
@@ -33,16 +32,15 @@ namespace DetourNavigator
return object;
}
- bool CachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize,
- const osg::Vec3f& shift)
+ bool CachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, int cellSize, float level)
{
- if (!mImpl.addWater(cellPosition, cellSize, shift))
+ if (!mImpl.addWater(cellPosition, cellSize, level))
return false;
mOutdatedCache = true;
return true;
}
- std::optional<Cell> CachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition)
+ std::optional<Water> CachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition)
{
const auto water = mImpl.removeWater(cellPosition);
if (water)
@@ -51,20 +49,20 @@ namespace DetourNavigator
}
bool CachedRecastMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize,
- const osg::Vec3f& shift, const HeightfieldShape& shape)
+ const HeightfieldShape& shape)
{
- if (!mImpl.addHeightfield(cellPosition, cellSize, shift, shape))
+ if (!mImpl.addHeightfield(cellPosition, cellSize, shape))
return false;
mOutdatedCache = true;
return true;
}
- std::optional<Cell> CachedRecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition)
+ std::optional<SizedHeightfieldShape> CachedRecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition)
{
- const auto cell = mImpl.removeHeightfield(cellPosition);
- if (cell)
+ const auto heightfield = mImpl.removeHeightfield(cellPosition);
+ if (heightfield)
mOutdatedCache = true;
- return cell;
+ return heightfield;
}
std::shared_ptr<RecastMesh> CachedRecastMeshManager::getMesh()
@@ -86,6 +84,11 @@ namespace DetourNavigator
return *mCached.lockConst();
}
+ std::shared_ptr<RecastMesh> CachedRecastMeshManager::getNewMesh() const
+ {
+ return mImpl.getMesh();
+ }
+
bool CachedRecastMeshManager::isEmpty() const
{
return mImpl.isEmpty();
diff --git a/components/detournavigator/cachedrecastmeshmanager.hpp b/components/detournavigator/cachedrecastmeshmanager.hpp
index b1e2570b0a..b92d39efa4 100644
--- a/components/detournavigator/cachedrecastmeshmanager.hpp
+++ b/components/detournavigator/cachedrecastmeshmanager.hpp
@@ -14,7 +14,7 @@ namespace DetourNavigator
class CachedRecastMeshManager
{
public:
- CachedRecastMeshManager(const Settings& settings, const TileBounds& bounds, std::size_t generation);
+ explicit CachedRecastMeshManager(const TileBounds& bounds, std::size_t generation);
bool addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform,
const AreaType areaType);
@@ -23,19 +23,20 @@ namespace DetourNavigator
std::optional<RemovedRecastMeshObject> removeObject(const ObjectId id);
- bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift);
+ bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level);
- std::optional<Cell> removeWater(const osg::Vec2i& cellPosition);
+ std::optional<Water> removeWater(const osg::Vec2i& cellPosition);
- bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift,
- const HeightfieldShape& shape);
+ bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape);
- std::optional<Cell> removeHeightfield(const osg::Vec2i& cellPosition);
+ std::optional<SizedHeightfieldShape> removeHeightfield(const osg::Vec2i& cellPosition);
std::shared_ptr<RecastMesh> getMesh();
std::shared_ptr<RecastMesh> getCachedMesh() const;
+ std::shared_ptr<RecastMesh> getNewMesh() const;
+
bool isEmpty() const;
void reportNavMeshChange(const Version& recastMeshVersion, const Version& navMeshVersion);
diff --git a/components/detournavigator/dbrefgeometryobject.hpp b/components/detournavigator/dbrefgeometryobject.hpp
new file mode 100644
index 0000000000..acf2a58b19
--- /dev/null
+++ b/components/detournavigator/dbrefgeometryobject.hpp
@@ -0,0 +1,61 @@
+#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_DBREFGEOMETRYOBJECT_H
+#define OPENMW_COMPONENTS_DETOURNAVIGATOR_DBREFGEOMETRYOBJECT_H
+
+#include "objecttransform.hpp"
+#include "recastmesh.hpp"
+
+#include <components/misc/typetraits.hpp>
+
+#include <algorithm>
+#include <cstdint>
+#include <tuple>
+#include <vector>
+#include <optional>
+#include <type_traits>
+
+namespace DetourNavigator
+{
+ struct DbRefGeometryObject
+ {
+ std::int64_t mShapeId;
+ ObjectTransform mObjectTransform;
+
+ friend inline auto tie(const DbRefGeometryObject& v)
+ {
+ return std::tie(v.mShapeId, v.mObjectTransform);
+ }
+
+ friend inline bool operator<(const DbRefGeometryObject& l, const DbRefGeometryObject& r)
+ {
+ return tie(l) < tie(r);
+ }
+ };
+
+ template <class ResolveMeshSource>
+ inline auto makeDbRefGeometryObjects(const std::vector<MeshSource>& meshSources, ResolveMeshSource&& resolveMeshSource)
+ -> std::conditional_t<
+ Misc::isOptional<std::decay_t<decltype(resolveMeshSource(meshSources.front()))>>,
+ std::optional<std::vector<DbRefGeometryObject>>,
+ std::vector<DbRefGeometryObject>
+ >
+ {
+ std::vector<DbRefGeometryObject> result;
+ result.reserve(meshSources.size());
+ for (const MeshSource& meshSource : meshSources)
+ {
+ const auto shapeId = resolveMeshSource(meshSource);
+ if constexpr (Misc::isOptional<std::decay_t<decltype(shapeId)>>)
+ {
+ if (!shapeId.has_value())
+ return std::nullopt;
+ result.push_back(DbRefGeometryObject {*shapeId, meshSource.mObjectTransform});
+ }
+ else
+ result.push_back(DbRefGeometryObject {shapeId, meshSource.mObjectTransform});
+ }
+ std::sort(result.begin(), result.end());
+ return result;
+ }
+}
+
+#endif
diff --git a/components/detournavigator/debug.cpp b/components/detournavigator/debug.cpp
index 07832d748f..8394c6696d 100644
--- a/components/detournavigator/debug.cpp
+++ b/components/detournavigator/debug.cpp
@@ -11,7 +11,8 @@
namespace DetourNavigator
{
- void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix, const std::string& revision, const Settings& settings)
+ void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix,
+ const std::string& revision, const RecastSettings& settings)
{
const auto path = pathPrefix + "recastmesh" + revision + ".obj";
boost::filesystem::ofstream file(boost::filesystem::path(path), std::ios::out);
diff --git a/components/detournavigator/debug.hpp b/components/detournavigator/debug.hpp
index d86d923a41..ce1e2d2023 100644
--- a/components/detournavigator/debug.hpp
+++ b/components/detournavigator/debug.hpp
@@ -3,20 +3,13 @@
#include "tilebounds.hpp"
#include "status.hpp"
+#include "recastmesh.hpp"
#include <osg/io_utils>
#include <components/bullethelpers/operators.hpp>
-#include <components/misc/guarded.hpp>
-
-#include <chrono>
-#include <fstream>
-#include <iomanip>
-#include <iostream>
-#include <memory>
-#include <sstream>
+
#include <string>
-#include <thread>
class dtNavMesh;
@@ -47,10 +40,40 @@ namespace DetourNavigator
return stream << "DetourNavigator::Error::" << static_cast<int>(value);
}
+ inline std::ostream& operator<<(std::ostream& s, const Water& v)
+ {
+ return s << "Water {" << v.mCellSize << ", " << v.mLevel << "}";
+ }
+
+ inline std::ostream& operator<<(std::ostream& s, const CellWater& v)
+ {
+ return s << "CellWater {" << v.mCellPosition << ", " << v.mWater << "}";
+ }
+
+ inline std::ostream& operator<<(std::ostream& s, const FlatHeightfield& v)
+ {
+ return s << "FlatHeightfield {" << v.mCellPosition << ", " << v.mCellSize << ", " << v.mHeight << "}";
+ }
+
+ inline std::ostream& operator<<(std::ostream& s, const Heightfield& v)
+ {
+ s << "Heightfield {.mCellPosition=" << v.mCellPosition
+ << ", .mCellSize=" << v.mCellSize
+ << ", .mLength=" << static_cast<int>(v.mLength)
+ << ", .mMinHeight=" << v.mMinHeight
+ << ", .mMaxHeight=" << v.mMaxHeight
+ << ", .mHeights={";
+ for (float h : v.mHeights)
+ s << h << ", ";
+ s << "}";
+ return s << ", .mOriginalSize=" << v.mOriginalSize << "}";
+ }
+
class RecastMesh;
- struct Settings;
+ struct RecastSettings;
- void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix, const std::string& revision, const Settings& settings);
+ void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix,
+ const std::string& revision, const RecastSettings& settings);
void writeToFile(const dtNavMesh& navMesh, const std::string& pathPrefix, const std::string& revision);
}
diff --git a/components/detournavigator/findrandompointaroundcircle.cpp b/components/detournavigator/findrandompointaroundcircle.cpp
index a3407a61c3..1e1e2401c5 100644
--- a/components/detournavigator/findrandompointaroundcircle.cpp
+++ b/components/detournavigator/findrandompointaroundcircle.cpp
@@ -10,7 +10,7 @@
namespace DetourNavigator
{
std::optional<osg::Vec3f> findRandomPointAroundCircle(const dtNavMesh& navMesh, const osg::Vec3f& halfExtents,
- const osg::Vec3f& start, const float maxRadius, const Flags includeFlags, const Settings& settings)
+ const osg::Vec3f& start, const float maxRadius, const Flags includeFlags, const DetourSettings& settings)
{
dtNavMeshQuery navMeshQuery;
if (!initNavMeshQuery(navMeshQuery, navMesh, settings.mMaxNavMeshQueryNodes))
diff --git a/components/detournavigator/findrandompointaroundcircle.hpp b/components/detournavigator/findrandompointaroundcircle.hpp
index d0dc2bbbc0..89a3c0964c 100644
--- a/components/detournavigator/findrandompointaroundcircle.hpp
+++ b/components/detournavigator/findrandompointaroundcircle.hpp
@@ -10,10 +10,10 @@ class dtNavMesh;
namespace DetourNavigator
{
- struct Settings;
+ struct DetourSettings;
std::optional<osg::Vec3f> findRandomPointAroundCircle(const dtNavMesh& navMesh, const osg::Vec3f& halfExtents,
- const osg::Vec3f& start, const float maxRadius, const Flags includeFlags, const Settings& settings);
+ const osg::Vec3f& start, const float maxRadius, const Flags includeFlags, const DetourSettings& settings);
}
#endif
diff --git a/components/detournavigator/findsmoothpath.hpp b/components/detournavigator/findsmoothpath.hpp
index 07d3054e19..fa35470c42 100644
--- a/components/detournavigator/findsmoothpath.hpp
+++ b/components/detournavigator/findsmoothpath.hpp
@@ -18,6 +18,7 @@
#include <cassert>
#include <vector>
+#include <functional>
class dtNavMesh;
@@ -59,7 +60,7 @@ namespace DetourNavigator
class OutputTransformIterator
{
public:
- OutputTransformIterator(OutputIterator& impl, const Settings& settings)
+ explicit OutputTransformIterator(OutputIterator& impl, const RecastSettings& settings)
: mImpl(impl), mSettings(settings)
{
}
@@ -90,7 +91,7 @@ namespace DetourNavigator
private:
std::reference_wrapper<OutputIterator> mImpl;
- std::reference_wrapper<const Settings> mSettings;
+ std::reference_wrapper<const RecastSettings> mSettings;
};
inline bool initNavMeshQuery(dtNavMeshQuery& value, const dtNavMesh& navMesh, const int maxNodes)
@@ -260,7 +261,7 @@ namespace DetourNavigator
const Settings& settings, float endTolerance, OutputIterator& out)
{
dtNavMeshQuery navMeshQuery;
- if (!initNavMeshQuery(navMeshQuery, navMesh, settings.mMaxNavMeshQueryNodes))
+ if (!initNavMeshQuery(navMeshQuery, navMesh, settings.mDetour.mMaxNavMeshQueryNodes))
return Status::InitNavMeshQueryFailed;
dtQueryFilter queryFilter;
@@ -282,7 +283,7 @@ namespace DetourNavigator
if (endRef == 0)
return Status::EndPolygonNotFound;
- std::vector<dtPolyRef> polygonPath(settings.mMaxPolygonPathSize);
+ std::vector<dtPolyRef> polygonPath(settings.mDetour.mMaxPolygonPathSize);
const auto polygonPathSize = findPath(navMeshQuery, startRef, endRef, start, end, queryFilter,
polygonPath.data(), polygonPath.size());
@@ -293,9 +294,9 @@ namespace DetourNavigator
return Status::Success;
const bool partialPath = polygonPath[*polygonPathSize - 1] != endRef;
- auto outTransform = OutputTransformIterator<OutputIterator>(out, settings);
+ auto outTransform = OutputTransformIterator<OutputIterator>(out, settings.mRecast);
const Status smoothStatus = makeSmoothPath(navMesh, navMeshQuery, queryFilter, start, end, stepSize,
- polygonPath, *polygonPathSize, settings.mMaxSmoothPathSize, outTransform);
+ polygonPath, *polygonPathSize, settings.mDetour.mMaxSmoothPathSize, outTransform);
if (smoothStatus != Status::Success)
return smoothStatus;
diff --git a/components/detournavigator/generatenavmeshtile.cpp b/components/detournavigator/generatenavmeshtile.cpp
new file mode 100644
index 0000000000..ad8978cd4b
--- /dev/null
+++ b/components/detournavigator/generatenavmeshtile.cpp
@@ -0,0 +1,95 @@
+#include "generatenavmeshtile.hpp"
+
+#include "dbrefgeometryobject.hpp"
+#include "makenavmesh.hpp"
+#include "offmeshconnectionsmanager.hpp"
+#include "preparednavmeshdata.hpp"
+#include "serialization.hpp"
+#include "settings.hpp"
+#include "tilecachedrecastmeshmanager.hpp"
+
+#include <components/debug/debuglog.hpp>
+
+#include <osg/Vec3f>
+#include <osg/io_utils>
+
+#include <memory>
+#include <stdexcept>
+#include <vector>
+#include <optional>
+#include <functional>
+
+namespace DetourNavigator
+{
+ namespace
+ {
+ struct Ignore
+ {
+ std::shared_ptr<NavMeshTileConsumer> mConsumer;
+
+ ~Ignore() noexcept
+ {
+ if (mConsumer != nullptr)
+ mConsumer->ignore();
+ }
+ };
+ }
+
+ GenerateNavMeshTile::GenerateNavMeshTile(std::string worldspace, const TilePosition& tilePosition,
+ RecastMeshProvider recastMeshProvider, const osg::Vec3f& agentHalfExtents,
+ const DetourNavigator::Settings& settings, std::weak_ptr<NavMeshTileConsumer> consumer)
+ : mWorldspace(std::move(worldspace))
+ , mTilePosition(tilePosition)
+ , mRecastMeshProvider(recastMeshProvider)
+ , mAgentHalfExtents(agentHalfExtents)
+ , mSettings(settings)
+ , mConsumer(std::move(consumer)) {}
+
+ void GenerateNavMeshTile::doWork()
+ {
+ impl();
+ }
+
+ void GenerateNavMeshTile::impl() noexcept
+ {
+ const auto consumer = mConsumer.lock();
+
+ if (consumer == nullptr)
+ return;
+
+ try
+ {
+ Ignore ignore {consumer};
+
+ const std::shared_ptr<RecastMesh> recastMesh = mRecastMeshProvider.getMesh(mWorldspace, mTilePosition);
+
+ if (recastMesh == nullptr || isEmpty(*recastMesh))
+ return;
+
+ const std::vector<DbRefGeometryObject> objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(),
+ [&] (const MeshSource& v) { return consumer->resolveMeshSource(v); });
+ std::vector<std::byte> input = serialize(mSettings.mRecast, *recastMesh, objects);
+ const std::optional<NavMeshTileInfo> info = consumer->find(mWorldspace, mTilePosition, input);
+
+ if (info.has_value() && info->mVersion == mSettings.mNavMeshVersion)
+ return;
+
+ const auto data = prepareNavMeshTileData(*recastMesh, mTilePosition, mAgentHalfExtents, mSettings.mRecast);
+
+ if (data == nullptr)
+ return;
+
+ if (info.has_value())
+ consumer->update(info->mTileId, mSettings.mNavMeshVersion, *data);
+ else
+ consumer->insert(mWorldspace, mTilePosition, mSettings.mNavMeshVersion, input, *data);
+
+ ignore.mConsumer = nullptr;
+ }
+ catch (const std::exception& e)
+ {
+ Log(Debug::Warning) << "Failed to generate navmesh for worldspace \"" << mWorldspace
+ << "\" tile " << mTilePosition << ": " << e.what();
+ }
+ }
+}
diff --git a/components/detournavigator/generatenavmeshtile.hpp b/components/detournavigator/generatenavmeshtile.hpp
new file mode 100644
index 0000000000..511b8dfb8f
--- /dev/null
+++ b/components/detournavigator/generatenavmeshtile.hpp
@@ -0,0 +1,71 @@
+#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_GENERATENAVMESHTILE_H
+#define OPENMW_COMPONENTS_DETOURNAVIGATOR_GENERATENAVMESHTILE_H
+
+#include "recastmeshprovider.hpp"
+#include "tileposition.hpp"
+
+#include <components/sceneutil/workqueue.hpp>
+
+#include <osg/Vec3f>
+
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <optional>
+#include <string_view>
+#include <vector>
+
+namespace DetourNavigator
+{
+ class OffMeshConnectionsManager;
+ class RecastMesh;
+ struct NavMeshTileConsumer;
+ struct OffMeshConnection;
+ struct PreparedNavMeshData;
+ struct Settings;
+
+ struct NavMeshTileInfo
+ {
+ std::int64_t mTileId;
+ std::int64_t mVersion;
+ };
+
+ struct NavMeshTileConsumer
+ {
+ virtual ~NavMeshTileConsumer() = default;
+
+ virtual std::int64_t resolveMeshSource(const MeshSource& source) = 0;
+
+ virtual std::optional<NavMeshTileInfo> find(const std::string& worldspace, const TilePosition& tilePosition,
+ const std::vector<std::byte>& input) = 0;
+
+ virtual void ignore() = 0;
+
+ virtual void insert(const std::string& worldspace, const TilePosition& tilePosition,
+ std::int64_t version, const std::vector<std::byte>& input, PreparedNavMeshData& data) = 0;
+
+ virtual void update(std::int64_t tileId, std::int64_t version, PreparedNavMeshData& data) = 0;
+ };
+
+ class GenerateNavMeshTile final : public SceneUtil::WorkItem
+ {
+ public:
+ GenerateNavMeshTile(std::string worldspace, const TilePosition& tilePosition,
+ RecastMeshProvider recastMeshProvider, const osg::Vec3f& agentHalfExtents, const Settings& settings,
+ std::weak_ptr<NavMeshTileConsumer> consumer);
+
+ void doWork() final;
+
+ private:
+ const std::string mWorldspace;
+ const TilePosition mTilePosition;
+ const RecastMeshProvider mRecastMeshProvider;
+ const osg::Vec3f mAgentHalfExtents;
+ const Settings& mSettings;
+ std::weak_ptr<NavMeshTileConsumer> mConsumer;
+
+ inline void impl() noexcept;
+ };
+}
+
+#endif
diff --git a/components/detournavigator/gettilespositions.hpp b/components/detournavigator/gettilespositions.hpp
index 27c8f7a4ac..707db0b512 100644
--- a/components/detournavigator/gettilespositions.hpp
+++ b/components/detournavigator/gettilespositions.hpp
@@ -15,7 +15,7 @@ namespace DetourNavigator
{
template <class Callback>
void getTilesPositions(const osg::Vec3f& aabbMin, const osg::Vec3f& aabbMax,
- const Settings& settings, Callback&& callback)
+ const RecastSettings& settings, Callback&& callback)
{
auto min = toNavMeshCoordinates(settings, aabbMin);
auto max = toNavMeshCoordinates(settings, aabbMax);
@@ -40,21 +40,23 @@ namespace DetourNavigator
template <class Callback>
void getTilesPositions(const btCollisionShape& shape, const btTransform& transform,
- const Settings& settings, Callback&& callback)
+ const RecastSettings& settings, Callback&& callback)
{
btVector3 aabbMin;
btVector3 aabbMax;
shape.getAabb(transform, aabbMin, aabbMax);
- getTilesPositions(Misc::Convert::makeOsgVec3f(aabbMin), Misc::Convert::makeOsgVec3f(aabbMax), settings, std::forward<Callback>(callback));
+ getTilesPositions(Misc::Convert::toOsg(aabbMin), Misc::Convert::toOsg(aabbMax), settings, std::forward<Callback>(callback));
}
template <class Callback>
- void getTilesPositions(const int cellSize, const osg::Vec3f& shift,
- const Settings& settings, Callback&& callback)
+ void getTilesPositions(const int cellSize, const btVector3& shift,
+ const RecastSettings& settings, Callback&& callback)
{
+ using Misc::Convert::toOsg;
+
const auto halfCellSize = cellSize / 2;
- const btTransform transform(btMatrix3x3::getIdentity(), Misc::Convert::toBullet(shift));
+ const btTransform transform(btMatrix3x3::getIdentity(), shift);
auto aabbMin = transform(btVector3(-halfCellSize, -halfCellSize, 0));
auto aabbMax = transform(btVector3(halfCellSize, halfCellSize, 0));
@@ -64,7 +66,7 @@ namespace DetourNavigator
aabbMax.setX(std::max(aabbMin.x(), aabbMax.x()));
aabbMax.setY(std::max(aabbMin.y(), aabbMax.y()));
- getTilesPositions(Misc::Convert::makeOsgVec3f(aabbMin), Misc::Convert::makeOsgVec3f(aabbMax), settings, std::forward<Callback>(callback));
+ getTilesPositions(toOsg(aabbMin), toOsg(aabbMax), settings, std::forward<Callback>(callback));
}
}
diff --git a/components/detournavigator/heightfieldshape.hpp b/components/detournavigator/heightfieldshape.hpp
index 48a273725b..06770e9b3d 100644
--- a/components/detournavigator/heightfieldshape.hpp
+++ b/components/detournavigator/heightfieldshape.hpp
@@ -1,6 +1,10 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_HEIGHFIELDSHAPE_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_HEIGHFIELDSHAPE_H
+#include <components/bullethelpers/heightfield.hpp>
+
+#include <osg/Vec2i>
+
#include <cstddef>
#include <variant>
@@ -20,6 +24,21 @@ namespace DetourNavigator
};
using HeightfieldShape = std::variant<HeightfieldPlane, HeightfieldSurface>;
+
+ inline btVector3 getHeightfieldShift(const HeightfieldPlane& v, const osg::Vec2i& cellPosition, int cellSize)
+ {
+ return BulletHelpers::getHeightfieldShift(cellPosition.x(), cellPosition.y(), cellSize, v.mHeight, v.mHeight);
+ }
+
+ inline btVector3 getHeightfieldShift(const HeightfieldSurface& v, const osg::Vec2i& cellPosition, int cellSize)
+ {
+ return BulletHelpers::getHeightfieldShift(cellPosition.x(), cellPosition.y(), cellSize, v.mMinHeight, v.mMaxHeight);
+ }
+
+ inline btVector3 getHeightfieldShift(const HeightfieldShape& v, const osg::Vec2i& cellPosition, int cellSize)
+ {
+ return std::visit([&] (const auto& w) { return getHeightfieldShift(w, cellPosition, cellSize); }, v);
+ }
}
#endif
diff --git a/components/detournavigator/makenavmesh.cpp b/components/detournavigator/makenavmesh.cpp
index 3f133f5033..070a227454 100644
--- a/components/detournavigator/makenavmesh.cpp
+++ b/components/detournavigator/makenavmesh.cpp
@@ -10,9 +10,15 @@
#include "preparednavmeshdata.hpp"
#include "navmeshdata.hpp"
#include "recastmeshbuilder.hpp"
+#include "navmeshdb.hpp"
+#include "serialization.hpp"
+#include "dbrefgeometryobject.hpp"
+#include "navmeshdbutils.hpp"
#include <components/misc/convert.hpp>
#include <components/bullethelpers/processtrianglecallback.hpp>
+#include <components/misc/convert.hpp>
+#include <components/misc/guarded.hpp>
#include <DetourNavMesh.h>
#include <DetourNavMeshBuilder.h>
@@ -26,43 +32,16 @@
#include <limits>
#include <array>
+namespace DetourNavigator
+{
namespace
{
- using namespace DetourNavigator;
-
struct Rectangle
{
TileBounds mBounds;
float mHeight;
};
- Rectangle getSwimRectangle(const Cell& water, const Settings& settings,
- const osg::Vec3f& agentHalfExtents)
- {
- if (water.mSize == std::numeric_limits<int>::max())
- {
- return Rectangle {
- TileBounds {
- osg::Vec2f(-std::numeric_limits<float>::max(), -std::numeric_limits<float>::max()),
- osg::Vec2f(std::numeric_limits<float>::max(), std::numeric_limits<float>::max())
- },
- toNavMeshCoordinates(settings, getSwimLevel(settings, water.mShift.z(), agentHalfExtents.z()))
- };
- }
- else
- {
- const osg::Vec2f shift(water.mShift.x(), water.mShift.y());
- const float halfCellSize = water.mSize / 2.0f;
- return Rectangle {
- TileBounds{
- toNavMeshCoordinates(settings, shift + osg::Vec2f(-halfCellSize, -halfCellSize)),
- toNavMeshCoordinates(settings, shift + osg::Vec2f(halfCellSize, halfCellSize))
- },
- toNavMeshCoordinates(settings, getSwimLevel(settings, water.mShift.z(), agentHalfExtents.z()))
- };
- }
- }
-
std::vector<float> getOffMeshVerts(const std::vector<OffMeshConnection>& connections)
{
std::vector<float> result;
@@ -121,49 +100,70 @@ namespace
return result;
}
- rcConfig makeConfig(const osg::Vec3f& agentHalfExtents, const osg::Vec3f& boundsMin, const osg::Vec3f& boundsMax,
- const Settings& settings)
- {
- rcConfig config;
-
- config.cs = settings.mCellSize;
- config.ch = settings.mCellHeight;
- config.walkableSlopeAngle = settings.mMaxSlope;
- config.walkableHeight = static_cast<int>(std::ceil(getHeight(settings, agentHalfExtents) / config.ch));
- config.walkableClimb = static_cast<int>(std::floor(getMaxClimb(settings) / config.ch));
- config.walkableRadius = static_cast<int>(std::ceil(getRadius(settings, agentHalfExtents) / config.cs));
- config.maxEdgeLen = static_cast<int>(std::round(settings.mMaxEdgeLen / config.cs));
- config.maxSimplificationError = settings.mMaxSimplificationError;
- config.minRegionArea = settings.mRegionMinSize * settings.mRegionMinSize;
- config.mergeRegionArea = settings.mRegionMergeSize * settings.mRegionMergeSize;
- config.maxVertsPerPoly = settings.mMaxVertsPerPoly;
- config.detailSampleDist = settings.mDetailSampleDist < 0.9f ? 0 : config.cs * settings.mDetailSampleDist;
- config.detailSampleMaxError = config.ch * settings.mDetailSampleMaxError;
- config.borderSize = settings.mBorderSize;
- config.width = settings.mTileSize + config.borderSize * 2;
- config.height = settings.mTileSize + config.borderSize * 2;
- rcVcopy(config.bmin, boundsMin.ptr());
- rcVcopy(config.bmax, boundsMax.ptr());
- config.bmin[0] -= getBorderSize(settings);
- config.bmin[2] -= getBorderSize(settings);
- config.bmax[0] += getBorderSize(settings);
- config.bmax[2] += getBorderSize(settings);
- config.tileSize = settings.mTileSize;
-
- return config;
+ float getHeight(const RecastSettings& settings,const osg::Vec3f& agentHalfExtents)
+ {
+ return 2.0f * agentHalfExtents.z() * settings.mRecastScaleFactor;
+ }
+
+ float getMaxClimb(const RecastSettings& settings)
+ {
+ return settings.mMaxClimb * settings.mRecastScaleFactor;
+ }
+
+ float getRadius(const RecastSettings& settings, const osg::Vec3f& agentHalfExtents)
+ {
+ return std::max(agentHalfExtents.x(), agentHalfExtents.y()) * std::sqrt(2) * settings.mRecastScaleFactor;
+ }
+
+ float getSwimLevel(const RecastSettings& settings, const float waterLevel, const float agentHalfExtentsZ)
+ {
+ return waterLevel - settings.mSwimHeightScale * agentHalfExtentsZ - agentHalfExtentsZ;;
}
- void createHeightfield(rcContext& context, rcHeightfield& solid, int width, int height, const float* bmin,
- const float* bmax, const float cs, const float ch)
+ struct RecastParams
+ {
+ float mSampleDist = 0;
+ float mSampleMaxError = 0;
+ int mMaxEdgeLen = 0;
+ int mWalkableClimb = 0;
+ int mWalkableHeight = 0;
+ int mWalkableRadius = 0;
+ };
+
+ RecastParams makeRecastParams(const RecastSettings& settings, const osg::Vec3f& agentHalfExtents)
{
- const auto result = rcCreateHeightfield(&context, solid, width, height, bmin, bmax, cs, ch);
+ RecastParams result;
+
+ result.mWalkableHeight = static_cast<int>(std::ceil(getHeight(settings, agentHalfExtents) / settings.mCellHeight));
+ result.mWalkableClimb = static_cast<int>(std::floor(getMaxClimb(settings) / settings.mCellHeight));
+ result.mWalkableRadius = static_cast<int>(std::ceil(getRadius(settings, agentHalfExtents) / settings.mCellSize));
+ result.mMaxEdgeLen = static_cast<int>(std::round(static_cast<float>(settings.mMaxEdgeLen) / settings.mCellSize));
+ result.mSampleDist = settings.mDetailSampleDist < 0.9f ? 0 : settings.mCellSize * settings.mDetailSampleDist;
+ result.mSampleMaxError = settings.mCellHeight * settings.mDetailSampleMaxError;
+
+ return result;
+ }
+
+ void initHeightfield(rcContext& context, const TilePosition& tilePosition, float minZ, float maxZ,
+ const RecastSettings& settings, rcHeightfield& solid)
+ {
+ const int size = settings.mTileSize + settings.mBorderSize * 2;
+ const int width = size;
+ const int height = size;
+ const float halfBoundsSize = size * settings.mCellSize * 0.5f;
+ const osg::Vec2f shift = osg::Vec2f(tilePosition.x() + 0.5f, tilePosition.y() + 0.5f) * getTileSize(settings);
+ const osg::Vec3f bmin(shift.x() - halfBoundsSize, minZ, shift.y() - halfBoundsSize);
+ const osg::Vec3f bmax(shift.x() + halfBoundsSize, maxZ, shift.y() + halfBoundsSize);
+
+ const auto result = rcCreateHeightfield(&context, solid, width, height, bmin.ptr(), bmax.ptr(),
+ settings.mCellSize, settings.mCellHeight);
if (!result)
throw NavigatorException("Failed to create heightfield for navmesh");
}
- bool rasterizeTriangles(rcContext& context, const Mesh& mesh, const Settings& settings, const rcConfig& config,
- rcHeightfield& solid)
+ bool rasterizeTriangles(rcContext& context, const Mesh& mesh, const RecastSettings& settings,
+ const RecastParams& params, rcHeightfield& solid)
{
std::vector<unsigned char> areas(mesh.getAreaTypes().begin(), mesh.getAreaTypes().end());
std::vector<float> vertices = mesh.getVertices();
@@ -177,7 +177,7 @@ namespace
rcClearUnwalkableTriangles(
&context,
- config.walkableSlopeAngle,
+ settings.mMaxSlope,
vertices.data(),
static_cast<int>(mesh.getVerticesCount()),
mesh.getIndices().data(),
@@ -193,30 +193,18 @@ namespace
areas.data(),
static_cast<int>(areas.size()),
solid,
- config.walkableClimb
+ params.mWalkableClimb
);
}
- bool rasterizeTriangles(rcContext& context, const Rectangle& rectangle, const rcConfig& config,
- const unsigned char* areas, std::size_t areasSize, rcHeightfield& solid)
+ bool rasterizeTriangles(rcContext& context, const Rectangle& rectangle, AreaType areaType,
+ const RecastParams& params, rcHeightfield& solid)
{
- const osg::Vec2f tileBoundsMin(
- std::clamp(rectangle.mBounds.mMin.x(), config.bmin[0], config.bmax[0]),
- std::clamp(rectangle.mBounds.mMin.y(), config.bmin[2], config.bmax[2])
- );
- const osg::Vec2f tileBoundsMax(
- std::clamp(rectangle.mBounds.mMax.x(), config.bmin[0], config.bmax[0]),
- std::clamp(rectangle.mBounds.mMax.y(), config.bmin[2], config.bmax[2])
- );
-
- if (tileBoundsMax == tileBoundsMin)
- return true;
-
const std::array vertices {
- tileBoundsMin.x(), rectangle.mHeight, tileBoundsMin.y(),
- tileBoundsMin.x(), rectangle.mHeight, tileBoundsMax.y(),
- tileBoundsMax.x(), rectangle.mHeight, tileBoundsMax.y(),
- tileBoundsMax.x(), rectangle.mHeight, tileBoundsMin.y(),
+ rectangle.mBounds.mMin.x(), rectangle.mHeight, rectangle.mBounds.mMin.y(),
+ rectangle.mBounds.mMin.x(), rectangle.mHeight, rectangle.mBounds.mMax.y(),
+ rectangle.mBounds.mMax.x(), rectangle.mHeight, rectangle.mBounds.mMax.y(),
+ rectangle.mBounds.mMax.x(), rectangle.mHeight, rectangle.mBounds.mMin.y(),
};
const std::array indices {
@@ -224,65 +212,78 @@ namespace
0, 2, 3,
};
+ const std::array<unsigned char, 2> areas {areaType, areaType};
+
return rcRasterizeTriangles(
&context,
vertices.data(),
static_cast<int>(vertices.size() / 3),
indices.data(),
- areas,
- static_cast<int>(areasSize),
+ areas.data(),
+ static_cast<int>(areas.size()),
solid,
- config.walkableClimb
+ params.mWalkableClimb
);
}
- bool rasterizeTriangles(rcContext& context, const osg::Vec3f& agentHalfExtents, const std::vector<Cell>& cells,
- const Settings& settings, const rcConfig& config, rcHeightfield& solid)
+ bool rasterizeTriangles(rcContext& context, const osg::Vec3f& agentHalfExtents, const std::vector<CellWater>& water,
+ const RecastSettings& settings, const RecastParams& params, const TileBounds& realTileBounds, rcHeightfield& solid)
{
- const std::array<unsigned char, 2> areas {{AreaType_water, AreaType_water}};
- for (const Cell& cell : cells)
+ for (const CellWater& cellWater : water)
{
- const Rectangle rectangle = getSwimRectangle(cell, settings, agentHalfExtents);
- if (!rasterizeTriangles(context, rectangle, config, areas.data(), areas.size(), solid))
- return false;
+ const TileBounds cellTileBounds = maxCellTileBounds(cellWater.mCellPosition, cellWater.mWater.mCellSize);
+ if (auto intersection = getIntersection(realTileBounds, cellTileBounds))
+ {
+ const Rectangle rectangle {
+ toNavMeshCoordinates(settings, *intersection),
+ toNavMeshCoordinates(settings, getSwimLevel(settings, cellWater.mWater.mLevel, agentHalfExtents.z()))
+ };
+ if (!rasterizeTriangles(context, rectangle, AreaType_water, params, solid))
+ return false;
+ }
}
return true;
}
- bool rasterizeTriangles(rcContext& context, const std::vector<FlatHeightfield>& heightfields,
- const Settings& settings, const rcConfig& config, rcHeightfield& solid)
+ bool rasterizeTriangles(rcContext& context, const TileBounds& realTileBounds, const std::vector<FlatHeightfield>& heightfields,
+ const RecastSettings& settings, const RecastParams& params, rcHeightfield& solid)
{
for (const FlatHeightfield& heightfield : heightfields)
{
- const std::array<unsigned char, 2> areas {{AreaType_ground, AreaType_ground}};
- const Rectangle rectangle {heightfield.mBounds, toNavMeshCoordinates(settings, heightfield.mHeight)};
- if (!rasterizeTriangles(context, rectangle, config, areas.data(), areas.size(), solid))
- return false;
+ const TileBounds cellTileBounds = maxCellTileBounds(heightfield.mCellPosition, heightfield.mCellSize);
+ if (auto intersection = getIntersection(realTileBounds, cellTileBounds))
+ {
+ const Rectangle rectangle {
+ toNavMeshCoordinates(settings, *intersection),
+ toNavMeshCoordinates(settings, heightfield.mHeight)
+ };
+ if (!rasterizeTriangles(context, rectangle, AreaType_ground, params, solid))
+ return false;
+ }
}
return true;
}
bool rasterizeTriangles(rcContext& context, const std::vector<Heightfield>& heightfields,
- const Settings& settings, const rcConfig& config, rcHeightfield& solid)
+ const RecastSettings& settings, const RecastParams& params, rcHeightfield& solid)
{
- using BulletHelpers::makeProcessTriangleCallback;
-
for (const Heightfield& heightfield : heightfields)
{
const Mesh mesh = makeMesh(heightfield);
- if (!rasterizeTriangles(context, mesh, settings, config, solid))
+ if (!rasterizeTriangles(context, mesh, settings, params, solid))
return false;
}
return true;
}
- bool rasterizeTriangles(rcContext& context, const osg::Vec3f& agentHalfExtents, const RecastMesh& recastMesh,
- const rcConfig& config, const Settings& settings, rcHeightfield& solid)
+ bool rasterizeTriangles(rcContext& context, const TilePosition& tilePosition, const osg::Vec3f& agentHalfExtents,
+ const RecastMesh& recastMesh, const RecastSettings& settings, const RecastParams& params, rcHeightfield& solid)
{
- return rasterizeTriangles(context, recastMesh.getMesh(), settings, config, solid)
- && rasterizeTriangles(context, agentHalfExtents, recastMesh.getWater(), settings, config, solid)
- && rasterizeTriangles(context, recastMesh.getHeightfields(), settings, config, solid)
- && rasterizeTriangles(context, recastMesh.getFlatHeightfields(), settings, config, solid);
+ const TileBounds realTileBounds = makeRealTileBoundsWithBorder(settings, tilePosition);
+ return rasterizeTriangles(context, recastMesh.getMesh(), settings, params, solid)
+ && rasterizeTriangles(context, agentHalfExtents, recastMesh.getWater(), settings, params, realTileBounds, solid)
+ && rasterizeTriangles(context, recastMesh.getHeightfields(), settings, params, solid)
+ && rasterizeTriangles(context, realTileBounds, recastMesh.getFlatHeightfields(), settings, params, solid);
}
void buildCompactHeightfield(rcContext& context, const int walkableHeight, const int walkableClimb,
@@ -353,26 +354,25 @@ namespace
polyMesh.flags[i] = getFlag(static_cast<AreaType>(polyMesh.areas[i]));
}
- bool fillPolyMesh(rcContext& context, const rcConfig& config, rcHeightfield& solid, rcPolyMesh& polyMesh,
- rcPolyMeshDetail& polyMeshDetail)
+ bool fillPolyMesh(rcContext& context, const RecastSettings& settings, const RecastParams& params,
+ rcHeightfield& solid, rcPolyMesh& polyMesh, rcPolyMeshDetail& polyMeshDetail)
{
rcCompactHeightfield compact;
- buildCompactHeightfield(context, config.walkableHeight, config.walkableClimb, solid, compact);
+ buildCompactHeightfield(context, params.mWalkableHeight, params.mWalkableClimb, solid, compact);
- erodeWalkableArea(context, config.walkableRadius, compact);
+ erodeWalkableArea(context, params.mWalkableRadius, compact);
buildDistanceField(context, compact);
- buildRegions(context, compact, config.borderSize, config.minRegionArea, config.mergeRegionArea);
+ buildRegions(context, compact, settings.mBorderSize, settings.mRegionMinArea, settings.mRegionMergeArea);
rcContourSet contourSet;
- buildContours(context, compact, config.maxSimplificationError, config.maxEdgeLen, contourSet);
+ buildContours(context, compact, settings.mMaxSimplificationError, params.mMaxEdgeLen, contourSet);
if (contourSet.nconts == 0)
return false;
- buildPolyMesh(context, contourSet, config.maxVertsPerPoly, polyMesh);
+ buildPolyMesh(context, contourSet, settings.mMaxVertsPerPoly, polyMesh);
- buildPolyMeshDetail(context, polyMesh, compact, config.detailSampleDist, config.detailSampleMaxError,
- polyMeshDetail);
+ buildPolyMeshDetail(context, polyMesh, compact, params.mSampleDist, params.mSampleMaxError, polyMeshDetail);
setPolyMeshFlags(polyMesh);
@@ -387,44 +387,82 @@ namespace
++power;
return power;
}
+
+ std::pair<float, float> getBoundsByZ(const RecastMesh& recastMesh, const osg::Vec3f& agentHalfExtents, const RecastSettings& settings)
+ {
+ float minZ = 0;
+ float maxZ = 0;
+
+ const std::vector<float>& vertices = recastMesh.getMesh().getVertices();
+ for (std::size_t i = 0, n = vertices.size(); i < n; i += 3)
+ {
+ minZ = std::min(minZ, vertices[i + 2]);
+ maxZ = std::max(maxZ, vertices[i + 2]);
+ }
+
+ for (const CellWater& water : recastMesh.getWater())
+ {
+ const float swimLevel = getSwimLevel(settings, water.mWater.mLevel, agentHalfExtents.z());
+ minZ = std::min(minZ, swimLevel);
+ maxZ = std::max(maxZ, swimLevel);
+ }
+
+ for (const Heightfield& heightfield : recastMesh.getHeightfields())
+ {
+ if (heightfield.mHeights.empty())
+ continue;
+ const auto [minHeight, maxHeight] = std::minmax_element(heightfield.mHeights.begin(), heightfield.mHeights.end());
+ minZ = std::min(minZ, *minHeight);
+ maxZ = std::max(maxZ, *maxHeight);
+ }
+
+ for (const FlatHeightfield& heightfield : recastMesh.getFlatHeightfields())
+ {
+ minZ = std::min(minZ, heightfield.mHeight);
+ maxZ = std::max(maxZ, heightfield.mHeight);
+ }
+
+ return {minZ, maxZ};
+ }
}
+} // namespace DetourNavigator
namespace DetourNavigator
{
std::unique_ptr<PreparedNavMeshData> prepareNavMeshTileData(const RecastMesh& recastMesh,
- const TilePosition& tile, const Bounds& bounds, const osg::Vec3f& agentHalfExtents, const Settings& settings)
+ const TilePosition& tilePosition, const osg::Vec3f& agentHalfExtents, const RecastSettings& settings)
{
- const TileBounds tileBounds = makeTileBounds(settings, tile);
- const osg::Vec3f boundsMin(tileBounds.mMin.x(), bounds.mMin.y() - 1, tileBounds.mMin.y());
- const osg::Vec3f boundsMax(tileBounds.mMax.x(), bounds.mMax.y() + 1, tileBounds.mMax.y());
-
rcContext context;
- const auto config = makeConfig(agentHalfExtents, boundsMin, boundsMax, settings);
+
+ const auto [minZ, maxZ] = getBoundsByZ(recastMesh, agentHalfExtents, settings);
rcHeightfield solid;
- createHeightfield(context, solid, config.width, config.height, config.bmin, config.bmax, config.cs, config.ch);
+ initHeightfield(context, tilePosition, toNavMeshCoordinates(settings, minZ),
+ toNavMeshCoordinates(settings, maxZ), settings, solid);
+
+ const RecastParams params = makeRecastParams(settings, agentHalfExtents);
- if (!rasterizeTriangles(context, agentHalfExtents, recastMesh, config, settings, solid))
+ if (!rasterizeTriangles(context, tilePosition, agentHalfExtents, recastMesh, settings, params, solid))
return nullptr;
- rcFilterLowHangingWalkableObstacles(&context, config.walkableClimb, solid);
- rcFilterLedgeSpans(&context, config.walkableHeight, config.walkableClimb, solid);
- rcFilterWalkableLowHeightSpans(&context, config.walkableHeight, solid);
+ rcFilterLowHangingWalkableObstacles(&context, params.mWalkableClimb, solid);
+ rcFilterLedgeSpans(&context, params.mWalkableHeight, params.mWalkableClimb, solid);
+ rcFilterWalkableLowHeightSpans(&context, params.mWalkableHeight, solid);
std::unique_ptr<PreparedNavMeshData> result = std::make_unique<PreparedNavMeshData>();
- if (!fillPolyMesh(context, config, solid, result->mPolyMesh, result->mPolyMeshDetail))
+ if (!fillPolyMesh(context, settings, params, solid, result->mPolyMesh, result->mPolyMeshDetail))
return nullptr;
- result->mCellSize = config.cs;
- result->mCellHeight = config.ch;
+ result->mCellSize = settings.mCellSize;
+ result->mCellHeight = settings.mCellHeight;
return result;
}
NavMeshData makeNavMeshTileData(const PreparedNavMeshData& data,
const std::vector<OffMeshConnection>& offMeshConnections, const osg::Vec3f& agentHalfExtents,
- const TilePosition& tile, const Settings& settings)
+ const TilePosition& tile, const RecastSettings& settings)
{
const auto offMeshConVerts = getOffMeshVerts(offMeshConnections);
const std::vector<float> offMeshConRad(offMeshConnections.size(), getRadius(settings, agentHalfExtents));
@@ -480,7 +518,7 @@ namespace DetourNavigator
// Max tiles and max polys affect how the tile IDs are caculated.
// There are 22 bits available for identifying a tile and a polygon.
const int polysAndTilesBits = 22;
- const auto polysBits = getMinValuableBitsNumber(settings.mMaxPolys);
+ const auto polysBits = getMinValuableBitsNumber(settings.mDetour.mMaxPolys);
if (polysBits >= polysAndTilesBits)
throw InvalidArgument("Too many polygons per tile");
@@ -489,8 +527,8 @@ namespace DetourNavigator
dtNavMeshParams params;
std::fill_n(params.orig, 3, 0.0f);
- params.tileWidth = settings.mTileSize * settings.mCellSize;
- params.tileHeight = settings.mTileSize * settings.mCellSize;
+ params.tileWidth = settings.mRecast.mTileSize * settings.mRecast.mCellSize;
+ params.tileHeight = settings.mRecast.mTileSize * settings.mRecast.mCellSize;
params.maxTiles = 1 << tilesBits;
params.maxPolys = 1 << polysBits;
@@ -506,83 +544,4 @@ namespace DetourNavigator
return navMesh;
}
-
- UpdateNavMeshStatus updateNavMesh(const osg::Vec3f& agentHalfExtents, const RecastMesh* recastMesh,
- const TilePosition& changedTile, const TilePosition& playerTile,
- const std::vector<OffMeshConnection>& offMeshConnections, const Settings& settings,
- const SharedNavMeshCacheItem& navMeshCacheItem, NavMeshTilesCache& navMeshTilesCache, UpdateType updateType)
- {
- Log(Debug::Debug) << std::fixed << std::setprecision(2) <<
- "Update NavMesh with multiple tiles:" <<
- " agentHeight=" << getHeight(settings, agentHalfExtents) <<
- " agentMaxClimb=" << getMaxClimb(settings) <<
- " agentRadius=" << getRadius(settings, agentHalfExtents) <<
- " changedTile=(" << changedTile << ")" <<
- " playerTile=(" << playerTile << ")" <<
- " changedTileDistance=" << getDistance(changedTile, playerTile);
-
- if (!recastMesh)
- {
- Log(Debug::Debug) << "Ignore add tile: recastMesh is null";
- return navMeshCacheItem->lock()->removeTile(changedTile);
- }
-
- auto recastMeshBounds = recastMesh->getBounds();
- recastMeshBounds.mMin = toNavMeshCoordinates(settings, recastMeshBounds.mMin);
- recastMeshBounds.mMax = toNavMeshCoordinates(settings, recastMeshBounds.mMax);
-
- for (const auto& water : recastMesh->getWater())
- {
- const float height = toNavMeshCoordinates(settings, getSwimLevel(settings, water.mShift.z(), agentHalfExtents.z()));
- recastMeshBounds.mMin.y() = std::min(recastMeshBounds.mMin.y(), height);
- recastMeshBounds.mMax.y() = std::max(recastMeshBounds.mMax.y(), height);
- }
-
- if (isEmpty(recastMeshBounds))
- {
- Log(Debug::Debug) << "Ignore add tile: recastMesh is empty";
- return navMeshCacheItem->lock()->removeTile(changedTile);
- }
-
- const dtNavMeshParams params = *navMeshCacheItem->lockConst()->getImpl().getParams();
-
- if (!shouldAddTile(changedTile, playerTile, std::min(settings.mMaxTilesNumber, params.maxTiles)))
- {
- Log(Debug::Debug) << "Ignore add tile: too far from player";
- return navMeshCacheItem->lock()->removeTile(changedTile);
- }
-
- auto cachedNavMeshData = navMeshTilesCache.get(agentHalfExtents, changedTile, *recastMesh);
- bool cached = static_cast<bool>(cachedNavMeshData);
-
- if (!cachedNavMeshData)
- {
- auto prepared = prepareNavMeshTileData(*recastMesh, changedTile, recastMeshBounds,
- agentHalfExtents, settings);
-
- if (prepared == nullptr)
- {
- Log(Debug::Debug) << "Ignore add tile: NavMeshData is null";
- return navMeshCacheItem->lock()->removeTile(changedTile);
- }
-
- if (updateType == UpdateType::Temporary)
- return navMeshCacheItem->lock()->updateTile(changedTile, NavMeshTilesCache::Value(),
- makeNavMeshTileData(*prepared, offMeshConnections, agentHalfExtents, changedTile, settings));
-
- cachedNavMeshData = navMeshTilesCache.set(agentHalfExtents, changedTile, *recastMesh, std::move(prepared));
-
- if (!cachedNavMeshData)
- {
- Log(Debug::Debug) << "Navigator cache overflow";
- return navMeshCacheItem->lock()->updateTile(changedTile, NavMeshTilesCache::Value(),
- makeNavMeshTileData(*prepared, offMeshConnections, agentHalfExtents, changedTile, settings));
- }
- }
-
- const auto updateStatus = navMeshCacheItem->lock()->updateTile(changedTile, std::move(cachedNavMeshData),
- makeNavMeshTileData(cachedNavMeshData.get(), offMeshConnections, agentHalfExtents, changedTile, settings));
-
- return UpdateNavMeshStatusBuilder(updateStatus).cached(cached).getResult();
- }
}
diff --git a/components/detournavigator/makenavmesh.hpp b/components/detournavigator/makenavmesh.hpp
index 5b4169374b..14919ab134 100644
--- a/components/detournavigator/makenavmesh.hpp
+++ b/components/detournavigator/makenavmesh.hpp
@@ -7,6 +7,9 @@
#include "sharednavmesh.hpp"
#include "navmeshtilescache.hpp"
#include "offmeshconnection.hpp"
+#include "navmeshdb.hpp"
+
+#include <components/misc/guarded.hpp>
#include <osg/Vec3f>
@@ -14,6 +17,7 @@
#include <vector>
class dtNavMesh;
+struct rcConfig;
namespace DetourNavigator
{
@@ -38,25 +42,22 @@ namespace DetourNavigator
return expectedTilesCount <= maxTiles;
}
- std::unique_ptr<PreparedNavMeshData> prepareNavMeshTileData(const RecastMesh& recastMesh, const TilePosition& tile,
- const Bounds& bounds, const osg::Vec3f& agentHalfExtents, const Settings& settings);
+ inline bool isEmpty(const RecastMesh& recastMesh)
+ {
+ return recastMesh.getMesh().getIndices().empty()
+ && recastMesh.getWater().empty()
+ && recastMesh.getHeightfields().empty()
+ && recastMesh.getFlatHeightfields().empty();
+ }
+
+ std::unique_ptr<PreparedNavMeshData> prepareNavMeshTileData(const RecastMesh& recastMesh,
+ const TilePosition& tilePosition, const osg::Vec3f& agentHalfExtents, const RecastSettings& settings);
NavMeshData makeNavMeshTileData(const PreparedNavMeshData& data,
const std::vector<OffMeshConnection>& offMeshConnections, const osg::Vec3f& agentHalfExtents,
- const TilePosition& tile, const Settings& settings);
+ const TilePosition& tile, const RecastSettings& settings);
NavMeshPtr makeEmptyNavMesh(const Settings& settings);
-
- enum class UpdateType
- {
- Persistent,
- Temporary
- };
-
- UpdateNavMeshStatus updateNavMesh(const osg::Vec3f& agentHalfExtents, const RecastMesh* recastMesh,
- const TilePosition& changedTile, const TilePosition& playerTile,
- const std::vector<OffMeshConnection>& offMeshConnections, const Settings& settings,
- const SharedNavMeshCacheItem& navMeshCacheItem, NavMeshTilesCache& navMeshTilesCache, UpdateType updateType);
}
#endif
diff --git a/components/detournavigator/navigator.cpp b/components/detournavigator/navigator.cpp
index 700217c52f..cf3c4ba5b3 100644
--- a/components/detournavigator/navigator.cpp
+++ b/components/detournavigator/navigator.cpp
@@ -1,36 +1,23 @@
-#include "findrandompointaroundcircle.hpp"
#include "navigator.hpp"
-#include "raycast.hpp"
+#include "navigatorimpl.hpp"
+#include "navigatorstub.hpp"
+#include "recastglobalallocator.hpp"
namespace DetourNavigator
{
- std::optional<osg::Vec3f> Navigator::findRandomPointAroundCircle(const osg::Vec3f& agentHalfExtents,
- const osg::Vec3f& start, const float maxRadius, const Flags includeFlags) const
+ std::unique_ptr<Navigator> makeNavigator(const Settings& settings, const std::string& userDataPath)
{
- const auto navMesh = getNavMesh(agentHalfExtents);
- if (!navMesh)
- return std::optional<osg::Vec3f>();
- const auto settings = getSettings();
- const auto result = DetourNavigator::findRandomPointAroundCircle(navMesh->lockConst()->getImpl(),
- toNavMeshCoordinates(settings, agentHalfExtents), toNavMeshCoordinates(settings, start),
- toNavMeshCoordinates(settings, maxRadius), includeFlags, settings);
- if (!result)
- return std::optional<osg::Vec3f>();
- return std::optional<osg::Vec3f>(fromNavMeshCoordinates(settings, *result));
+ DetourNavigator::RecastGlobalAllocator::init();
+
+ std::unique_ptr<NavMeshDb> db;
+ if (settings.mEnableNavMeshDiskCache)
+ db = std::make_unique<NavMeshDb>(userDataPath + "/navmesh.db");
+
+ return std::make_unique<NavigatorImpl>(settings, std::move(db));
}
- std::optional<osg::Vec3f> Navigator::raycast(const osg::Vec3f& agentHalfExtents, const osg::Vec3f& start,
- const osg::Vec3f& end, const Flags includeFlags) const
+ std::unique_ptr<Navigator> makeNavigatorStub()
{
- const auto navMesh = getNavMesh(agentHalfExtents);
- if (navMesh == nullptr)
- return {};
- const auto settings = getSettings();
- const auto result = DetourNavigator::raycast(navMesh->lockConst()->getImpl(),
- toNavMeshCoordinates(settings, agentHalfExtents), toNavMeshCoordinates(settings, start),
- toNavMeshCoordinates(settings, end), includeFlags, settings);
- if (!result)
- return {};
- return fromNavMeshCoordinates(settings, *result);
+ return std::make_unique<NavigatorStub>();
}
}
diff --git a/components/detournavigator/navigator.hpp b/components/detournavigator/navigator.hpp
index 265d69b6e1..14731fcc5b 100644
--- a/components/detournavigator/navigator.hpp
+++ b/components/detournavigator/navigator.hpp
@@ -1,18 +1,16 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATOR_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATOR_H
-#include "findsmoothpath.hpp"
-#include "flags.hpp"
-#include "settings.hpp"
#include "objectid.hpp"
#include "navmeshcacheitem.hpp"
#include "recastmeshtiles.hpp"
#include "waitconditiontype.hpp"
#include "heightfieldshape.hpp"
+#include "objecttransform.hpp"
#include <components/resource/bulletshape.hpp>
-#include <variant>
+#include <string_view>
namespace ESM
{
@@ -27,13 +25,19 @@ namespace Loading
namespace DetourNavigator
{
+ struct Settings;
+
struct ObjectShapes
{
osg::ref_ptr<const Resource::BulletShapeInstance> mShapeInstance;
+ ObjectTransform mTransform;
- ObjectShapes(const osg::ref_ptr<const Resource::BulletShapeInstance>& shapeInstance)
+ ObjectShapes(const osg::ref_ptr<const Resource::BulletShapeInstance>& shapeInstance, const ObjectTransform& transform)
: mShapeInstance(shapeInstance)
- {}
+ , mTransform(transform)
+ {
+ assert(mShapeInstance != nullptr);
+ }
};
struct DoorShapes : ObjectShapes
@@ -42,8 +46,8 @@ namespace DetourNavigator
osg::Vec3f mConnectionEnd;
DoorShapes(const osg::ref_ptr<const Resource::BulletShapeInstance>& shapeInstance,
- const osg::Vec3f& connectionStart,const osg::Vec3f& connectionEnd)
- : ObjectShapes(shapeInstance)
+ const ObjectTransform& transform, const osg::Vec3f& connectionStart, const osg::Vec3f& connectionEnd)
+ : ObjectShapes(shapeInstance, transform)
, mConnectionStart(connectionStart)
, mConnectionEnd(connectionEnd)
{}
@@ -74,6 +78,12 @@ namespace DetourNavigator
virtual void removeAgent(const osg::Vec3f& agentHalfExtents) = 0;
/**
+ * @brief setWorldspace should be called before adding object from new worldspace
+ * @param worldspace
+ */
+ virtual void setWorldspace(std::string_view worldspace) = 0;
+
+ /**
* @brief addObject is used to add complex object with allowed to walk and avoided to walk shapes
* @param id is used to distinguish different objects
* @param shape members must live until object is updated by another shape removed from Navigator
@@ -125,7 +135,7 @@ namespace DetourNavigator
* at least single object is added to the scene, false if there is already water for given cell or there is no
* any other objects.
*/
- virtual bool addWater(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift) = 0;
+ virtual bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level) = 0;
/**
* @brief removeWater to make it no more available at the scene.
@@ -134,8 +144,7 @@ namespace DetourNavigator
*/
virtual bool removeWater(const osg::Vec2i& cellPosition) = 0;
- virtual bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift,
- const HeightfieldShape& shape) = 0;
+ virtual bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape) = 0;
virtual bool removeHeightfield(const osg::Vec2i& cellPosition) = 0;
@@ -167,38 +176,6 @@ namespace DetourNavigator
virtual void wait(Loading::Listener& listener, WaitConditionType waitConditionType) = 0;
/**
- * @brief findPath fills output iterator with points of scene surfaces to be used for actor to walk through.
- * @param agentHalfExtents allows to find navmesh for given actor.
- * @param start path from given point.
- * @param end path at given point.
- * @param includeFlags setup allowed surfaces for actor to walk.
- * @param out the beginning of the destination range.
- * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents
- * @return Output iterator to the element in the destination range, one past the last element of found path.
- * Equal to out if no path is found.
- */
- template <class OutputIterator>
- Status findPath(const osg::Vec3f& agentHalfExtents, const float stepSize, const osg::Vec3f& start,
- const osg::Vec3f& end, const Flags includeFlags, const DetourNavigator::AreaCosts& areaCosts,
- float endTolerance, OutputIterator& out) const
- {
- static_assert(
- std::is_same<
- typename std::iterator_traits<OutputIterator>::iterator_category,
- std::output_iterator_tag
- >::value,
- "out is not an OutputIterator"
- );
- const auto navMesh = getNavMesh(agentHalfExtents);
- if (!navMesh)
- return Status::NavMeshNotFound;
- const auto settings = getSettings();
- return findSmoothPath(navMesh->lockConst()->getImpl(), toNavMeshCoordinates(settings, agentHalfExtents),
- toNavMeshCoordinates(settings, stepSize), toNavMeshCoordinates(settings, start),
- toNavMeshCoordinates(settings, end), includeFlags, areaCosts, settings, endTolerance, out);
- }
-
- /**
* @brief getNavMesh returns navmesh for specific agent half extents
* @return navmesh
*/
@@ -214,32 +191,14 @@ namespace DetourNavigator
virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0;
- /**
- * @brief findRandomPointAroundCircle returns random location on navmesh within the reach of specified location.
- * @param agentHalfExtents allows to find navmesh for given actor.
- * @param start path from given point.
- * @param maxRadius limit maximum distance from start.
- * @param includeFlags setup allowed surfaces for actor to walk.
- * @return not empty optional with position if point is found and empty optional if point is not found.
- */
- std::optional<osg::Vec3f> findRandomPointAroundCircle(const osg::Vec3f& agentHalfExtents,
- const osg::Vec3f& start, const float maxRadius, const Flags includeFlags) const;
-
- /**
- * @brief raycast finds farest navmesh point from start on a line from start to end that has path from start.
- * @param agentHalfExtents allows to find navmesh for given actor.
- * @param start of the line
- * @param end of the line
- * @param includeFlags setup allowed surfaces for actor to walk.
- * @return not empty optional with position if point is found and empty optional if point is not found.
- */
- std::optional<osg::Vec3f> raycast(const osg::Vec3f& agentHalfExtents, const osg::Vec3f& start,
- const osg::Vec3f& end, const Flags includeFlags) const;
-
virtual RecastMeshTiles getRecastMeshTiles() const = 0;
virtual float getMaxNavmeshAreaRealRadius() const = 0;
};
+
+ std::unique_ptr<Navigator> makeNavigator(const Settings& settings, const std::string& userDataPath);
+
+ std::unique_ptr<Navigator> makeNavigatorStub();
}
#endif
diff --git a/components/detournavigator/navigatorimpl.cpp b/components/detournavigator/navigatorimpl.cpp
index 44b42b22c2..85d86e6b2b 100644
--- a/components/detournavigator/navigatorimpl.cpp
+++ b/components/detournavigator/navigatorimpl.cpp
@@ -8,9 +8,9 @@
namespace DetourNavigator
{
- NavigatorImpl::NavigatorImpl(const Settings& settings)
+ NavigatorImpl::NavigatorImpl(const Settings& settings, std::unique_ptr<NavMeshDb>&& db)
: mSettings(settings)
- , mNavMeshManager(mSettings)
+ , mNavMeshManager(mSettings, std::move(db))
, mUpdatesEnabled(true)
{
}
@@ -32,14 +32,19 @@ namespace DetourNavigator
--it->second;
}
+ void NavigatorImpl::setWorldspace(std::string_view worldspace)
+ {
+ mNavMeshManager.setWorldspace(worldspace);
+ }
+
bool NavigatorImpl::addObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform)
{
- CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->mCollisionShape};
+ const CollisionShape collisionShape(shapes.mShapeInstance, *shapes.mShapeInstance->mCollisionShape, shapes.mTransform);
bool result = mNavMeshManager.addObject(id, collisionShape, transform, AreaType_ground);
if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->mAvoidCollisionShape.get())
{
const ObjectId avoidId(avoidShape);
- CollisionShape avoidCollisionShape {shapes.mShapeInstance, *avoidShape};
+ const CollisionShape avoidCollisionShape(shapes.mShapeInstance, *avoidShape, shapes.mTransform);
if (mNavMeshManager.addObject(avoidId, avoidCollisionShape, transform, AreaType_null))
{
updateAvoidShapeId(id, avoidId);
@@ -53,8 +58,8 @@ namespace DetourNavigator
{
if (addObject(id, static_cast<const ObjectShapes&>(shapes), transform))
{
- const osg::Vec3f start = toNavMeshCoordinates(mSettings, shapes.mConnectionStart);
- const osg::Vec3f end = toNavMeshCoordinates(mSettings, shapes.mConnectionEnd);
+ const osg::Vec3f start = toNavMeshCoordinates(mSettings.mRecast, shapes.mConnectionStart);
+ const osg::Vec3f end = toNavMeshCoordinates(mSettings.mRecast, shapes.mConnectionEnd);
mNavMeshManager.addOffMeshConnection(id, start, end, AreaType_door);
mNavMeshManager.addOffMeshConnection(id, end, start, AreaType_door);
return true;
@@ -64,12 +69,12 @@ namespace DetourNavigator
bool NavigatorImpl::updateObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform)
{
- const CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->mCollisionShape};
+ const CollisionShape collisionShape(shapes.mShapeInstance, *shapes.mShapeInstance->mCollisionShape, shapes.mTransform);
bool result = mNavMeshManager.updateObject(id, collisionShape, transform, AreaType_ground);
if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->mAvoidCollisionShape.get())
{
const ObjectId avoidId(avoidShape);
- const CollisionShape avoidCollisionShape {shapes.mShapeInstance, *avoidShape};
+ const CollisionShape avoidCollisionShape(shapes.mShapeInstance, *avoidShape, shapes.mTransform);
if (mNavMeshManager.updateObject(avoidId, avoidCollisionShape, transform, AreaType_null))
{
updateAvoidShapeId(id, avoidId);
@@ -97,9 +102,9 @@ namespace DetourNavigator
return result;
}
- bool NavigatorImpl::addWater(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift)
+ bool NavigatorImpl::addWater(const osg::Vec2i& cellPosition, int cellSize, float level)
{
- return mNavMeshManager.addWater(cellPosition, cellSize, shift);
+ return mNavMeshManager.addWater(cellPosition, cellSize, level);
}
bool NavigatorImpl::removeWater(const osg::Vec2i& cellPosition)
@@ -107,10 +112,9 @@ namespace DetourNavigator
return mNavMeshManager.removeWater(cellPosition);
}
- bool NavigatorImpl::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift,
- const HeightfieldShape& shape)
+ bool NavigatorImpl::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape)
{
- return mNavMeshManager.addHeightfield(cellPosition, cellSize, shift, shape);
+ return mNavMeshManager.addHeightfield(cellPosition, cellSize, shape);
}
bool NavigatorImpl::removeHeightfield(const osg::Vec2i& cellPosition)
@@ -127,8 +131,8 @@ namespace DetourNavigator
const auto dst = Misc::Convert::makeOsgVec3f(converter.toWorldPoint(pathgrid.mPoints[edge.mV1]));
mNavMeshManager.addOffMeshConnection(
ObjectId(&pathgrid),
- toNavMeshCoordinates(mSettings, src),
- toNavMeshCoordinates(mSettings, dst),
+ toNavMeshCoordinates(mSettings.mRecast, src),
+ toNavMeshCoordinates(mSettings.mRecast, dst),
AreaType_pathgrid
);
}
@@ -150,7 +154,7 @@ namespace DetourNavigator
void NavigatorImpl::updatePlayerPosition(const osg::Vec3f& playerPosition)
{
- const TilePosition tilePosition = getTilePosition(mSettings, toNavMeshCoordinates(mSettings, playerPosition));
+ const TilePosition tilePosition = getTilePosition(mSettings.mRecast, toNavMeshCoordinates(mSettings.mRecast, playerPosition));
if (mLastPlayerPosition.has_value() && *mLastPlayerPosition == tilePosition)
return;
update(playerPosition);
@@ -226,6 +230,6 @@ namespace DetourNavigator
float NavigatorImpl::getMaxNavmeshAreaRealRadius() const
{
const auto& settings = getSettings();
- return getRealTileSize(settings) * getMaxNavmeshAreaRadius(settings);
+ return getRealTileSize(settings.mRecast) * getMaxNavmeshAreaRadius(settings);
}
}
diff --git a/components/detournavigator/navigatorimpl.hpp b/components/detournavigator/navigatorimpl.hpp
index 8ed16b9a5b..116817395c 100644
--- a/components/detournavigator/navigatorimpl.hpp
+++ b/components/detournavigator/navigatorimpl.hpp
@@ -5,6 +5,7 @@
#include "navmeshmanager.hpp"
#include <set>
+#include <memory>
namespace DetourNavigator
{
@@ -15,12 +16,14 @@ namespace DetourNavigator
* @brief Navigator constructor initializes all internal data. Constructed object is ready to build a scene.
* @param settings allows to customize navigator work. Constructor is only place to set navigator settings.
*/
- explicit NavigatorImpl(const Settings& settings);
+ explicit NavigatorImpl(const Settings& settings, std::unique_ptr<NavMeshDb>&& db);
void addAgent(const osg::Vec3f& agentHalfExtents) override;
void removeAgent(const osg::Vec3f& agentHalfExtents) override;
+ void setWorldspace(std::string_view worldspace) override;
+
bool addObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) override;
bool addObject(const ObjectId id, const DoorShapes& shapes, const btTransform& transform) override;
@@ -31,12 +34,11 @@ namespace DetourNavigator
bool removeObject(const ObjectId id) override;
- bool addWater(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift) override;
+ bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level) override;
bool removeWater(const osg::Vec2i& cellPosition) override;
- bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift,
- const HeightfieldShape& shape) override;
+ bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape) override;
bool removeHeightfield(const osg::Vec2i& cellPosition) override;
diff --git a/components/detournavigator/navigatorstub.hpp b/components/detournavigator/navigatorstub.hpp
index 758519769b..6a320c1a08 100644
--- a/components/detournavigator/navigatorstub.hpp
+++ b/components/detournavigator/navigatorstub.hpp
@@ -19,6 +19,8 @@ namespace DetourNavigator
void removeAgent(const osg::Vec3f& /*agentHalfExtents*/) override {}
+ void setWorldspace(std::string_view /*worldspace*/) override {}
+
bool addObject(const ObjectId /*id*/, const ObjectShapes& /*shapes*/, const btTransform& /*transform*/) override
{
return false;
@@ -44,7 +46,7 @@ namespace DetourNavigator
return false;
}
- bool addWater(const osg::Vec2i& /*cellPosition*/, int /*cellSize*/, const osg::Vec3f& /*shift*/) override
+ bool addWater(const osg::Vec2i& /*cellPosition*/, int /*cellSize*/, float /*level*/) override
{
return false;
}
@@ -54,8 +56,7 @@ namespace DetourNavigator
return false;
}
- bool addHeightfield(const osg::Vec2i& /*cellPosition*/, int /*cellSize*/, const osg::Vec3f& /*shift*/,
- const HeightfieldShape& /*height*/) override
+ bool addHeightfield(const osg::Vec2i& /*cellPosition*/, int /*cellSize*/, const HeightfieldShape& /*height*/) override
{
return false;
}
diff --git a/components/detournavigator/navigatorutils.cpp b/components/detournavigator/navigatorutils.cpp
new file mode 100644
index 0000000000..bfb0946b9e
--- /dev/null
+++ b/components/detournavigator/navigatorutils.cpp
@@ -0,0 +1,37 @@
+#include "navigatorutils.hpp"
+#include "findrandompointaroundcircle.hpp"
+#include "navigator.hpp"
+#include "raycast.hpp"
+
+namespace DetourNavigator
+{
+ std::optional<osg::Vec3f> findRandomPointAroundCircle(const Navigator& navigator, const osg::Vec3f& agentHalfExtents,
+ const osg::Vec3f& start, const float maxRadius, const Flags includeFlags)
+ {
+ const auto navMesh = navigator.getNavMesh(agentHalfExtents);
+ if (!navMesh)
+ return std::nullopt;
+ const auto& settings = navigator.getSettings();
+ const auto result = DetourNavigator::findRandomPointAroundCircle(navMesh->lockConst()->getImpl(),
+ toNavMeshCoordinates(settings.mRecast, agentHalfExtents), toNavMeshCoordinates(settings.mRecast, start),
+ toNavMeshCoordinates(settings.mRecast, maxRadius), includeFlags, settings.mDetour);
+ if (!result)
+ return std::nullopt;
+ return std::optional<osg::Vec3f>(fromNavMeshCoordinates(settings.mRecast, *result));
+ }
+
+ std::optional<osg::Vec3f> raycast(const Navigator& navigator, const osg::Vec3f& agentHalfExtents, const osg::Vec3f& start,
+ const osg::Vec3f& end, const Flags includeFlags)
+ {
+ const auto navMesh = navigator.getNavMesh(agentHalfExtents);
+ if (navMesh == nullptr)
+ return std::nullopt;
+ const auto& settings = navigator.getSettings();
+ const auto result = DetourNavigator::raycast(navMesh->lockConst()->getImpl(),
+ toNavMeshCoordinates(settings.mRecast, agentHalfExtents), toNavMeshCoordinates(settings.mRecast, start),
+ toNavMeshCoordinates(settings.mRecast, end), includeFlags, settings.mDetour);
+ if (!result)
+ return std::nullopt;
+ return fromNavMeshCoordinates(settings.mRecast, *result);
+ }
+}
diff --git a/components/detournavigator/navigatorutils.hpp b/components/detournavigator/navigatorutils.hpp
new file mode 100644
index 0000000000..8f3b6161f9
--- /dev/null
+++ b/components/detournavigator/navigatorutils.hpp
@@ -0,0 +1,68 @@
+#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATORUTILS_H
+#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATORUTILS_H
+
+#include "findsmoothpath.hpp"
+#include "flags.hpp"
+#include "settings.hpp"
+#include "navigator.hpp"
+
+#include <optional>
+
+namespace DetourNavigator
+{
+ /**
+ * @brief findPath fills output iterator with points of scene surfaces to be used for actor to walk through.
+ * @param agentHalfExtents allows to find navmesh for given actor.
+ * @param start path from given point.
+ * @param end path at given point.
+ * @param includeFlags setup allowed surfaces for actor to walk.
+ * @param out the beginning of the destination range.
+ * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents
+ * @return Output iterator to the element in the destination range, one past the last element of found path.
+ * Equal to out if no path is found.
+ */
+ template <class OutputIterator>
+ inline Status findPath(const Navigator& navigator, const osg::Vec3f& agentHalfExtents, const float stepSize, const osg::Vec3f& start,
+ const osg::Vec3f& end, const Flags includeFlags, const DetourNavigator::AreaCosts& areaCosts,
+ float endTolerance, OutputIterator& out)
+ {
+ static_assert(
+ std::is_same<
+ typename std::iterator_traits<OutputIterator>::iterator_category,
+ std::output_iterator_tag
+ >::value,
+ "out is not an OutputIterator"
+ );
+ const auto navMesh = navigator.getNavMesh(agentHalfExtents);
+ if (navMesh == nullptr)
+ return Status::NavMeshNotFound;
+ const auto settings = navigator.getSettings();
+ return findSmoothPath(navMesh->lockConst()->getImpl(), toNavMeshCoordinates(settings.mRecast, agentHalfExtents),
+ toNavMeshCoordinates(settings.mRecast, stepSize), toNavMeshCoordinates(settings.mRecast, start),
+ toNavMeshCoordinates(settings.mRecast, end), includeFlags, areaCosts, settings, endTolerance, out);
+ }
+
+ /**
+ * @brief findRandomPointAroundCircle returns random location on navmesh within the reach of specified location.
+ * @param agentHalfExtents allows to find navmesh for given actor.
+ * @param start path from given point.
+ * @param maxRadius limit maximum distance from start.
+ * @param includeFlags setup allowed surfaces for actor to walk.
+ * @return not empty optional with position if point is found and empty optional if point is not found.
+ */
+ std::optional<osg::Vec3f> findRandomPointAroundCircle(const Navigator& navigator, const osg::Vec3f& agentHalfExtents,
+ const osg::Vec3f& start, const float maxRadius, const Flags includeFlags);
+
+ /**
+ * @brief raycast finds farest navmesh point from start on a line from start to end that has path from start.
+ * @param agentHalfExtents allows to find navmesh for given actor.
+ * @param start of the line
+ * @param end of the line
+ * @param includeFlags setup allowed surfaces for actor to walk.
+ * @return not empty optional with position if point is found and empty optional if point is not found.
+ */
+ std::optional<osg::Vec3f> raycast(const Navigator& navigator, const osg::Vec3f& agentHalfExtents, const osg::Vec3f& start,
+ const osg::Vec3f& end, const Flags includeFlags);
+}
+
+#endif
diff --git a/components/detournavigator/navmeshcacheitem.cpp b/components/detournavigator/navmeshcacheitem.cpp
index ee6f3308d0..ffcb0a7359 100644
--- a/components/detournavigator/navmeshcacheitem.cpp
+++ b/components/detournavigator/navmeshcacheitem.cpp
@@ -13,12 +13,6 @@ namespace
{
using DetourNavigator::TilePosition;
- const dtMeshTile* getTile(const dtNavMesh& navMesh, const TilePosition& position)
- {
- const int layer = 0;
- return navMesh.getTileAt(position.x(), position.y(), layer);
- }
-
bool removeTile(dtNavMesh& navMesh, const TilePosition& position)
{
const int layer = 0;
@@ -41,21 +35,39 @@ namespace
namespace DetourNavigator
{
+ const dtMeshTile* getTile(const dtNavMesh& navMesh, const TilePosition& position)
+ {
+ const int layer = 0;
+ return navMesh.getTileAt(position.x(), position.y(), layer);
+ }
+
UpdateNavMeshStatus NavMeshCacheItem::updateTile(const TilePosition& position, NavMeshTilesCache::Value&& cached,
NavMeshData&& navMeshData)
{
- const dtMeshTile* currentTile = ::getTile(*mImpl, position);
+ const dtMeshTile* currentTile = getTile(*mImpl, position);
if (currentTile != nullptr
&& asNavMeshTileConstView(*currentTile) == asNavMeshTileConstView(navMeshData.mValue.get()))
{
return UpdateNavMeshStatus::ignored;
}
- const auto removed = ::removeTile(*mImpl, position);
+ bool removed = ::removeTile(*mImpl, position);
+ removed = mEmptyTiles.erase(position) > 0 || removed;
const auto addStatus = addTile(*mImpl, navMeshData.mValue.get(), navMeshData.mSize);
if (dtStatusSucceed(addStatus))
{
- mUsedTiles[position] = std::make_pair(std::move(cached), std::move(navMeshData));
- ++mNavMeshRevision;
+ auto tile = mUsedTiles.find(position);
+ if (tile == mUsedTiles.end())
+ {
+ mUsedTiles.emplace_hint(tile, position,
+ Tile {Version {mVersion.mRevision, 1}, std::move(cached), std::move(navMeshData)});
+ }
+ else
+ {
+ ++tile->second.mVersion.mRevision;
+ tile->second.mCached = std::move(cached);
+ tile->second.mData = std::move(navMeshData);
+ }
+ ++mVersion.mRevision;
return UpdateNavMeshStatusBuilder().added(true).removed(removed).getResult();
}
else
@@ -63,7 +75,7 @@ namespace DetourNavigator
if (removed)
{
mUsedTiles.erase(position);
- ++mNavMeshRevision;
+ ++mVersion.mRevision;
}
return UpdateNavMeshStatusBuilder().removed(removed).failed((addStatus & DT_OUT_OF_MEMORY) != 0).getResult();
}
@@ -71,12 +83,30 @@ namespace DetourNavigator
UpdateNavMeshStatus NavMeshCacheItem::removeTile(const TilePosition& position)
{
- const auto removed = ::removeTile(*mImpl, position);
+ bool removed = ::removeTile(*mImpl, position);
+ removed = mEmptyTiles.erase(position) > 0 || removed;
if (removed)
{
mUsedTiles.erase(position);
- ++mNavMeshRevision;
+ ++mVersion.mRevision;
}
return UpdateNavMeshStatusBuilder().removed(removed).getResult();
}
+
+ UpdateNavMeshStatus NavMeshCacheItem::markAsEmpty(const TilePosition& position)
+ {
+ bool removed = ::removeTile(*mImpl, position);
+ removed = mEmptyTiles.insert(position).second || removed;
+ if (removed)
+ {
+ mUsedTiles.erase(position);
+ ++mVersion.mRevision;
+ }
+ return UpdateNavMeshStatusBuilder().removed(removed).getResult();
+ }
+
+ bool NavMeshCacheItem::isEmptyTile(const TilePosition& position) const
+ {
+ return mEmptyTiles.find(position) != mEmptyTiles.end();
+ }
}
diff --git a/components/detournavigator/navmeshcacheitem.hpp b/components/detournavigator/navmeshcacheitem.hpp
index ac68caedb3..ae4a2de66b 100644
--- a/components/detournavigator/navmeshcacheitem.hpp
+++ b/components/detournavigator/navmeshcacheitem.hpp
@@ -6,11 +6,13 @@
#include "navmeshtilescache.hpp"
#include "dtstatus.hpp"
#include "navmeshdata.hpp"
+#include "version.hpp"
#include <components/misc/guarded.hpp>
#include <map>
#include <ostream>
+#include <set>
struct dtMeshTile;
@@ -123,11 +125,14 @@ namespace DetourNavigator
}
};
+ const dtMeshTile* getTile(const dtNavMesh& navMesh, const TilePosition& position);
+
class NavMeshCacheItem
{
public:
NavMeshCacheItem(const NavMeshPtr& impl, std::size_t generation)
- : mImpl(impl), mGeneration(generation), mNavMeshRevision(0)
+ : mImpl(impl)
+ , mVersion {generation, 0}
{
}
@@ -136,26 +141,37 @@ namespace DetourNavigator
return *mImpl;
}
- std::size_t getGeneration() const
- {
- return mGeneration;
- }
-
- std::size_t getNavMeshRevision() const
- {
- return mNavMeshRevision;
- }
+ const Version& getVersion() const { return mVersion; }
UpdateNavMeshStatus updateTile(const TilePosition& position, NavMeshTilesCache::Value&& cached,
NavMeshData&& navMeshData);
UpdateNavMeshStatus removeTile(const TilePosition& position);
+ UpdateNavMeshStatus markAsEmpty(const TilePosition& position);
+
+ bool isEmptyTile(const TilePosition& position) const;
+
+ template <class Function>
+ void forEachUsedTile(Function&& function) const
+ {
+ for (const auto& [position, tile] : mUsedTiles)
+ if (const dtMeshTile* meshTile = getTile(*mImpl, position))
+ function(position, tile.mVersion, *meshTile);
+ }
+
private:
+ struct Tile
+ {
+ Version mVersion;
+ NavMeshTilesCache::Value mCached;
+ NavMeshData mData;
+ };
+
NavMeshPtr mImpl;
- std::size_t mGeneration;
- std::size_t mNavMeshRevision;
- std::map<TilePosition, std::pair<NavMeshTilesCache::Value, NavMeshData>> mUsedTiles;
+ Version mVersion;
+ std::map<TilePosition, Tile> mUsedTiles;
+ std::set<TilePosition> mEmptyTiles;
};
using GuardedNavMeshCacheItem = Misc::ScopeGuarded<NavMeshCacheItem>;
diff --git a/components/detournavigator/navmeshdb.cpp b/components/detournavigator/navmeshdb.cpp
new file mode 100644
index 0000000000..ebff250ee0
--- /dev/null
+++ b/components/detournavigator/navmeshdb.cpp
@@ -0,0 +1,296 @@
+#include "navmeshdb.hpp"
+
+#include <components/debug/debuglog.hpp>
+#include <components/misc/compression.hpp>
+#include <components/sqlite3/db.hpp>
+#include <components/sqlite3/request.hpp>
+
+#include <DetourAlloc.h>
+
+#include <sqlite3.h>
+
+#include <cstddef>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace DetourNavigator
+{
+ namespace
+ {
+ constexpr const char schema[] = R"(
+ BEGIN TRANSACTION;
+
+ CREATE TABLE IF NOT EXISTS tiles (
+ tile_id INTEGER PRIMARY KEY,
+ revision INTEGER NOT NULL DEFAULT 1,
+ worldspace TEXT NOT NULL,
+ tile_position_x INTEGER NOT NULL,
+ tile_position_y INTEGER NOT NULL,
+ version INTEGER NOT NULL,
+ input BLOB,
+ data BLOB
+ );
+
+ CREATE UNIQUE INDEX IF NOT EXISTS index_unique_tiles_by_worldspace_and_tile_position_and_input
+ ON tiles (worldspace, tile_position_x, tile_position_y, input);
+
+ CREATE TABLE IF NOT EXISTS shapes (
+ shape_id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL,
+ type INTEGER NOT NULL,
+ hash BLOB NOT NULL
+ );
+
+ CREATE UNIQUE INDEX IF NOT EXISTS index_unique_shapes_by_name_and_type_and_hash
+ ON shapes (name, type, hash);
+
+ COMMIT;
+ )";
+
+ constexpr std::string_view getMaxTileIdQuery = R"(
+ SELECT max(tile_id) FROM tiles
+ )";
+
+ constexpr std::string_view findTileQuery = R"(
+ SELECT tile_id, version
+ FROM tiles
+ WHERE worldspace = :worldspace
+ AND tile_position_x = :tile_position_x
+ AND tile_position_y = :tile_position_y
+ AND input = :input
+ )";
+
+ constexpr std::string_view getTileDataQuery = R"(
+ SELECT tile_id, version, data
+ FROM tiles
+ WHERE worldspace = :worldspace
+ AND tile_position_x = :tile_position_x
+ AND tile_position_y = :tile_position_y
+ AND input = :input
+ )";
+
+ constexpr std::string_view insertTileQuery = R"(
+ INSERT INTO tiles ( tile_id, worldspace, version, tile_position_x, tile_position_y, input, data)
+ VALUES (:tile_id, :worldspace, :version, :tile_position_x, :tile_position_y, :input, :data)
+ )";
+
+ constexpr std::string_view updateTileQuery = R"(
+ UPDATE tiles
+ SET version = :version,
+ data = :data,
+ revision = revision + 1
+ WHERE tile_id = :tile_id
+ )";
+
+ constexpr std::string_view getMaxShapeIdQuery = R"(
+ SELECT max(shape_id) FROM shapes
+ )";
+
+ constexpr std::string_view findShapeIdQuery = R"(
+ SELECT shape_id
+ FROM shapes
+ WHERE name = :name
+ AND type = :type
+ AND hash = :hash
+ )";
+
+ constexpr std::string_view insertShapeQuery = R"(
+ INSERT INTO shapes ( shape_id, name, type, hash)
+ VALUES (:shape_id, :name, :type, :hash)
+ )";
+ }
+
+ std::ostream& operator<<(std::ostream& stream, ShapeType value)
+ {
+ switch (value)
+ {
+ case ShapeType::Collision: return stream << "collision";
+ case ShapeType::Avoid: return stream << "avoid";
+ }
+ return stream << "unknown shape type (" << static_cast<std::underlying_type_t<ShapeType>>(value) << ")";
+ }
+
+ NavMeshDb::NavMeshDb(std::string_view path)
+ : mDb(Sqlite3::makeDb(path, schema))
+ , mGetMaxTileId(*mDb, DbQueries::GetMaxTileId {})
+ , mFindTile(*mDb, DbQueries::FindTile {})
+ , mGetTileData(*mDb, DbQueries::GetTileData {})
+ , mInsertTile(*mDb, DbQueries::InsertTile {})
+ , mUpdateTile(*mDb, DbQueries::UpdateTile {})
+ , mGetMaxShapeId(*mDb, DbQueries::GetMaxShapeId {})
+ , mFindShapeId(*mDb, DbQueries::FindShapeId {})
+ , mInsertShape(*mDb, DbQueries::InsertShape {})
+ {
+ }
+
+ Sqlite3::Transaction NavMeshDb::startTransaction()
+ {
+ return Sqlite3::Transaction(*mDb);
+ }
+
+ TileId NavMeshDb::getMaxTileId()
+ {
+ TileId tileId {0};
+ request(*mDb, mGetMaxTileId, &tileId, 1);
+ return tileId;
+ }
+
+ std::optional<Tile> NavMeshDb::findTile(const std::string& worldspace,
+ const TilePosition& tilePosition, const std::vector<std::byte>& input)
+ {
+ Tile result;
+ auto row = std::tie(result.mTileId, result.mVersion);
+ const std::vector<std::byte> compressedInput = Misc::compress(input);
+ if (&row == request(*mDb, mFindTile, &row, 1, worldspace, tilePosition, compressedInput))
+ return {};
+ return result;
+ }
+
+ std::optional<TileData> NavMeshDb::getTileData(const std::string& worldspace,
+ const TilePosition& tilePosition, const std::vector<std::byte>& input)
+ {
+ TileData result;
+ auto row = std::tie(result.mTileId, result.mVersion, result.mData);
+ const std::vector<std::byte> compressedInput = Misc::compress(input);
+ if (&row == request(*mDb, mGetTileData, &row, 1, worldspace, tilePosition, compressedInput))
+ return {};
+ result.mData = Misc::decompress(result.mData);
+ return result;
+ }
+
+ int NavMeshDb::insertTile(TileId tileId, const std::string& worldspace, const TilePosition& tilePosition,
+ TileVersion version, const std::vector<std::byte>& input, const std::vector<std::byte>& data)
+ {
+ const std::vector<std::byte> compressedInput = Misc::compress(input);
+ const std::vector<std::byte> compressedData = Misc::compress(data);
+ return execute(*mDb, mInsertTile, tileId, worldspace, tilePosition, version, compressedInput, compressedData);
+ }
+
+ int NavMeshDb::updateTile(TileId tileId, TileVersion version, const std::vector<std::byte>& data)
+ {
+ const std::vector<std::byte> compressedData = Misc::compress(data);
+ return execute(*mDb, mUpdateTile, tileId, version, compressedData);
+ }
+
+ ShapeId NavMeshDb::getMaxShapeId()
+ {
+ ShapeId shapeId {0};
+ request(*mDb, mGetMaxShapeId, &shapeId, 1);
+ return shapeId;
+ }
+
+ std::optional<ShapeId> NavMeshDb::findShapeId(const std::string& name, ShapeType type,
+ const Sqlite3::ConstBlob& hash)
+ {
+ ShapeId shapeId;
+ if (&shapeId == request(*mDb, mFindShapeId, &shapeId, 1, name, type, hash))
+ return {};
+ return shapeId;
+ }
+
+ int NavMeshDb::insertShape(ShapeId shapeId, const std::string& name, ShapeType type,
+ const Sqlite3::ConstBlob& hash)
+ {
+ return execute(*mDb, mInsertShape, shapeId, name, type, hash);
+ }
+
+ namespace DbQueries
+ {
+ std::string_view GetMaxTileId::text() noexcept
+ {
+ return getMaxTileIdQuery;
+ }
+
+ std::string_view FindTile::text() noexcept
+ {
+ return findTileQuery;
+ }
+
+ void FindTile::bind(sqlite3& db, sqlite3_stmt& statement, const std::string& worldspace,
+ const TilePosition& tilePosition, const std::vector<std::byte>& input)
+ {
+ Sqlite3::bindParameter(db, statement, ":worldspace", worldspace);
+ Sqlite3::bindParameter(db, statement, ":tile_position_x", tilePosition.x());
+ Sqlite3::bindParameter(db, statement, ":tile_position_y", tilePosition.y());
+ Sqlite3::bindParameter(db, statement, ":input", input);
+ }
+
+ std::string_view GetTileData::text() noexcept
+ {
+ return getTileDataQuery;
+ }
+
+ void GetTileData::bind(sqlite3& db, sqlite3_stmt& statement, const std::string& worldspace,
+ const TilePosition& tilePosition, const std::vector<std::byte>& input)
+ {
+ Sqlite3::bindParameter(db, statement, ":worldspace", worldspace);
+ Sqlite3::bindParameter(db, statement, ":tile_position_x", tilePosition.x());
+ Sqlite3::bindParameter(db, statement, ":tile_position_y", tilePosition.y());
+ Sqlite3::bindParameter(db, statement, ":input", input);
+ }
+
+ std::string_view InsertTile::text() noexcept
+ {
+ return insertTileQuery;
+ }
+
+ void InsertTile::bind(sqlite3& db, sqlite3_stmt& statement, TileId tileId, const std::string& worldspace,
+ const TilePosition& tilePosition, TileVersion version, const std::vector<std::byte>& input,
+ const std::vector<std::byte>& data)
+ {
+ Sqlite3::bindParameter(db, statement, ":tile_id", tileId);
+ Sqlite3::bindParameter(db, statement, ":worldspace", worldspace);
+ Sqlite3::bindParameter(db, statement, ":tile_position_x", tilePosition.x());
+ Sqlite3::bindParameter(db, statement, ":tile_position_y", tilePosition.y());
+ Sqlite3::bindParameter(db, statement, ":version", version);
+ Sqlite3::bindParameter(db, statement, ":input", input);
+ Sqlite3::bindParameter(db, statement, ":data", data);
+ }
+
+ std::string_view UpdateTile::text() noexcept
+ {
+ return updateTileQuery;
+ }
+
+ void UpdateTile::bind(sqlite3& db, sqlite3_stmt& statement, TileId tileId, TileVersion version,
+ const std::vector<std::byte>& data)
+ {
+ Sqlite3::bindParameter(db, statement, ":tile_id", tileId);
+ Sqlite3::bindParameter(db, statement, ":version", version);
+ Sqlite3::bindParameter(db, statement, ":data", data);
+ }
+
+ std::string_view GetMaxShapeId::text() noexcept
+ {
+ return getMaxShapeIdQuery;
+ }
+
+ std::string_view FindShapeId::text() noexcept
+ {
+ return findShapeIdQuery;
+ }
+
+ void FindShapeId::bind(sqlite3& db, sqlite3_stmt& statement, const std::string& name,
+ ShapeType type, const Sqlite3::ConstBlob& hash)
+ {
+ Sqlite3::bindParameter(db, statement, ":name", name);
+ Sqlite3::bindParameter(db, statement, ":type", static_cast<int>(type));
+ Sqlite3::bindParameter(db, statement, ":hash", hash);
+ }
+
+ std::string_view InsertShape::text() noexcept
+ {
+ return insertShapeQuery;
+ }
+
+ void InsertShape::bind(sqlite3& db, sqlite3_stmt& statement, ShapeId shapeId, const std::string& name,
+ ShapeType type, const Sqlite3::ConstBlob& hash)
+ {
+ Sqlite3::bindParameter(db, statement, ":shape_id", shapeId);
+ Sqlite3::bindParameter(db, statement, ":name", name);
+ Sqlite3::bindParameter(db, statement, ":type", static_cast<int>(type));
+ Sqlite3::bindParameter(db, statement, ":hash", hash);
+ }
+ }
+}
diff --git a/components/detournavigator/navmeshdb.hpp b/components/detournavigator/navmeshdb.hpp
new file mode 100644
index 0000000000..636f1de000
--- /dev/null
+++ b/components/detournavigator/navmeshdb.hpp
@@ -0,0 +1,153 @@
+#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDB_H
+#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDB_H
+
+#include "tileposition.hpp"
+
+#include <components/sqlite3/db.hpp>
+#include <components/sqlite3/statement.hpp>
+#include <components/sqlite3/transaction.hpp>
+#include <components/sqlite3/types.hpp>
+
+#include <boost/serialization/strong_typedef.hpp>
+
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <optional>
+#include <stdexcept>
+#include <string>
+#include <string_view>
+#include <tuple>
+#include <utility>
+#include <vector>
+#include <memory>
+
+struct sqlite3;
+struct sqlite3_stmt;
+
+namespace DetourNavigator
+{
+ BOOST_STRONG_TYPEDEF(std::int64_t, TileId)
+ BOOST_STRONG_TYPEDEF(std::int64_t, TileRevision)
+ BOOST_STRONG_TYPEDEF(std::int64_t, TileVersion)
+ BOOST_STRONG_TYPEDEF(std::int64_t, ShapeId)
+
+ struct Tile
+ {
+ TileId mTileId;
+ TileVersion mVersion;
+ };
+
+ struct TileData
+ {
+ TileId mTileId;
+ TileVersion mVersion;
+ std::vector<std::byte> mData;
+ };
+
+ enum class ShapeType
+ {
+ Collision = 1,
+ Avoid = 2,
+ };
+
+ std::ostream& operator<<(std::ostream& stream, ShapeType value);
+
+ namespace DbQueries
+ {
+ struct GetMaxTileId
+ {
+ static std::string_view text() noexcept;
+ static void bind(sqlite3&, sqlite3_stmt&) {}
+ };
+
+ struct FindTile
+ {
+ static std::string_view text() noexcept;
+ static void bind(sqlite3& db, sqlite3_stmt& statement, const std::string& worldspace,
+ const TilePosition& tilePosition, const std::vector<std::byte>& input);
+ };
+
+ struct GetTileData
+ {
+ static std::string_view text() noexcept;
+ static void bind(sqlite3& db, sqlite3_stmt& statement, const std::string& worldspace,
+ const TilePosition& tilePosition, const std::vector<std::byte>& input);
+ };
+
+ struct InsertTile
+ {
+ static std::string_view text() noexcept;
+ static void bind(sqlite3& db, sqlite3_stmt& statement, TileId tileId, const std::string& worldspace,
+ const TilePosition& tilePosition, TileVersion version, const std::vector<std::byte>& input,
+ const std::vector<std::byte>& data);
+ };
+
+ struct UpdateTile
+ {
+ static std::string_view text() noexcept;
+ static void bind(sqlite3& db, sqlite3_stmt& statement, TileId tileId, TileVersion version,
+ const std::vector<std::byte>& data);
+ };
+
+ struct GetMaxShapeId
+ {
+ static std::string_view text() noexcept;
+ static void bind(sqlite3&, sqlite3_stmt&) {}
+ };
+
+ struct FindShapeId
+ {
+ static std::string_view text() noexcept;
+ static void bind(sqlite3& db, sqlite3_stmt& statement, const std::string& name,
+ ShapeType type, const Sqlite3::ConstBlob& hash);
+ };
+
+ struct InsertShape
+ {
+ static std::string_view text() noexcept;
+ static void bind(sqlite3& db, sqlite3_stmt& statement, ShapeId shapeId, const std::string& name,
+ ShapeType type, const Sqlite3::ConstBlob& hash);
+ };
+ }
+
+ class NavMeshDb
+ {
+ public:
+ explicit NavMeshDb(std::string_view path);
+
+ Sqlite3::Transaction startTransaction();
+
+ TileId getMaxTileId();
+
+ std::optional<Tile> findTile(const std::string& worldspace,
+ const TilePosition& tilePosition, const std::vector<std::byte>& input);
+
+ std::optional<TileData> getTileData(const std::string& worldspace,
+ const TilePosition& tilePosition, const std::vector<std::byte>& input);
+
+ int insertTile(TileId tileId, const std::string& worldspace, const TilePosition& tilePosition,
+ TileVersion version, const std::vector<std::byte>& input, const std::vector<std::byte>& data);
+
+ int updateTile(TileId tileId, TileVersion version, const std::vector<std::byte>& data);
+
+ ShapeId getMaxShapeId();
+
+ std::optional<ShapeId> findShapeId(const std::string& name, ShapeType type, const Sqlite3::ConstBlob& hash);
+
+ int insertShape(ShapeId shapeId, const std::string& name, ShapeType type, const Sqlite3::ConstBlob& hash);
+
+ private:
+ Sqlite3::Db mDb;
+ Sqlite3::Statement<DbQueries::GetMaxTileId> mGetMaxTileId;
+ Sqlite3::Statement<DbQueries::FindTile> mFindTile;
+ Sqlite3::Statement<DbQueries::GetTileData> mGetTileData;
+ Sqlite3::Statement<DbQueries::InsertTile> mInsertTile;
+ Sqlite3::Statement<DbQueries::UpdateTile> mUpdateTile;
+ Sqlite3::Statement<DbQueries::GetMaxShapeId> mGetMaxShapeId;
+ Sqlite3::Statement<DbQueries::FindShapeId> mFindShapeId;
+ Sqlite3::Statement<DbQueries::InsertShape> mInsertShape;
+ };
+}
+
+#endif
diff --git a/components/detournavigator/navmeshdbutils.cpp b/components/detournavigator/navmeshdbutils.cpp
new file mode 100644
index 0000000000..86f81bfc51
--- /dev/null
+++ b/components/detournavigator/navmeshdbutils.cpp
@@ -0,0 +1,63 @@
+#include "navmeshdbutils.hpp"
+#include "navmeshdb.hpp"
+#include "recastmesh.hpp"
+
+#include <components/debug/debuglog.hpp>
+
+#include <cassert>
+#include <optional>
+
+namespace DetourNavigator
+{
+ namespace
+ {
+ std::optional<ShapeId> findShapeId(NavMeshDb& db, const std::string& name, ShapeType type,
+ const std::string& hash)
+ {
+ const Sqlite3::ConstBlob hashData {hash.data(), static_cast<int>(hash.size())};
+ return db.findShapeId(name, type, hashData);
+ }
+
+ ShapeId getShapeId(NavMeshDb& db, const std::string& name, ShapeType type,
+ const std::string& hash, ShapeId& nextShapeId)
+ {
+ const Sqlite3::ConstBlob hashData {hash.data(), static_cast<int>(hash.size())};
+ if (const auto existingShapeId = db.findShapeId(name, type, hashData))
+ return *existingShapeId;
+ const ShapeId newShapeId = nextShapeId;
+ db.insertShape(newShapeId, name, type, hashData);
+ Log(Debug::Verbose) << "Added " << name << " " << type << " shape to navmeshdb with id " << newShapeId;
+ ++nextShapeId.t;
+ return newShapeId;
+ }
+ }
+
+ ShapeId resolveMeshSource(NavMeshDb& db, const MeshSource& source, ShapeId& nextShapeId)
+ {
+ switch (source.mAreaType)
+ {
+ case AreaType_null:
+ return getShapeId(db, source.mShape->mFileName, ShapeType::Avoid, source.mShape->mFileHash, nextShapeId);
+ case AreaType_ground:
+ return getShapeId(db, source.mShape->mFileName, ShapeType::Collision, source.mShape->mFileHash, nextShapeId);
+ default:
+ Log(Debug::Warning) << "Trying to resolve recast mesh source with unsupported area type: " << source.mAreaType;
+ assert(false);
+ return ShapeId(0);
+ }
+ }
+
+ std::optional<ShapeId> resolveMeshSource(NavMeshDb& db, const MeshSource& source)
+ {
+ switch (source.mAreaType)
+ {
+ case AreaType_null:
+ return findShapeId(db, source.mShape->mFileName, ShapeType::Avoid, source.mShape->mFileHash);
+ case AreaType_ground:
+ return findShapeId(db, source.mShape->mFileName, ShapeType::Collision, source.mShape->mFileHash);
+ default:
+ Log(Debug::Warning) << "Trying to resolve recast mesh source with unsupported area type: " << source.mAreaType;
+ return std::nullopt;
+ }
+ }
+}
diff --git a/components/detournavigator/navmeshdbutils.hpp b/components/detournavigator/navmeshdbutils.hpp
new file mode 100644
index 0000000000..aafde3307c
--- /dev/null
+++ b/components/detournavigator/navmeshdbutils.hpp
@@ -0,0 +1,17 @@
+#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDBUTILS_H
+#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDBUTILS_H
+
+#include "navmeshdb.hpp"
+
+#include <optional>
+
+namespace DetourNavigator
+{
+ struct MeshSource;
+
+ ShapeId resolveMeshSource(NavMeshDb& db, const MeshSource& source, ShapeId& nextShapeId);
+
+ std::optional<ShapeId> resolveMeshSource(NavMeshDb& db, const MeshSource& source);
+}
+
+#endif
diff --git a/components/detournavigator/navmeshmanager.cpp b/components/detournavigator/navmeshmanager.cpp
index 362c6938c0..399af8a6a9 100644
--- a/components/detournavigator/navmeshmanager.cpp
+++ b/components/detournavigator/navmeshmanager.cpp
@@ -8,6 +8,7 @@
#include "waitconditiontype.hpp"
#include <components/debug/debuglog.hpp>
+#include <components/bullethelpers/heightfield.hpp>
#include <DetourNavMesh.h>
@@ -40,13 +41,23 @@ namespace
namespace DetourNavigator
{
- NavMeshManager::NavMeshManager(const Settings& settings)
+ NavMeshManager::NavMeshManager(const Settings& settings, std::unique_ptr<NavMeshDb>&& db)
: mSettings(settings)
- , mRecastMeshManager(settings)
- , mOffMeshConnectionsManager(settings)
- , mAsyncNavMeshUpdater(settings, mRecastMeshManager, mOffMeshConnectionsManager)
+ , mRecastMeshManager(settings.mRecast)
+ , mOffMeshConnectionsManager(settings.mRecast)
+ , mAsyncNavMeshUpdater(settings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db))
{}
+ void NavMeshManager::setWorldspace(std::string_view worldspace)
+ {
+ if (worldspace == mWorldspace)
+ return;
+ mRecastMeshManager.setWorldspace(worldspace);
+ for (auto& [agent, cache] : mCache)
+ cache = std::make_shared<GuardedNavMeshCacheItem>(makeEmptyNavMesh(mSettings), ++mGenerationCounter);
+ mWorldspace = worldspace;
+ }
+
bool NavMeshManager::addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform,
const AreaType areaType)
{
@@ -73,10 +84,11 @@ namespace DetourNavigator
return true;
}
- bool NavMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift)
+ bool NavMeshManager::addWater(const osg::Vec2i& cellPosition, int cellSize, float level)
{
- if (!mRecastMeshManager.addWater(cellPosition, cellSize, shift))
+ if (!mRecastMeshManager.addWater(cellPosition, cellSize, level))
return false;
+ const btVector3 shift = Misc::Convert::toBullet(getWaterShift3d(cellPosition, cellSize, level));
addChangedTiles(cellSize, shift, ChangeType::add);
return true;
}
@@ -86,15 +98,16 @@ namespace DetourNavigator
const auto water = mRecastMeshManager.removeWater(cellPosition);
if (!water)
return false;
- addChangedTiles(water->mSize, water->mShift, ChangeType::remove);
+ const btVector3 shift = Misc::Convert::toBullet(getWaterShift3d(cellPosition, water->mCellSize, water->mLevel));
+ addChangedTiles(water->mCellSize, shift, ChangeType::remove);
return true;
}
- bool NavMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift,
- const HeightfieldShape& shape)
+ bool NavMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape)
{
- if (!mRecastMeshManager.addHeightfield(cellPosition, cellSize, shift, shape))
+ if (!mRecastMeshManager.addHeightfield(cellPosition, cellSize, shape))
return false;
+ const btVector3 shift = getHeightfieldShift(shape, cellPosition, cellSize);
addChangedTiles(cellSize, shift, ChangeType::add);
return true;
}
@@ -104,7 +117,8 @@ namespace DetourNavigator
const auto heightfield = mRecastMeshManager.removeHeightfield(cellPosition);
if (!heightfield)
return false;
- addChangedTiles(heightfield->mSize, heightfield->mShift, ChangeType::remove);
+ const btVector3 shift = getHeightfieldShift(heightfield->mShape, cellPosition, heightfield->mCellSize);
+ addChangedTiles(heightfield->mCellSize, shift, ChangeType::remove);
return true;
}
@@ -136,8 +150,8 @@ namespace DetourNavigator
{
mOffMeshConnectionsManager.add(id, OffMeshConnection {start, end, areaType});
- const auto startTilePosition = getTilePosition(mSettings, start);
- const auto endTilePosition = getTilePosition(mSettings, end);
+ const auto startTilePosition = getTilePosition(mSettings.mRecast, start);
+ const auto endTilePosition = getTilePosition(mSettings.mRecast, end);
addChangedTile(startTilePosition, ChangeType::add);
@@ -154,7 +168,7 @@ namespace DetourNavigator
void NavMeshManager::update(const osg::Vec3f& playerPosition, const osg::Vec3f& agentHalfExtents)
{
- const auto playerTile = getTilePosition(mSettings, toNavMeshCoordinates(mSettings, playerPosition));
+ const auto playerTile = getTilePosition(mSettings.mRecast, toNavMeshCoordinates(mSettings.mRecast, playerPosition));
auto& lastRevision = mLastRecastMeshManagerRevision[agentHalfExtents];
auto lastPlayerTile = mPlayerTile.find(agentHalfExtents);
if (lastRevision == mRecastMeshManager.getRevision() && lastPlayerTile != mPlayerTile.end()
@@ -197,14 +211,14 @@ namespace DetourNavigator
const auto shouldAdd = shouldAddTile(tile, playerTile, maxTiles);
const auto presentInNavMesh = bool(navMesh.getTileAt(tile.x(), tile.y(), 0));
if (shouldAdd && !presentInNavMesh)
- tilesToPost.insert(std::make_pair(tile, ChangeType::add));
+ tilesToPost.insert(std::make_pair(tile, locked->isEmptyTile(tile) ? ChangeType::update : ChangeType::add));
else if (!shouldAdd && presentInNavMesh)
tilesToPost.insert(std::make_pair(tile, ChangeType::mixed));
else
recastMeshManager.reportNavMeshChange(recastMeshManager.getVersion(), Version {0, 0});
});
}
- mAsyncNavMeshUpdater.post(agentHalfExtents, cached, playerTile, tilesToPost);
+ mAsyncNavMeshUpdater.post(agentHalfExtents, cached, playerTile, mRecastMeshManager.getWorldspace(), tilesToPost);
if (changedTiles != mChangedTiles.end())
changedTiles->second.clear();
Log(Debug::Debug) << "Cache update posted for agent=" << agentHalfExtents <<
@@ -229,7 +243,7 @@ namespace DetourNavigator
void NavMeshManager::reportStats(unsigned int frameNumber, osg::Stats& stats) const
{
- mAsyncNavMeshUpdater.reportStats(frameNumber, stats);
+ DetourNavigator::reportStats(mAsyncNavMeshUpdater.getStats(), frameNumber, stats);
}
RecastMeshTiles NavMeshManager::getRecastMeshTiles() const
@@ -237,9 +251,10 @@ namespace DetourNavigator
std::vector<TilePosition> tiles;
mRecastMeshManager.forEachTile(
[&tiles] (const TilePosition& tile, const CachedRecastMeshManager&) { tiles.push_back(tile); });
+ const std::string worldspace = mRecastMeshManager.getWorldspace();
RecastMeshTiles result;
for (const TilePosition& tile : tiles)
- if (auto mesh = mRecastMeshManager.getCachedMesh(tile))
+ if (auto mesh = mRecastMeshManager.getCachedMesh(worldspace, tile))
result.emplace(tile, std::move(mesh));
return result;
}
@@ -247,17 +262,17 @@ namespace DetourNavigator
void NavMeshManager::addChangedTiles(const btCollisionShape& shape, const btTransform& transform,
const ChangeType changeType)
{
- getTilesPositions(shape, transform, mSettings,
+ getTilesPositions(shape, transform, mSettings.mRecast,
[&] (const TilePosition& v) { addChangedTile(v, changeType); });
}
- void NavMeshManager::addChangedTiles(const int cellSize, const osg::Vec3f& shift,
+ void NavMeshManager::addChangedTiles(const int cellSize, const btVector3& shift,
const ChangeType changeType)
{
if (cellSize == std::numeric_limits<int>::max())
return;
- getTilesPositions(cellSize, shift, mSettings,
+ getTilesPositions(cellSize, shift, mSettings.mRecast,
[&] (const TilePosition& v) { addChangedTile(v, changeType); });
}
diff --git a/components/detournavigator/navmeshmanager.hpp b/components/detournavigator/navmeshmanager.hpp
index b1496dc817..3fd2d28d74 100644
--- a/components/detournavigator/navmeshmanager.hpp
+++ b/components/detournavigator/navmeshmanager.hpp
@@ -22,7 +22,9 @@ namespace DetourNavigator
class NavMeshManager
{
public:
- NavMeshManager(const Settings& settings);
+ explicit NavMeshManager(const Settings& settings, std::unique_ptr<NavMeshDb>&& db);
+
+ void setWorldspace(std::string_view worldspace);
bool addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform,
const AreaType areaType);
@@ -34,12 +36,11 @@ namespace DetourNavigator
void addAgent(const osg::Vec3f& agentHalfExtents);
- bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift);
+ bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level);
bool removeWater(const osg::Vec2i& cellPosition);
- bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift,
- const HeightfieldShape& shape);
+ bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape);
bool removeHeightfield(const osg::Vec2i& cellPosition);
@@ -63,6 +64,7 @@ namespace DetourNavigator
private:
const Settings& mSettings;
+ std::string mWorldspace;
TileCachedRecastMeshManager mRecastMeshManager;
OffMeshConnectionsManager mOffMeshConnectionsManager;
AsyncNavMeshUpdater mAsyncNavMeshUpdater;
@@ -74,7 +76,7 @@ namespace DetourNavigator
void addChangedTiles(const btCollisionShape& shape, const btTransform& transform, const ChangeType changeType);
- void addChangedTiles(const int cellSize, const osg::Vec3f& shift, const ChangeType changeType);
+ void addChangedTiles(const int cellSize, const btVector3& shift, const ChangeType changeType);
void addChangedTile(const TilePosition& tilePosition, const ChangeType changeType);
diff --git a/components/detournavigator/navmeshtilescache.cpp b/components/detournavigator/navmeshtilescache.cpp
index 3d595f13a8..bbda8a3179 100644
--- a/components/detournavigator/navmeshtilescache.cpp
+++ b/components/detournavigator/navmeshtilescache.cpp
@@ -79,12 +79,11 @@ namespace DetourNavigator
return result;
}
- void NavMeshTilesCache::reportStats(unsigned int frameNumber, osg::Stats& out) const
+ void reportStats(const NavMeshTilesCache::Stats& stats, unsigned int frameNumber, osg::Stats& out)
{
- const Stats stats = getStats();
- out.setAttribute(frameNumber, "NavMesh CacheSize", stats.mNavMeshCacheSize);
- out.setAttribute(frameNumber, "NavMesh UsedTiles", stats.mUsedNavMeshTiles);
- out.setAttribute(frameNumber, "NavMesh CachedTiles", stats.mCachedNavMeshTiles);
+ out.setAttribute(frameNumber, "NavMesh CacheSize", static_cast<double>(stats.mNavMeshCacheSize));
+ out.setAttribute(frameNumber, "NavMesh UsedTiles", static_cast<double>(stats.mUsedNavMeshTiles));
+ out.setAttribute(frameNumber, "NavMesh CachedTiles", static_cast<double>(stats.mCachedNavMeshTiles));
if (stats.mGetCount > 0)
out.setAttribute(frameNumber, "NavMesh CacheHitRate", static_cast<double>(stats.mHitCount) / stats.mGetCount * 100.0);
}
diff --git a/components/detournavigator/navmeshtilescache.hpp b/components/detournavigator/navmeshtilescache.hpp
index 06d6c69e9b..e7e0b6c7a8 100644
--- a/components/detournavigator/navmeshtilescache.hpp
+++ b/components/detournavigator/navmeshtilescache.hpp
@@ -23,7 +23,7 @@ namespace DetourNavigator
struct RecastMeshData
{
Mesh mMesh;
- std::vector<Cell> mWater;
+ std::vector<CellWater> mWater;
std::vector<Heightfield> mHeightfields;
std::vector<FlatHeightfield> mFlatHeightfields;
};
@@ -144,8 +144,6 @@ namespace DetourNavigator
Stats getStats() const;
- void reportStats(unsigned int frameNumber, osg::Stats& stats) const;
-
private:
mutable std::mutex mMutex;
std::size_t mMaxNavMeshDataSize;
@@ -163,6 +161,8 @@ namespace DetourNavigator
void releaseItem(ItemIterator iterator);
};
+
+ void reportStats(const NavMeshTilesCache::Stats& stats, unsigned int frameNumber, osg::Stats& out);
}
#endif
diff --git a/components/detournavigator/navmeshtileview.cpp b/components/detournavigator/navmeshtileview.cpp
index 336cd1ba84..d12bcecd7e 100644
--- a/components/detournavigator/navmeshtileview.cpp
+++ b/components/detournavigator/navmeshtileview.cpp
@@ -9,14 +9,11 @@
#include <stdexcept>
#include <tuple>
-namespace
+inline bool operator==(const dtMeshHeader& lhs, const dtMeshHeader& rhs) noexcept
{
- using DetourNavigator::ArrayRef;
- using DetourNavigator::Ref;
- using DetourNavigator::Span;
-
- auto makeTuple(const dtMeshHeader& v)
+ const auto makeTuple = [] (const dtMeshHeader& v)
{
+ using DetourNavigator::ArrayRef;
return std::tuple(
v.x,
v.y,
@@ -39,47 +36,46 @@ namespace
ArrayRef(v.bmax),
v.bvQuantFactor
);
- }
+ };
+ return makeTuple(lhs) == makeTuple(rhs);
+}
- auto makeTuple(const dtPoly& v)
+inline bool operator==(const dtPoly& lhs, const dtPoly& rhs) noexcept
+{
+ const auto makeTuple = [] (const dtPoly& v)
{
+ using DetourNavigator::ArrayRef;
return std::tuple(ArrayRef(v.verts), ArrayRef(v.neis), v.flags, v.vertCount, v.areaAndtype);
- }
+ };
+ return makeTuple(lhs) == makeTuple(rhs);
+}
- auto makeTuple(const dtPolyDetail& v)
+inline bool operator==(const dtPolyDetail& lhs, const dtPolyDetail& rhs) noexcept
+{
+ const auto makeTuple = [] (const dtPolyDetail& v)
{
return std::tuple(v.vertBase, v.triBase, v.vertCount, v.triCount);
- }
+ };
+ return makeTuple(lhs) == makeTuple(rhs);
+}
- auto makeTuple(const dtBVNode& v)
+inline bool operator==(const dtBVNode& lhs, const dtBVNode& rhs) noexcept
+{
+ const auto makeTuple = [] (const dtBVNode& v)
{
+ using DetourNavigator::ArrayRef;
return std::tuple(ArrayRef(v.bmin), ArrayRef(v.bmax), v.i);
- }
-
- auto makeTuple(const dtOffMeshConnection& v)
- {
- return std::tuple(ArrayRef(v.pos), v.rad, v.poly, v.flags, v.side, v.userId);
- }
-
- auto makeTuple(const DetourNavigator::NavMeshTileConstView& v)
- {
- return std::tuple(
- Ref(*v.mHeader),
- Span(v.mPolys, v.mHeader->polyCount),
- Span(v.mVerts, v.mHeader->vertCount),
- Span(v.mDetailMeshes, v.mHeader->detailMeshCount),
- Span(v.mDetailVerts, v.mHeader->detailVertCount),
- Span(v.mDetailTris, v.mHeader->detailTriCount),
- Span(v.mBvTree, v.mHeader->bvNodeCount),
- Span(v.mOffMeshCons, v.mHeader->offMeshConCount)
- );
- }
+ };
+ return makeTuple(lhs) == makeTuple(rhs);
}
-template <class T>
-inline auto operator==(const T& lhs, const T& rhs)
- -> std::enable_if_t<std::is_same_v<std::void_t<decltype(makeTuple(lhs))>, void>, bool>
+inline bool operator==(const dtOffMeshConnection& lhs, const dtOffMeshConnection& rhs) noexcept
{
+ const auto makeTuple = [] (const dtOffMeshConnection& v)
+ {
+ using DetourNavigator::ArrayRef;
+ return std::tuple(ArrayRef(v.pos), v.rad, v.poly, v.flags, v.side, v.userId);
+ };
return makeTuple(lhs) == makeTuple(rhs);
}
@@ -139,8 +135,23 @@ namespace DetourNavigator
return view;
}
- bool operator==(const NavMeshTileConstView& lhs, const NavMeshTileConstView& rhs)
+ bool operator==(const NavMeshTileConstView& lhs, const NavMeshTileConstView& rhs) noexcept
{
+ using DetourNavigator::Ref;
+ using DetourNavigator::Span;
+ const auto makeTuple = [] (const DetourNavigator::NavMeshTileConstView& v)
+ {
+ return std::tuple(
+ Ref(*v.mHeader),
+ Span(v.mPolys, v.mHeader->polyCount),
+ Span(v.mVerts, v.mHeader->vertCount),
+ Span(v.mDetailMeshes, v.mHeader->detailMeshCount),
+ Span(v.mDetailVerts, v.mHeader->detailVertCount),
+ Span(v.mDetailTris, v.mHeader->detailTriCount),
+ Span(v.mBvTree, v.mHeader->bvNodeCount),
+ Span(v.mOffMeshCons, v.mHeader->offMeshConCount)
+ );
+ };
return makeTuple(lhs) == makeTuple(rhs);
}
}
diff --git a/components/detournavigator/navmeshtileview.hpp b/components/detournavigator/navmeshtileview.hpp
index 92017360c3..b797545b8a 100644
--- a/components/detournavigator/navmeshtileview.hpp
+++ b/components/detournavigator/navmeshtileview.hpp
@@ -21,7 +21,7 @@ namespace DetourNavigator
const dtBVNode* mBvTree;
const dtOffMeshConnection* mOffMeshCons;
- friend bool operator==(const NavMeshTileConstView& lhs, const NavMeshTileConstView& rhs);
+ friend bool operator==(const NavMeshTileConstView& lhs, const NavMeshTileConstView& rhs) noexcept;
};
NavMeshTileConstView asNavMeshTileConstView(const unsigned char* data);
diff --git a/components/detournavigator/objectid.hpp b/components/detournavigator/objectid.hpp
index 9c4b5b2710..22fc792c6f 100644
--- a/components/detournavigator/objectid.hpp
+++ b/components/detournavigator/objectid.hpp
@@ -15,6 +15,11 @@ namespace DetourNavigator
{
}
+ explicit ObjectId(std::size_t value) noexcept
+ : mValue(value)
+ {
+ }
+
std::size_t value() const noexcept
{
return mValue;
diff --git a/components/detournavigator/objecttransform.hpp b/components/detournavigator/objecttransform.hpp
new file mode 100644
index 0000000000..2da9a25348
--- /dev/null
+++ b/components/detournavigator/objecttransform.hpp
@@ -0,0 +1,27 @@
+#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_OBJECTTRANSFORM_H
+#define OPENMW_COMPONENTS_DETOURNAVIGATOR_OBJECTTRANSFORM_H
+
+#include <components/esm/defs.hpp>
+
+#include <tuple>
+
+namespace DetourNavigator
+{
+ struct ObjectTransform
+ {
+ ESM::Position mPosition;
+ float mScale;
+
+ friend inline auto tie(const ObjectTransform& v)
+ {
+ return std::tie(v.mPosition, v.mScale);
+ }
+
+ friend inline bool operator<(const ObjectTransform& l, const ObjectTransform& r)
+ {
+ return tie(l) < tie(r);
+ }
+ };
+}
+
+#endif
diff --git a/components/detournavigator/offmeshconnectionsmanager.cpp b/components/detournavigator/offmeshconnectionsmanager.cpp
index a673ae3e68..a11da21218 100644
--- a/components/detournavigator/offmeshconnectionsmanager.cpp
+++ b/components/detournavigator/offmeshconnectionsmanager.cpp
@@ -11,7 +11,7 @@
namespace DetourNavigator
{
- OffMeshConnectionsManager::OffMeshConnectionsManager(const Settings& settings)
+ OffMeshConnectionsManager::OffMeshConnectionsManager(const RecastSettings& settings)
: mSettings(settings)
{}
@@ -65,11 +65,11 @@ namespace DetourNavigator
return removed;
}
- std::vector<OffMeshConnection> OffMeshConnectionsManager::get(const TilePosition& tilePosition)
+ std::vector<OffMeshConnection> OffMeshConnectionsManager::get(const TilePosition& tilePosition) const
{
std::vector<OffMeshConnection> result;
- const auto values = mValues.lock();
+ const auto values = mValues.lockConst();
const auto itByTilePosition = values->mByTilePosition.find(tilePosition);
diff --git a/components/detournavigator/offmeshconnectionsmanager.hpp b/components/detournavigator/offmeshconnectionsmanager.hpp
index 20a6427cd5..455b03276a 100644
--- a/components/detournavigator/offmeshconnectionsmanager.hpp
+++ b/components/detournavigator/offmeshconnectionsmanager.hpp
@@ -18,13 +18,13 @@ namespace DetourNavigator
class OffMeshConnectionsManager
{
public:
- OffMeshConnectionsManager(const Settings& settings);
+ explicit OffMeshConnectionsManager(const RecastSettings& settings);
void add(const ObjectId id, const OffMeshConnection& value);
std::set<TilePosition> remove(const ObjectId id);
- std::vector<OffMeshConnection> get(const TilePosition& tilePosition);
+ std::vector<OffMeshConnection> get(const TilePosition& tilePosition) const;
private:
struct Values
@@ -33,7 +33,7 @@ namespace DetourNavigator
std::map<TilePosition, std::unordered_set<ObjectId>> mByTilePosition;
};
- const Settings& mSettings;
+ const RecastSettings& mSettings;
Misc::ScopeGuarded<Values> mValues;
};
}
diff --git a/components/detournavigator/preparednavmeshdata.cpp b/components/detournavigator/preparednavmeshdata.cpp
index 3fea46b26c..a737ae19a5 100644
--- a/components/detournavigator/preparednavmeshdata.cpp
+++ b/components/detournavigator/preparednavmeshdata.cpp
@@ -1,13 +1,13 @@
#include "preparednavmeshdata.hpp"
#include "preparednavmeshdatatuple.hpp"
+#include "recast.hpp"
#include <Recast.h>
-#include <RecastAlloc.h>
+
+#include <cstring>
namespace
{
- using namespace DetourNavigator;
-
void initPolyMeshDetail(rcPolyMeshDetail& value) noexcept
{
value.meshes = nullptr;
@@ -17,20 +17,6 @@ namespace
value.nverts = 0;
value.ntris = 0;
}
-
- void freePolyMeshDetail(rcPolyMeshDetail& value) noexcept
- {
- rcFree(value.meshes);
- rcFree(value.verts);
- rcFree(value.tris);
- }
-}
-
-template <class T>
-inline constexpr auto operator==(const T& lhs, const T& rhs) noexcept
- -> std::enable_if_t<std::is_same_v<std::void_t<decltype(makeTuple(lhs))>, void>, bool>
-{
- return makeTuple(lhs) == makeTuple(rhs);
}
namespace DetourNavigator
@@ -40,6 +26,15 @@ namespace DetourNavigator
initPolyMeshDetail(mPolyMeshDetail);
}
+ PreparedNavMeshData::PreparedNavMeshData(const PreparedNavMeshData& other)
+ : mUserId(other.mUserId)
+ , mCellSize(other.mCellSize)
+ , mCellHeight(other.mCellHeight)
+ {
+ copyPolyMesh(other.mPolyMesh, mPolyMesh);
+ copyPolyMeshDetail(other.mPolyMeshDetail, mPolyMeshDetail);
+ }
+
PreparedNavMeshData::~PreparedNavMeshData() noexcept
{
freePolyMeshDetail(mPolyMeshDetail);
diff --git a/components/detournavigator/preparednavmeshdata.hpp b/components/detournavigator/preparednavmeshdata.hpp
index 3566cfc71b..b3de7a447f 100644
--- a/components/detournavigator/preparednavmeshdata.hpp
+++ b/components/detournavigator/preparednavmeshdata.hpp
@@ -18,7 +18,7 @@ namespace DetourNavigator
rcPolyMeshDetail mPolyMeshDetail;
PreparedNavMeshData() noexcept;
- PreparedNavMeshData(const PreparedNavMeshData&) = delete;
+ PreparedNavMeshData(const PreparedNavMeshData& other);
~PreparedNavMeshData() noexcept;
diff --git a/components/detournavigator/preparednavmeshdatatuple.hpp b/components/detournavigator/preparednavmeshdatatuple.hpp
index bcca0ace37..03b192ad38 100644
--- a/components/detournavigator/preparednavmeshdatatuple.hpp
+++ b/components/detournavigator/preparednavmeshdatatuple.hpp
@@ -9,10 +9,11 @@
#include <tuple>
-namespace DetourNavigator
+inline bool operator==(const rcPolyMesh& lhs, const rcPolyMesh& rhs) noexcept
{
- constexpr auto makeTuple(const rcPolyMesh& v) noexcept
+ const auto makeTuple = [] (const rcPolyMesh& v)
{
+ using namespace DetourNavigator;
return std::tuple(
Span(v.verts, static_cast<int>(getVertsLength(v))),
Span(v.polys, static_cast<int>(getPolysLength(v))),
@@ -26,18 +27,27 @@ namespace DetourNavigator
v.borderSize,
v.maxEdgeError
);
- }
+ };
+ return makeTuple(lhs) == makeTuple(rhs);
+}
- constexpr auto makeTuple(const rcPolyMeshDetail& v) noexcept
+inline bool operator==(const rcPolyMeshDetail& lhs, const rcPolyMeshDetail& rhs) noexcept
+{
+ const auto makeTuple = [] (const rcPolyMeshDetail& v)
{
+ using namespace DetourNavigator;
return std::tuple(
Span(v.meshes, static_cast<int>(getMeshesLength(v))),
Span(v.verts, static_cast<int>(getVertsLength(v))),
Span(v.tris, static_cast<int>(getTrisLength(v)))
);
- }
+ };
+ return makeTuple(lhs) == makeTuple(rhs);
+}
- constexpr auto makeTuple(const PreparedNavMeshData& v) noexcept
+namespace DetourNavigator
+{
+ inline auto makeTuple(const PreparedNavMeshData& v) noexcept
{
return std::tuple(
v.mUserId,
diff --git a/components/detournavigator/raycast.cpp b/components/detournavigator/raycast.cpp
index 271da22496..be3217ba40 100644
--- a/components/detournavigator/raycast.cpp
+++ b/components/detournavigator/raycast.cpp
@@ -10,7 +10,7 @@
namespace DetourNavigator
{
std::optional<osg::Vec3f> raycast(const dtNavMesh& navMesh, const osg::Vec3f& halfExtents,
- const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const Settings& settings)
+ const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const DetourSettings& settings)
{
dtNavMeshQuery navMeshQuery;
if (!initNavMeshQuery(navMeshQuery, navMesh, settings.mMaxNavMeshQueryNodes))
diff --git a/components/detournavigator/raycast.hpp b/components/detournavigator/raycast.hpp
index ddf61b49f4..60cdf0a157 100644
--- a/components/detournavigator/raycast.hpp
+++ b/components/detournavigator/raycast.hpp
@@ -10,10 +10,10 @@ class dtNavMesh;
namespace DetourNavigator
{
- struct Settings;
+ struct DetourSettings;
std::optional<osg::Vec3f> raycast(const dtNavMesh& navMesh, const osg::Vec3f& halfExtents,
- const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const Settings& settings);
+ const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const DetourSettings& settings);
}
#endif
diff --git a/components/detournavigator/recast.cpp b/components/detournavigator/recast.cpp
new file mode 100644
index 0000000000..c1d14c0aa8
--- /dev/null
+++ b/components/detournavigator/recast.cpp
@@ -0,0 +1,80 @@
+#include "recast.hpp"
+
+#include <Recast.h>
+#include <RecastAlloc.h>
+
+#include <cstring>
+#include <new>
+
+namespace DetourNavigator
+{
+ void* permRecastAlloc(std::size_t size)
+ {
+ void* const result = rcAlloc(size, RC_ALLOC_PERM);
+ if (result == nullptr)
+ throw std::bad_alloc();
+ return result;
+ }
+
+ void permRecastAlloc(rcPolyMesh& value)
+ {
+ permRecastAlloc(value.verts, getVertsLength(value));
+ permRecastAlloc(value.polys, getPolysLength(value));
+ permRecastAlloc(value.regs, getRegsLength(value));
+ permRecastAlloc(value.flags, getFlagsLength(value));
+ permRecastAlloc(value.areas, getAreasLength(value));
+ }
+
+ void permRecastAlloc(rcPolyMeshDetail& value)
+ {
+ try
+ {
+ permRecastAlloc(value.meshes, getMeshesLength(value));
+ permRecastAlloc(value.verts, getVertsLength(value));
+ permRecastAlloc(value.tris, getTrisLength(value));
+ }
+ catch (...)
+ {
+ freePolyMeshDetail(value);
+ throw;
+ }
+ }
+
+ void freePolyMeshDetail(rcPolyMeshDetail& value) noexcept
+ {
+ rcFree(value.meshes);
+ rcFree(value.verts);
+ rcFree(value.tris);
+ }
+
+ void copyPolyMesh(const rcPolyMesh& src, rcPolyMesh& dst)
+ {
+ dst.nverts = src.nverts;
+ dst.npolys = src.npolys;
+ dst.maxpolys = src.maxpolys;
+ dst.nvp = src.nvp;
+ rcVcopy(dst.bmin, src.bmin);
+ rcVcopy(dst.bmax, src.bmax);
+ dst.cs = src.cs;
+ dst.ch = src.ch;
+ dst.borderSize = src.borderSize;
+ dst.maxEdgeError = src.maxEdgeError;
+ permRecastAlloc(dst);
+ std::memcpy(dst.verts, src.verts, getVertsLength(src) * sizeof(*dst.verts));
+ std::memcpy(dst.polys, src.polys, getPolysLength(src) * sizeof(*dst.polys));
+ std::memcpy(dst.regs, src.regs, getRegsLength(src) * sizeof(*dst.regs));
+ std::memcpy(dst.flags, src.flags, getFlagsLength(src) * sizeof(*dst.flags));
+ std::memcpy(dst.areas, src.areas, getAreasLength(src) * sizeof(*dst.areas));
+ }
+
+ void copyPolyMeshDetail(const rcPolyMeshDetail& src, rcPolyMeshDetail& dst)
+ {
+ dst.nmeshes = src.nmeshes;
+ dst.nverts = src.nverts;
+ dst.ntris = src.ntris;
+ permRecastAlloc(dst);
+ std::memcpy(dst.meshes, src.meshes, getMeshesLength(src) * sizeof(*dst.meshes));
+ std::memcpy(dst.verts, src.verts, getVertsLength(src) * sizeof(*dst.verts));
+ std::memcpy(dst.tris, src.tris, getTrisLength(src) * sizeof(*dst.tris));
+ }
+}
diff --git a/components/detournavigator/recast.hpp b/components/detournavigator/recast.hpp
index 4e9ab329b7..1811d35772 100644
--- a/components/detournavigator/recast.hpp
+++ b/components/detournavigator/recast.hpp
@@ -2,8 +2,10 @@
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECAST_H
#include <Recast.h>
+#include <RecastAlloc.h>
#include <cstddef>
+#include <type_traits>
namespace DetourNavigator
{
@@ -46,6 +48,25 @@ namespace DetourNavigator
{
return 4 * static_cast<std::size_t>(value.ntris);
}
+
+ void* permRecastAlloc(std::size_t size);
+
+ template <class T>
+ inline void permRecastAlloc(T*& values, std::size_t size)
+ {
+ static_assert(std::is_arithmetic_v<T>);
+ values = new (permRecastAlloc(size * sizeof(T))) T[size];
+ }
+
+ void permRecastAlloc(rcPolyMesh& value);
+
+ void permRecastAlloc(rcPolyMeshDetail& value);
+
+ void freePolyMeshDetail(rcPolyMeshDetail& value) noexcept;
+
+ void copyPolyMesh(const rcPolyMesh& src, rcPolyMesh& dst);
+
+ void copyPolyMeshDetail(const rcPolyMeshDetail& src, rcPolyMeshDetail& dst);
}
#endif
diff --git a/components/detournavigator/recastmesh.cpp b/components/detournavigator/recastmesh.cpp
index e2dea0ad6e..16220d74f1 100644
--- a/components/detournavigator/recastmesh.cpp
+++ b/components/detournavigator/recastmesh.cpp
@@ -18,40 +18,20 @@ namespace DetourNavigator
mAreaTypes = std::move(areaTypes);
}
- RecastMesh::RecastMesh(std::size_t generation, std::size_t revision, Mesh mesh, std::vector<Cell> water,
- std::vector<Heightfield> heightfields, std::vector<FlatHeightfield> flatHeightfields)
+ RecastMesh::RecastMesh(std::size_t generation, std::size_t revision, Mesh mesh, std::vector<CellWater> water,
+ std::vector<Heightfield> heightfields, std::vector<FlatHeightfield> flatHeightfields,
+ std::vector<MeshSource> meshSources)
: mGeneration(generation)
, mRevision(revision)
, mMesh(std::move(mesh))
, mWater(std::move(water))
, mHeightfields(std::move(heightfields))
, mFlatHeightfields(std::move(flatHeightfields))
+ , mMeshSources(std::move(meshSources))
{
- if (mMesh.getVerticesCount() > 0)
- rcCalcBounds(mMesh.getVertices().data(), static_cast<int>(mMesh.getVerticesCount()),
- mBounds.mMin.ptr(), mBounds.mMax.ptr());
mWater.shrink_to_fit();
mHeightfields.shrink_to_fit();
for (Heightfield& v : mHeightfields)
v.mHeights.shrink_to_fit();
- for (const Heightfield& v : mHeightfields)
- {
- const auto [min, max] = std::minmax_element(v.mHeights.begin(), v.mHeights.end());
- mBounds.mMin.x() = std::min(mBounds.mMin.x(), v.mBounds.mMin.x());
- mBounds.mMin.y() = std::min(mBounds.mMin.y(), v.mBounds.mMin.y());
- mBounds.mMin.z() = std::min(mBounds.mMin.z(), *min);
- mBounds.mMax.x() = std::max(mBounds.mMax.x(), v.mBounds.mMax.x());
- mBounds.mMax.y() = std::max(mBounds.mMax.y(), v.mBounds.mMax.y());
- mBounds.mMax.z() = std::max(mBounds.mMax.z(), *max);
- }
- for (const FlatHeightfield& v : mFlatHeightfields)
- {
- mBounds.mMin.x() = std::min(mBounds.mMin.x(), v.mBounds.mMin.x());
- mBounds.mMin.y() = std::min(mBounds.mMin.y(), v.mBounds.mMin.y());
- mBounds.mMin.z() = std::min(mBounds.mMin.z(), v.mHeight);
- mBounds.mMax.x() = std::max(mBounds.mMax.x(), v.mBounds.mMax.x());
- mBounds.mMax.y() = std::max(mBounds.mMax.y(), v.mBounds.mMax.y());
- mBounds.mMax.z() = std::max(mBounds.mMax.z(), v.mHeight);
- }
}
}
diff --git a/components/detournavigator/recastmesh.hpp b/components/detournavigator/recastmesh.hpp
index c8e160603b..df9d6414d5 100644
--- a/components/detournavigator/recastmesh.hpp
+++ b/components/detournavigator/recastmesh.hpp
@@ -4,10 +4,13 @@
#include "areatype.hpp"
#include "bounds.hpp"
#include "tilebounds.hpp"
+#include "objecttransform.hpp"
#include <components/bullethelpers/operators.hpp>
+#include <components/resource/bulletshape.hpp>
#include <osg/Vec3f>
+#include <osg/Vec2i>
#include <memory>
#include <string>
@@ -47,26 +50,57 @@ namespace DetourNavigator
}
};
- struct Cell
+ struct Water
{
- int mSize;
- osg::Vec3f mShift;
+ int mCellSize;
+ float mLevel;
};
+ inline bool operator<(const Water& lhs, const Water& rhs) noexcept
+ {
+ const auto tie = [] (const Water& v) { return std::tie(v.mCellSize, v.mLevel); };
+ return tie(lhs) < tie(rhs);
+ }
+
+ struct CellWater
+ {
+ osg::Vec2i mCellPosition;
+ Water mWater;
+ };
+
+ inline bool operator<(const CellWater& lhs, const CellWater& rhs) noexcept
+ {
+ const auto tie = [] (const CellWater& v) { return std::tie(v.mCellPosition, v.mWater); };
+ return tie(lhs) < tie(rhs);
+ }
+
+ inline osg::Vec2f getWaterShift2d(const osg::Vec2i& cellPosition, int cellSize)
+ {
+ return osg::Vec2f((cellPosition.x() + 0.5f) * cellSize, (cellPosition.y() + 0.5f) * cellSize);
+ }
+
+ inline osg::Vec3f getWaterShift3d(const osg::Vec2i& cellPosition, int cellSize, float level)
+ {
+ return osg::Vec3f(getWaterShift2d(cellPosition, cellSize), level);
+ }
+
struct Heightfield
{
- TileBounds mBounds;
+ osg::Vec2i mCellPosition;
+ int mCellSize;
std::uint8_t mLength;
float mMinHeight;
float mMaxHeight;
- osg::Vec3f mShift;
- float mScale;
std::vector<float> mHeights;
+ std::size_t mOriginalSize;
+ std::uint8_t mMinX;
+ std::uint8_t mMinY;
};
inline auto makeTuple(const Heightfield& v) noexcept
{
- return std::tie(v.mBounds, v.mLength, v.mMinHeight, v.mMaxHeight, v.mShift, v.mScale, v.mHeights);
+ return std::tie(v.mCellPosition, v.mCellSize, v.mLength, v.mMinHeight, v.mMaxHeight,
+ v.mHeights, v.mOriginalSize, v.mMinX, v.mMinY);
}
inline bool operator<(const Heightfield& lhs, const Heightfield& rhs) noexcept
@@ -76,20 +110,30 @@ namespace DetourNavigator
struct FlatHeightfield
{
- TileBounds mBounds;
+ osg::Vec2i mCellPosition;
+ int mCellSize;
float mHeight;
};
inline bool operator<(const FlatHeightfield& lhs, const FlatHeightfield& rhs) noexcept
{
- return std::tie(lhs.mBounds, lhs.mHeight) < std::tie(rhs.mBounds, rhs.mHeight);
+ const auto tie = [] (const FlatHeightfield& v) { return std::tie(v.mCellPosition, v.mCellSize, v.mHeight); };
+ return tie(lhs) < tie(rhs);
}
+ struct MeshSource
+ {
+ osg::ref_ptr<const Resource::BulletShape> mShape;
+ ObjectTransform mObjectTransform;
+ AreaType mAreaType;
+ };
+
class RecastMesh
{
public:
- RecastMesh(std::size_t generation, std::size_t revision, Mesh mesh, std::vector<Cell> water,
- std::vector<Heightfield> heightfields, std::vector<FlatHeightfield> flatHeightfields);
+ RecastMesh(std::size_t generation, std::size_t revision, Mesh mesh, std::vector<CellWater> water,
+ std::vector<Heightfield> heightfields, std::vector<FlatHeightfield> flatHeightfields,
+ std::vector<MeshSource> sources);
std::size_t getGeneration() const
{
@@ -103,7 +147,7 @@ namespace DetourNavigator
const Mesh& getMesh() const noexcept { return mMesh; }
- const std::vector<Cell>& getWater() const
+ const std::vector<CellWater>& getWater() const
{
return mWater;
}
@@ -118,39 +162,26 @@ namespace DetourNavigator
return mFlatHeightfields;
}
- const Bounds& getBounds() const
- {
- return mBounds;
- }
+ const std::vector<MeshSource>& getMeshSources() const noexcept { return mMeshSources; }
private:
std::size_t mGeneration;
std::size_t mRevision;
Mesh mMesh;
- std::vector<Cell> mWater;
+ std::vector<CellWater> mWater;
std::vector<Heightfield> mHeightfields;
std::vector<FlatHeightfield> mFlatHeightfields;
- Bounds mBounds;
-
- friend inline bool operator <(const RecastMesh& lhs, const RecastMesh& rhs) noexcept
- {
- return std::tie(lhs.mMesh, lhs.mWater) < std::tie(rhs.mMesh, rhs.mWater);
- }
+ std::vector<MeshSource> mMeshSources;
friend inline std::size_t getSize(const RecastMesh& value) noexcept
{
- return getSize(value.mMesh) + value.mWater.size() * sizeof(Cell)
+ return getSize(value.mMesh) + value.mWater.size() * sizeof(CellWater)
+ value.mHeightfields.size() * sizeof(Heightfield)
+ std::accumulate(value.mHeightfields.begin(), value.mHeightfields.end(), std::size_t {0},
[] (std::size_t r, const Heightfield& v) { return r + v.mHeights.size() * sizeof(float); })
+ value.mFlatHeightfields.size() * sizeof(FlatHeightfield);
}
};
-
- inline bool operator<(const Cell& lhs, const Cell& rhs) noexcept
- {
- return std::tie(lhs.mSize, lhs.mShift) < std::tie(rhs.mSize, rhs.mShift);
- }
}
#endif
diff --git a/components/detournavigator/recastmeshbuilder.cpp b/components/detournavigator/recastmeshbuilder.cpp
index 73b731c247..0f7552aa77 100644
--- a/components/detournavigator/recastmeshbuilder.cpp
+++ b/components/detournavigator/recastmeshbuilder.cpp
@@ -6,6 +6,7 @@
#include <components/bullethelpers/processtrianglecallback.hpp>
#include <components/misc/convert.hpp>
#include <components/debug/debuglog.hpp>
+#include <components/bullethelpers/heightfield.hpp>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
@@ -18,6 +19,7 @@
#include <cassert>
#include <array>
#include <vector>
+#include <sstream>
namespace DetourNavigator
{
@@ -30,17 +32,13 @@ namespace DetourNavigator
RecastMeshTriangle result;
result.mAreaType = areaType;
for (std::size_t i = 0; i < 3; ++i)
- result.mVertices[i] = Misc::Convert::makeOsgVec3f(vertices[i]);
+ result.mVertices[i] = Misc::Convert::toOsg(vertices[i]);
return result;
}
- TileBounds maxCellTileBounds(int size, const osg::Vec3f& shift)
+ float getHeightfieldScale(int cellSize, std::size_t dataSize)
{
- const float halfCellSize = static_cast<float>(size) / 2;
- return TileBounds {
- osg::Vec2f(shift.x() - halfCellSize, shift.y() - halfCellSize),
- osg::Vec2f(shift.x() + halfCellSize, shift.y() + halfCellSize)
- };
+ return static_cast<float>(cellSize) / (dataSize - 1);
}
}
@@ -107,7 +105,8 @@ namespace DetourNavigator
static_cast<int>(heightfield.mLength), heightfield.mHeights.data(),
heightfield.mMinHeight, heightfield.mMaxHeight, upAxis, flipQuadEdges);
#endif
- shape.setLocalScaling(btVector3(heightfield.mScale, heightfield.mScale, 1));
+ const float scale = getHeightfieldScale(heightfield.mCellSize, heightfield.mOriginalSize);
+ shape.setLocalScaling(btVector3(scale, scale, 1));
btVector3 aabbMin;
btVector3 aabbMax;
shape.getAabb(btTransform::getIdentity(), aabbMin, aabbMax);
@@ -117,8 +116,16 @@ namespace DetourNavigator
triangles.emplace_back(makeRecastMeshTriangle(vertices, AreaType_ground));
});
shape.processAllTriangles(&callback, aabbMin, aabbMax);
- const osg::Vec2f shift = (osg::Vec2f(aabbMax.x(), aabbMax.y()) - osg::Vec2f(aabbMin.x(), aabbMin.y())) * 0.5;
- return makeMesh(std::move(triangles), heightfield.mShift + osg::Vec3f(shift.x(), shift.y(), 0));
+ const osg::Vec2f aabbShift = (osg::Vec2f(aabbMax.x(), aabbMax.y()) - osg::Vec2f(aabbMin.x(), aabbMin.y())) * 0.5;
+ const osg::Vec2f tileShift = osg::Vec2f(heightfield.mMinX, heightfield.mMinY) * scale;
+ const osg::Vec2f localShift = aabbShift + tileShift;
+ const float cellSize = static_cast<float>(heightfield.mCellSize);
+ const osg::Vec3f cellShift(
+ heightfield.mCellPosition.x() * cellSize,
+ heightfield.mCellPosition.y() * cellSize,
+ (heightfield.mMinHeight + heightfield.mMaxHeight) * 0.5f
+ );
+ return makeMesh(std::move(triangles), cellShift + osg::Vec3f(localShift.x(), localShift.y(), 0));
}
RecastMeshBuilder::RecastMeshBuilder(const TileBounds& bounds) noexcept
@@ -127,6 +134,13 @@ namespace DetourNavigator
}
void RecastMeshBuilder::addObject(const btCollisionShape& shape, const btTransform& transform,
+ const AreaType areaType, osg::ref_ptr<const Resource::BulletShape> source, const ObjectTransform& objectTransform)
+ {
+ addObject(shape, transform, areaType);
+ mSources.push_back(MeshSource {std::move(source), objectTransform, areaType});
+ }
+
+ void RecastMeshBuilder::addObject(const btCollisionShape& shape, const btTransform& transform,
const AreaType areaType)
{
if (shape.isCompound())
@@ -199,24 +213,25 @@ namespace DetourNavigator
}
}
- void RecastMeshBuilder::addWater(const int cellSize, const osg::Vec3f& shift)
+ void RecastMeshBuilder::addWater(const osg::Vec2i& cellPosition, const Water& water)
{
- mWater.push_back(Cell {cellSize, shift});
+ mWater.push_back(CellWater {cellPosition, water});
}
- void RecastMeshBuilder::addHeightfield(int cellSize, const osg::Vec3f& shift, float height)
+ void RecastMeshBuilder::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, float height)
{
- if (const auto intersection = getIntersection(mBounds, maxCellTileBounds(cellSize, shift)))
- mFlatHeightfields.emplace_back(FlatHeightfield {*intersection, height + shift.z()});
+ if (const auto intersection = getIntersection(mBounds, maxCellTileBounds(cellPosition, cellSize)))
+ mFlatHeightfields.emplace_back(FlatHeightfield {cellPosition, cellSize, height});
}
- void RecastMeshBuilder::addHeightfield(int cellSize, const osg::Vec3f& shift, const float* heights,
+ void RecastMeshBuilder::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const float* heights,
std::size_t size, float minHeight, float maxHeight)
{
- const auto intersection = getIntersection(mBounds, maxCellTileBounds(cellSize, shift));
+ const auto intersection = getIntersection(mBounds, maxCellTileBounds(cellPosition, cellSize));
if (!intersection.has_value())
return;
- const float stepSize = static_cast<float>(cellSize) / (size - 1);
+ const osg::Vec3f shift = Misc::Convert::toOsg(BulletHelpers::getHeightfieldShift(cellPosition.x(), cellPosition.y(), cellSize, minHeight, maxHeight));
+ const float stepSize = getHeightfieldScale(cellSize, size);
const int halfCellSize = cellSize / 2;
const auto local = [&] (float v, float shift) { return (v - shift + halfCellSize) / stepSize; };
const auto index = [&] (float v, int add) { return std::clamp<int>(static_cast<int>(v) + add, 0, size); };
@@ -235,14 +250,16 @@ namespace DetourNavigator
for (std::size_t x = minX; x < endX; ++x)
tileHeights.push_back(heights[x + y * size]);
Heightfield heightfield;
- heightfield.mBounds = *intersection;
+ heightfield.mCellPosition = cellPosition;
+ heightfield.mCellSize = cellSize;
heightfield.mLength = static_cast<std::uint8_t>(endY - minY);
heightfield.mMinHeight = minHeight;
heightfield.mMaxHeight = maxHeight;
- heightfield.mShift = shift + osg::Vec3f(minX, minY, 0) * stepSize - osg::Vec3f(halfCellSize, halfCellSize, 0);
- heightfield.mScale = stepSize;
heightfield.mHeights = std::move(tileHeights);
- mHeightfields.emplace_back(heightfield);
+ heightfield.mOriginalSize = size;
+ heightfield.mMinX = static_cast<std::uint8_t>(minX);
+ heightfield.mMinY = static_cast<std::uint8_t>(minY);
+ mHeightfields.push_back(std::move(heightfield));
}
std::shared_ptr<RecastMesh> RecastMeshBuilder::create(std::size_t generation, std::size_t revision) &&
@@ -251,7 +268,8 @@ namespace DetourNavigator
std::sort(mWater.begin(), mWater.end());
Mesh mesh = makeMesh(std::move(mTriangles));
return std::make_shared<RecastMesh>(generation, revision, std::move(mesh), std::move(mWater),
- std::move(mHeightfields), std::move(mFlatHeightfields));
+ std::move(mHeightfields), std::move(mFlatHeightfields),
+ std::move(mSources));
}
void RecastMeshBuilder::addObject(const btConcaveShape& shape, const btTransform& transform,
diff --git a/components/detournavigator/recastmeshbuilder.hpp b/components/detournavigator/recastmeshbuilder.hpp
index 120e4b045a..d0848c2a45 100644
--- a/components/detournavigator/recastmeshbuilder.hpp
+++ b/components/detournavigator/recastmeshbuilder.hpp
@@ -4,12 +4,13 @@
#include "recastmesh.hpp"
#include "tilebounds.hpp"
+#include <components/resource/bulletshape.hpp>
+
#include <osg/Vec3f>
#include <LinearMath/btTransform.h>
#include <array>
-#include <functional>
#include <memory>
#include <tuple>
#include <vector>
@@ -39,7 +40,8 @@ namespace DetourNavigator
public:
explicit RecastMeshBuilder(const TileBounds& bounds) noexcept;
- void addObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType);
+ void addObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType,
+ osg::ref_ptr<const Resource::BulletShape> source, const ObjectTransform& objectTransform);
void addObject(const btCompoundShape& shape, const btTransform& transform, const AreaType areaType);
@@ -49,11 +51,11 @@ namespace DetourNavigator
void addObject(const btBoxShape& shape, const btTransform& transform, const AreaType areaType);
- void addWater(const int mCellSize, const osg::Vec3f& shift);
+ void addWater(const osg::Vec2i& cellPosition, const Water& water);
- void addHeightfield(int cellSize, const osg::Vec3f& shift, float height);
+ void addHeightfield(const osg::Vec2i& cellPosition, int cellSize, float height);
- void addHeightfield(int cellSize, const osg::Vec3f& shift, const float* heights, std::size_t size,
+ void addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const float* heights, std::size_t size,
float minHeight, float maxHeight);
std::shared_ptr<RecastMesh> create(std::size_t generation, std::size_t revision) &&;
@@ -61,9 +63,12 @@ namespace DetourNavigator
private:
const TileBounds mBounds;
std::vector<RecastMeshTriangle> mTriangles;
- std::vector<Cell> mWater;
+ std::vector<CellWater> mWater;
std::vector<Heightfield> mHeightfields;
std::vector<FlatHeightfield> mFlatHeightfields;
+ std::vector<MeshSource> mSources;
+
+ inline void addObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType);
void addObject(const btConcaveShape& shape, const btTransform& transform, btTriangleCallback&& callback);
diff --git a/components/detournavigator/recastmeshmanager.cpp b/components/detournavigator/recastmeshmanager.cpp
index 2eeb90a9b5..a7b24766fc 100644
--- a/components/detournavigator/recastmeshmanager.cpp
+++ b/components/detournavigator/recastmeshmanager.cpp
@@ -4,6 +4,7 @@
#include "heightfieldshape.hpp"
#include <components/debug/debuglog.hpp>
+#include <components/misc/convert.hpp>
#include <utility>
@@ -11,26 +12,26 @@ namespace
{
struct AddHeightfield
{
- const DetourNavigator::Cell& mCell;
+ osg::Vec2i mCellPosition;
+ int mCellSize;
DetourNavigator::RecastMeshBuilder& mBuilder;
void operator()(const DetourNavigator::HeightfieldSurface& v)
{
- mBuilder.addHeightfield(mCell.mSize, mCell.mShift, v.mHeights, v.mSize, v.mMinHeight, v.mMaxHeight);
+ mBuilder.addHeightfield(mCellPosition, mCellSize, v.mHeights, v.mSize, v.mMinHeight, v.mMaxHeight);
}
void operator()(DetourNavigator::HeightfieldPlane v)
{
- mBuilder.addHeightfield(mCell.mSize, mCell.mShift, v.mHeight);
+ mBuilder.addHeightfield(mCellPosition, mCellSize, v.mHeight);
}
};
}
namespace DetourNavigator
{
- RecastMeshManager::RecastMeshManager(const Settings& settings, const TileBounds& bounds, std::size_t generation)
- : mSettings(settings)
- , mGeneration(generation)
+ RecastMeshManager::RecastMeshManager(const TileBounds& bounds, std::size_t generation)
+ : mGeneration(generation)
, mTileBounds(bounds)
{
}
@@ -74,57 +75,55 @@ namespace DetourNavigator
return result;
}
- bool RecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift)
+ bool RecastMeshManager::addWater(const osg::Vec2i& cellPosition, int cellSize, float level)
{
const std::lock_guard lock(mMutex);
- if (!mWater.emplace(cellPosition, Cell {cellSize, shift}).second)
+ if (!mWater.emplace(cellPosition, Water {cellSize, level}).second)
return false;
++mRevision;
return true;
}
- std::optional<Cell> RecastMeshManager::removeWater(const osg::Vec2i& cellPosition)
+ std::optional<Water> RecastMeshManager::removeWater(const osg::Vec2i& cellPosition)
{
const std::lock_guard lock(mMutex);
const auto water = mWater.find(cellPosition);
if (water == mWater.end())
return std::nullopt;
++mRevision;
- const Cell result = water->second;
+ Water result = water->second;
mWater.erase(water);
return result;
}
- bool RecastMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift,
+ bool RecastMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize,
const HeightfieldShape& shape)
{
const std::lock_guard lock(mMutex);
- if (!mHeightfields.emplace(cellPosition, Heightfield {Cell {cellSize, shift}, shape}).second)
+ if (!mHeightfields.emplace(cellPosition, SizedHeightfieldShape {cellSize, shape}).second)
return false;
++mRevision;
return true;
}
- std::optional<Cell> RecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition)
+ std::optional<SizedHeightfieldShape> RecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition)
{
const std::lock_guard lock(mMutex);
const auto it = mHeightfields.find(cellPosition);
if (it == mHeightfields.end())
return std::nullopt;
++mRevision;
- const auto result = std::make_optional(it->second.mCell);
+ auto result = std::make_optional(it->second);
mHeightfields.erase(it);
return result;
}
std::shared_ptr<RecastMesh> RecastMeshManager::getMesh() const
{
- TileBounds tileBounds = mTileBounds;
- tileBounds.mMin /= mSettings.mRecastScaleFactor;
- tileBounds.mMax /= mSettings.mRecastScaleFactor;
- RecastMeshBuilder builder(tileBounds);
+ RecastMeshBuilder builder(mTileBounds);
using Object = std::tuple<
- osg::ref_ptr<const osg::Referenced>,
+ osg::ref_ptr<const Resource::BulletShapeInstance>,
+ ObjectTransform,
std::reference_wrapper<const btCollisionShape>,
btTransform,
AreaType
@@ -134,19 +133,20 @@ namespace DetourNavigator
{
const std::lock_guard lock(mMutex);
for (const auto& [k, v] : mWater)
- builder.addWater(v.mSize, v.mShift);
+ builder.addWater(k, v);
for (const auto& [cellPosition, v] : mHeightfields)
- std::visit(AddHeightfield {v.mCell, builder}, v.mShape);
+ std::visit(AddHeightfield {cellPosition, v.mCellSize, builder}, v.mShape);
objects.reserve(mObjects.size());
for (const auto& [k, object] : mObjects)
{
const RecastMeshObject& impl = object.getImpl();
- objects.emplace_back(impl.getHolder(), impl.getShape(), impl.getTransform(), impl.getAreaType());
+ objects.emplace_back(impl.getInstance(), impl.getObjectTransform(), impl.getShape(),
+ impl.getTransform(), impl.getAreaType());
}
revision = mRevision;
}
- for (const auto& [holder, shape, transform, areaType] : objects)
- builder.addObject(shape, transform, areaType);
+ for (const auto& [instance, objectTransform, shape, transform, areaType] : objects)
+ builder.addObject(shape, transform, areaType, instance->getSource(), objectTransform);
return std::move(builder).create(mGeneration, revision);
}
diff --git a/components/detournavigator/recastmeshmanager.hpp b/components/detournavigator/recastmeshmanager.hpp
index e1c1567cb3..e897c797fc 100644
--- a/components/detournavigator/recastmeshmanager.hpp
+++ b/components/detournavigator/recastmeshmanager.hpp
@@ -14,8 +14,6 @@
#include <map>
#include <optional>
#include <memory>
-#include <variant>
-#include <tuple>
#include <mutex>
class btCollisionShape;
@@ -31,10 +29,16 @@ namespace DetourNavigator
btTransform mTransform;
};
+ struct SizedHeightfieldShape
+ {
+ int mCellSize;
+ HeightfieldShape mShape;
+ };
+
class RecastMeshManager
{
public:
- RecastMeshManager(const Settings& settings, const TileBounds& bounds, std::size_t generation);
+ explicit RecastMeshManager(const TileBounds& bounds, std::size_t generation);
bool addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform,
const AreaType areaType);
@@ -43,14 +47,13 @@ namespace DetourNavigator
std::optional<RemovedRecastMeshObject> removeObject(const ObjectId id);
- bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift);
+ bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level);
- std::optional<Cell> removeWater(const osg::Vec2i& cellPosition);
+ std::optional<Water> removeWater(const osg::Vec2i& cellPosition);
- bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift,
- const HeightfieldShape& shape);
+ bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape);
- std::optional<Cell> removeHeightfield(const osg::Vec2i& cellPosition);
+ std::optional<SizedHeightfieldShape> removeHeightfield(const osg::Vec2i& cellPosition);
std::shared_ptr<RecastMesh> getMesh() const;
@@ -67,20 +70,13 @@ namespace DetourNavigator
Version mNavMeshVersion;
};
- struct Heightfield
- {
- Cell mCell;
- HeightfieldShape mShape;
- };
-
- const Settings& mSettings;
const std::size_t mGeneration;
const TileBounds mTileBounds;
mutable std::mutex mMutex;
std::size_t mRevision = 0;
std::map<ObjectId, OscillatingRecastMeshObject> mObjects;
- std::map<osg::Vec2i, Cell> mWater;
- std::map<osg::Vec2i, Heightfield> mHeightfields;
+ std::map<osg::Vec2i, Water> mWater;
+ std::map<osg::Vec2i, SizedHeightfieldShape> mHeightfields;
std::optional<Report> mLastNavMeshReportedChange;
std::optional<Report> mLastNavMeshReport;
};
diff --git a/components/detournavigator/recastmeshobject.cpp b/components/detournavigator/recastmeshobject.cpp
index 31aa13a208..343aeeb39e 100644
--- a/components/detournavigator/recastmeshobject.cpp
+++ b/components/detournavigator/recastmeshobject.cpp
@@ -75,7 +75,8 @@ namespace DetourNavigator
RecastMeshObject::RecastMeshObject(const CollisionShape& shape, const btTransform& transform,
const AreaType areaType)
- : mHolder(shape.getHolder())
+ : mInstance(shape.getInstance())
+ , mObjectTransform(shape.getObjectTransform())
, mImpl(shape.getShape(), transform, areaType)
{
}
diff --git a/components/detournavigator/recastmeshobject.hpp b/components/detournavigator/recastmeshobject.hpp
index e833ee37e3..760774353c 100644
--- a/components/detournavigator/recastmeshobject.hpp
+++ b/components/detournavigator/recastmeshobject.hpp
@@ -2,6 +2,9 @@
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHOBJECT_H
#include "areatype.hpp"
+#include "objecttransform.hpp"
+
+#include <components/resource/bulletshape.hpp>
#include <LinearMath/btTransform.h>
@@ -19,17 +22,21 @@ namespace DetourNavigator
class CollisionShape
{
public:
- CollisionShape(osg::ref_ptr<const osg::Referenced> holder, const btCollisionShape& shape)
- : mHolder(std::move(holder))
+ CollisionShape(osg::ref_ptr<const Resource::BulletShapeInstance> instance, const btCollisionShape& shape,
+ const ObjectTransform& transform)
+ : mInstance(std::move(instance))
, mShape(shape)
+ , mObjectTransform(transform)
{}
- const osg::ref_ptr<const osg::Referenced>& getHolder() const { return mHolder; }
+ const osg::ref_ptr<const Resource::BulletShapeInstance>& getInstance() const { return mInstance; }
const btCollisionShape& getShape() const { return mShape; }
+ const ObjectTransform& getObjectTransform() const { return mObjectTransform; }
private:
- osg::ref_ptr<const osg::Referenced> mHolder;
+ osg::ref_ptr<const Resource::BulletShapeInstance> mInstance;
std::reference_wrapper<const btCollisionShape> mShape;
+ ObjectTransform mObjectTransform;
};
class ChildRecastMeshObject
@@ -60,7 +67,7 @@ namespace DetourNavigator
bool update(const btTransform& transform, const AreaType areaType) { return mImpl.update(transform, areaType); }
- const osg::ref_ptr<const osg::Referenced>& getHolder() const { return mHolder; }
+ const osg::ref_ptr<const Resource::BulletShapeInstance>& getInstance() const { return mInstance; }
const btCollisionShape& getShape() const { return mImpl.getShape(); }
@@ -68,8 +75,11 @@ namespace DetourNavigator
AreaType getAreaType() const { return mImpl.getAreaType(); }
+ const ObjectTransform& getObjectTransform() const { return mObjectTransform; }
+
private:
- osg::ref_ptr<const osg::Referenced> mHolder;
+ osg::ref_ptr<const Resource::BulletShapeInstance> mInstance;
+ ObjectTransform mObjectTransform;
ChildRecastMeshObject mImpl;
};
}
diff --git a/components/detournavigator/recastmeshprovider.hpp b/components/detournavigator/recastmeshprovider.hpp
new file mode 100644
index 0000000000..b01b7c4ea1
--- /dev/null
+++ b/components/detournavigator/recastmeshprovider.hpp
@@ -0,0 +1,33 @@
+#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHPROVIDER_H
+#define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHPROVIDER_H
+
+#include "tileposition.hpp"
+#include "recastmesh.hpp"
+#include "tilecachedrecastmeshmanager.hpp"
+#include "version.hpp"
+
+#include <functional>
+#include <memory>
+
+namespace DetourNavigator
+{
+ class RecastMesh;
+
+ class RecastMeshProvider
+ {
+ public:
+ RecastMeshProvider(TileCachedRecastMeshManager& impl)
+ : mImpl(impl)
+ {}
+
+ std::shared_ptr<RecastMesh> getMesh(std::string_view worldspace, const TilePosition& tilePosition) const
+ {
+ return mImpl.get().getNewMesh(worldspace, tilePosition);
+ }
+
+ private:
+ std::reference_wrapper<TileCachedRecastMeshManager> mImpl;
+ };
+}
+
+#endif
diff --git a/components/detournavigator/serialization.cpp b/components/detournavigator/serialization.cpp
new file mode 100644
index 0000000000..50b014acb1
--- /dev/null
+++ b/components/detournavigator/serialization.cpp
@@ -0,0 +1,272 @@
+#include "serialization.hpp"
+
+#include "dbrefgeometryobject.hpp"
+#include "preparednavmeshdata.hpp"
+#include "recast.hpp"
+#include "recastmesh.hpp"
+#include "settings.hpp"
+
+#include <components/serialization/binaryreader.hpp>
+#include <components/serialization/binarywriter.hpp>
+#include <components/serialization/format.hpp>
+#include <components/serialization/sizeaccumulator.hpp>
+
+#include <cstddef>
+#include <cstring>
+#include <type_traits>
+#include <vector>
+
+namespace DetourNavigator
+{
+namespace
+{
+ template <Serialization::Mode mode>
+ struct Format : Serialization::Format<mode, Format<mode>>
+ {
+ using Serialization::Format<mode, Format<mode>>::operator();
+
+ template <class Visitor>
+ void operator()(Visitor&& visitor, const osg::Vec2i& value) const
+ {
+ visitor(*this, value.ptr(), 2);
+ }
+
+ template <class Visitor>
+ void operator()(Visitor&& visitor, const osg::Vec2f& value) const
+ {
+ visitor(*this, value.ptr(), 2);
+ }
+
+ template <class Visitor>
+ void operator()(Visitor&& visitor, const osg::Vec3f& value) const
+ {
+ visitor(*this, value.ptr(), 3);
+ }
+
+ template <class Visitor>
+ void operator()(Visitor&& visitor, const Water& value) const
+ {
+ visitor(*this, value.mCellSize);
+ visitor(*this, value.mLevel);
+ }
+
+ template <class Visitor>
+ void operator()(Visitor&& visitor, const CellWater& value) const
+ {
+ visitor(*this, value.mCellPosition);
+ visitor(*this, value.mWater);
+ }
+
+ template <class Visitor>
+ void operator()(Visitor&& visitor, const RecastSettings& value) const
+ {
+ visitor(*this, value.mCellHeight);
+ visitor(*this, value.mCellSize);
+ visitor(*this, value.mDetailSampleDist);
+ visitor(*this, value.mDetailSampleMaxError);
+ visitor(*this, value.mMaxClimb);
+ visitor(*this, value.mMaxSimplificationError);
+ visitor(*this, value.mMaxSlope);
+ visitor(*this, value.mRecastScaleFactor);
+ visitor(*this, value.mSwimHeightScale);
+ visitor(*this, value.mBorderSize);
+ visitor(*this, value.mMaxEdgeLen);
+ visitor(*this, value.mMaxVertsPerPoly);
+ visitor(*this, value.mRegionMergeArea);
+ visitor(*this, value.mRegionMinArea);
+ visitor(*this, value.mTileSize);
+ }
+
+ template <class Visitor>
+ void operator()(Visitor&& visitor, const TileBounds& value) const
+ {
+ visitor(*this, value.mMin);
+ visitor(*this, value.mMax);
+ }
+
+ template <class Visitor>
+ void operator()(Visitor&& visitor, const Heightfield& value) const
+ {
+ visitor(*this, value.mCellPosition);
+ visitor(*this, value.mCellSize);
+ visitor(*this, value.mLength);
+ visitor(*this, value.mMinHeight);
+ visitor(*this, value.mMaxHeight);
+ visitor(*this, value.mHeights);
+ visitor(*this, value.mOriginalSize);
+ visitor(*this, value.mMinX);
+ visitor(*this, value.mMinY);
+ }
+
+ template <class Visitor>
+ void operator()(Visitor&& visitor, const FlatHeightfield& value) const
+ {
+ visitor(*this, value.mCellPosition);
+ visitor(*this, value.mCellSize);
+ visitor(*this, value.mHeight);
+ }
+
+ template <class Visitor>
+ void operator()(Visitor&& visitor, const RecastMesh& value) const
+ {
+ visitor(*this, value.getWater());
+ visitor(*this, value.getHeightfields());
+ visitor(*this, value.getFlatHeightfields());
+ }
+
+ template <class Visitor>
+ void operator()(Visitor&& visitor, const ESM::Position& value) const
+ {
+ visitor(*this, value.pos);
+ visitor(*this, value.rot);
+ }
+
+ template <class Visitor>
+ void operator()(Visitor&& visitor, const ObjectTransform& value) const
+ {
+ visitor(*this, value.mPosition);
+ visitor(*this, value.mScale);
+ }
+
+ template <class Visitor>
+ void operator()(Visitor&& visitor, const DbRefGeometryObject& value) const
+ {
+ visitor(*this, value.mShapeId);
+ visitor(*this, value.mObjectTransform);
+ }
+
+ template <class Visitor>
+ void operator()(Visitor&& visitor, const RecastSettings& settings, const RecastMesh& recastMesh,
+ const std::vector<DbRefGeometryObject>& dbRefGeometryObjects) const
+ {
+ visitor(*this, DetourNavigator::recastMeshMagic);
+ visitor(*this, DetourNavigator::recastMeshVersion);
+ visitor(*this, settings);
+ visitor(*this, recastMesh);
+ visitor(*this, dbRefGeometryObjects);
+ }
+
+ template <class Visitor, class T>
+ auto operator()(Visitor&& visitor, T& value) const
+ -> std::enable_if_t<std::is_same_v<std::decay_t<T>, rcPolyMesh>>
+ {
+ visitor(*this, value.nverts);
+ visitor(*this, value.npolys);
+ visitor(*this, value.maxpolys);
+ visitor(*this, value.nvp);
+ visitor(*this, value.bmin);
+ visitor(*this, value.bmax);
+ visitor(*this, value.cs);
+ visitor(*this, value.ch);
+ visitor(*this, value.borderSize);
+ visitor(*this, value.maxEdgeError);
+ if constexpr (mode == Serialization::Mode::Read)
+ {
+ if (value.verts == nullptr)
+ permRecastAlloc(value.verts, getVertsLength(value));
+ if (value.polys == nullptr)
+ permRecastAlloc(value.polys, getPolysLength(value));
+ if (value.regs == nullptr)
+ permRecastAlloc(value.regs, getRegsLength(value));
+ if (value.flags == nullptr)
+ permRecastAlloc(value.flags, getFlagsLength(value));
+ if (value.areas == nullptr)
+ permRecastAlloc(value.areas, getAreasLength(value));
+ }
+ visitor(*this, value.verts, getVertsLength(value));
+ visitor(*this, value.polys, getPolysLength(value));
+ visitor(*this, value.regs, getRegsLength(value));
+ visitor(*this, value.flags, getFlagsLength(value));
+ visitor(*this, value.areas, getAreasLength(value));
+ }
+
+ template <class Visitor, class T>
+ auto operator()(Visitor&& visitor, T& value) const
+ -> std::enable_if_t<std::is_same_v<std::decay_t<T>, rcPolyMeshDetail>>
+ {
+ visitor(*this, value.nmeshes);
+ if constexpr (mode == Serialization::Mode::Read)
+ if (value.meshes == nullptr)
+ permRecastAlloc(value.meshes, getMeshesLength(value));
+ visitor(*this, value.meshes, getMeshesLength(value));
+ visitor(*this, value.nverts);
+ if constexpr (mode == Serialization::Mode::Read)
+ if (value.verts == nullptr)
+ permRecastAlloc(value.verts, getVertsLength(value));
+ visitor(*this, value.verts, getVertsLength(value));
+ visitor(*this, value.ntris);
+ if constexpr (mode == Serialization::Mode::Read)
+ if (value.tris == nullptr)
+ permRecastAlloc(value.tris, getTrisLength(value));
+ visitor(*this, value.tris, getTrisLength(value));
+ }
+
+ template <class Visitor, class T>
+ auto operator()(Visitor&& visitor, T& value) const
+ -> std::enable_if_t<std::is_same_v<std::decay_t<T>, PreparedNavMeshData>>
+ {
+ if constexpr (mode == Serialization::Mode::Write)
+ {
+ visitor(*this, DetourNavigator::preparedNavMeshDataMagic);
+ visitor(*this, DetourNavigator::preparedNavMeshDataVersion);
+ }
+ else
+ {
+ static_assert(mode == Serialization::Mode::Read);
+ char magic[std::size(DetourNavigator::preparedNavMeshDataMagic)];
+ visitor(*this, magic);
+ if (std::memcmp(magic, DetourNavigator::preparedNavMeshDataMagic, sizeof(magic)) != 0)
+ throw std::runtime_error("Bad PreparedNavMeshData magic");
+ std::uint32_t version = 0;
+ visitor(*this, version);
+ if (version != DetourNavigator::preparedNavMeshDataVersion)
+ throw std::runtime_error("Bad PreparedNavMeshData version");
+ }
+ visitor(*this, value.mUserId);
+ visitor(*this, value.mCellSize);
+ visitor(*this, value.mCellHeight);
+ visitor(*this, value.mPolyMesh);
+ visitor(*this, value.mPolyMeshDetail);
+ }
+ };
+}
+} // namespace DetourNavigator
+
+namespace DetourNavigator
+{
+ std::vector<std::byte> serialize(const RecastSettings& settings, const RecastMesh& recastMesh,
+ const std::vector<DbRefGeometryObject>& dbRefGeometryObjects)
+ {
+ constexpr Format<Serialization::Mode::Write> format;
+ Serialization::SizeAccumulator sizeAccumulator;
+ format(sizeAccumulator, settings, recastMesh, dbRefGeometryObjects);
+ std::vector<std::byte> result(sizeAccumulator.value());
+ format(Serialization::BinaryWriter(result.data(), result.data() + result.size()),
+ settings, recastMesh, dbRefGeometryObjects);
+ return result;
+ }
+
+ std::vector<std::byte> serialize(const PreparedNavMeshData& value)
+ {
+ constexpr Format<Serialization::Mode::Write> format;
+ Serialization::SizeAccumulator sizeAccumulator;
+ format(sizeAccumulator, value);
+ std::vector<std::byte> result(sizeAccumulator.value());
+ format(Serialization::BinaryWriter(result.data(), result.data() + result.size()), value);
+ return result;
+ }
+
+ bool deserialize(const std::vector<std::byte>& data, PreparedNavMeshData& value)
+ {
+ try
+ {
+ constexpr Format<Serialization::Mode::Read> format;
+ format(Serialization::BinaryReader(data.data(), data.data() + data.size()), value);
+ return true;
+ }
+ catch (const std::exception&)
+ {
+ return false;
+ }
+ }
+}
diff --git a/components/detournavigator/serialization.hpp b/components/detournavigator/serialization.hpp
new file mode 100644
index 0000000000..194e50a994
--- /dev/null
+++ b/components/detournavigator/serialization.hpp
@@ -0,0 +1,29 @@
+#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_H
+#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_H
+
+#include <cstddef>
+#include <cstdint>
+#include <vector>
+
+namespace DetourNavigator
+{
+ class RecastMesh;
+ struct DbRefGeometryObject;
+ struct PreparedNavMeshData;
+ struct RecastSettings;
+
+ constexpr char recastMeshMagic[] = {'r', 'c', 's', 't'};
+ constexpr std::uint32_t recastMeshVersion = 1;
+
+ constexpr char preparedNavMeshDataMagic[] = {'p', 'n', 'a', 'v'};
+ constexpr std::uint32_t preparedNavMeshDataVersion = 1;
+
+ std::vector<std::byte> serialize(const RecastSettings& settings, const RecastMesh& value,
+ const std::vector<DbRefGeometryObject>& dbRefGeometryObjects);
+
+ std::vector<std::byte> serialize(const PreparedNavMeshData& value);
+
+ bool deserialize(const std::vector<std::byte>& data, PreparedNavMeshData& value);
+}
+
+#endif
diff --git a/components/detournavigator/settings.cpp b/components/detournavigator/settings.cpp
index e428f3695a..cc2c685992 100644
--- a/components/detournavigator/settings.cpp
+++ b/components/detournavigator/settings.cpp
@@ -3,43 +3,68 @@
#include <components/settings/settings.hpp>
#include <components/misc/constants.hpp>
+#include <algorithm>
+
namespace DetourNavigator
{
+ RecastSettings makeRecastSettingsFromSettingsManager()
+ {
+ constexpr float epsilon = std::numeric_limits<float>::epsilon();
+
+ RecastSettings result;
+
+ result.mBorderSize = std::max(0, ::Settings::Manager::getInt("border size", "Navigator"));
+ result.mCellHeight = std::max(epsilon, ::Settings::Manager::getFloat("cell height", "Navigator"));
+ result.mCellSize = std::max(epsilon, ::Settings::Manager::getFloat("cell size", "Navigator"));
+ result.mDetailSampleDist = std::max(0.0f, ::Settings::Manager::getFloat("detail sample dist", "Navigator"));
+ result.mDetailSampleMaxError = std::max(0.0f, ::Settings::Manager::getFloat("detail sample max error", "Navigator"));
+ result.mMaxClimb = Constants::sStepSizeUp;
+ result.mMaxSimplificationError = std::max(0.0f, ::Settings::Manager::getFloat("max simplification error", "Navigator"));
+ result.mMaxSlope = Constants::sMaxSlope;
+ result.mRecastScaleFactor = std::max(epsilon, ::Settings::Manager::getFloat("recast scale factor", "Navigator"));
+ result.mSwimHeightScale = 0;
+ result.mMaxEdgeLen = std::max(0, ::Settings::Manager::getInt("max edge len", "Navigator"));
+ result.mMaxVertsPerPoly = std::max(3, ::Settings::Manager::getInt("max verts per poly", "Navigator"));
+ result.mRegionMergeArea = std::max(0, ::Settings::Manager::getInt("region merge area", "Navigator"));
+ result.mRegionMinArea = std::max(0, ::Settings::Manager::getInt("region min area", "Navigator"));
+ result.mTileSize = std::max(1, ::Settings::Manager::getInt("tile size", "Navigator"));
+
+ return result;
+ }
+
+ DetourSettings makeDetourSettingsFromSettingsManager()
+ {
+ DetourSettings result;
+
+ result.mMaxNavMeshQueryNodes = std::clamp(::Settings::Manager::getInt("max nav mesh query nodes", "Navigator"), 1, 65535);
+ result.mMaxPolys = std::clamp(::Settings::Manager::getInt("max polygons per tile", "Navigator"), 1, (1 << 22) - 1);
+ result.mMaxPolygonPathSize = static_cast<std::size_t>(std::max(0, ::Settings::Manager::getInt("max polygon path size", "Navigator")));
+ result.mMaxSmoothPathSize = static_cast<std::size_t>(std::max(0, ::Settings::Manager::getInt("max smooth path size", "Navigator")));
+
+ return result;
+ }
+
Settings makeSettingsFromSettingsManager()
{
- Settings navigatorSettings;
-
- navigatorSettings.mBorderSize = ::Settings::Manager::getInt("border size", "Navigator");
- navigatorSettings.mCellHeight = ::Settings::Manager::getFloat("cell height", "Navigator");
- navigatorSettings.mCellSize = ::Settings::Manager::getFloat("cell size", "Navigator");
- navigatorSettings.mDetailSampleDist = ::Settings::Manager::getFloat("detail sample dist", "Navigator");
- navigatorSettings.mDetailSampleMaxError = ::Settings::Manager::getFloat("detail sample max error", "Navigator");
- navigatorSettings.mMaxClimb = Constants::sStepSizeUp;
- navigatorSettings.mMaxSimplificationError = ::Settings::Manager::getFloat("max simplification error", "Navigator");
- navigatorSettings.mMaxSlope = Constants::sMaxSlope;
- navigatorSettings.mRecastScaleFactor = ::Settings::Manager::getFloat("recast scale factor", "Navigator");
- navigatorSettings.mSwimHeightScale = 0;
- navigatorSettings.mMaxEdgeLen = ::Settings::Manager::getInt("max edge len", "Navigator");
- navigatorSettings.mMaxNavMeshQueryNodes = ::Settings::Manager::getInt("max nav mesh query nodes", "Navigator");
- navigatorSettings.mMaxPolys = ::Settings::Manager::getInt("max polygons per tile", "Navigator");
- navigatorSettings.mMaxTilesNumber = ::Settings::Manager::getInt("max tiles number", "Navigator");
- navigatorSettings.mMaxVertsPerPoly = ::Settings::Manager::getInt("max verts per poly", "Navigator");
- navigatorSettings.mRegionMergeSize = ::Settings::Manager::getInt("region merge size", "Navigator");
- navigatorSettings.mRegionMinSize = ::Settings::Manager::getInt("region min size", "Navigator");
- navigatorSettings.mTileSize = ::Settings::Manager::getInt("tile size", "Navigator");
- navigatorSettings.mWaitUntilMinDistanceToPlayer = ::Settings::Manager::getInt("wait until min distance to player", "Navigator");
- navigatorSettings.mAsyncNavMeshUpdaterThreads = static_cast<std::size_t>(::Settings::Manager::getInt("async nav mesh updater threads", "Navigator"));
- navigatorSettings.mMaxNavMeshTilesCacheSize = static_cast<std::size_t>(::Settings::Manager::getInt("max nav mesh tiles cache size", "Navigator"));
- navigatorSettings.mMaxPolygonPathSize = static_cast<std::size_t>(::Settings::Manager::getInt("max polygon path size", "Navigator"));
- navigatorSettings.mMaxSmoothPathSize = static_cast<std::size_t>(::Settings::Manager::getInt("max smooth path size", "Navigator"));
- navigatorSettings.mEnableWriteRecastMeshToFile = ::Settings::Manager::getBool("enable write recast mesh to file", "Navigator");
- navigatorSettings.mEnableWriteNavMeshToFile = ::Settings::Manager::getBool("enable write nav mesh to file", "Navigator");
- navigatorSettings.mRecastMeshPathPrefix = ::Settings::Manager::getString("recast mesh path prefix", "Navigator");
- navigatorSettings.mNavMeshPathPrefix = ::Settings::Manager::getString("nav mesh path prefix", "Navigator");
- navigatorSettings.mEnableRecastMeshFileNameRevision = ::Settings::Manager::getBool("enable recast mesh file name revision", "Navigator");
- navigatorSettings.mEnableNavMeshFileNameRevision = ::Settings::Manager::getBool("enable nav mesh file name revision", "Navigator");
- navigatorSettings.mMinUpdateInterval = std::chrono::milliseconds(::Settings::Manager::getInt("min update interval ms", "Navigator"));
-
- return navigatorSettings;
+ Settings result;
+
+ result.mRecast = makeRecastSettingsFromSettingsManager();
+ result.mDetour = makeDetourSettingsFromSettingsManager();
+ result.mMaxTilesNumber = std::max(0, ::Settings::Manager::getInt("max tiles number", "Navigator"));
+ result.mWaitUntilMinDistanceToPlayer = ::Settings::Manager::getInt("wait until min distance to player", "Navigator");
+ result.mAsyncNavMeshUpdaterThreads = static_cast<std::size_t>(std::max(0, ::Settings::Manager::getInt("async nav mesh updater threads", "Navigator")));
+ result.mMaxNavMeshTilesCacheSize = static_cast<std::size_t>(std::max(std::int64_t {0}, ::Settings::Manager::getInt64("max nav mesh tiles cache size", "Navigator")));
+ result.mEnableWriteRecastMeshToFile = ::Settings::Manager::getBool("enable write recast mesh to file", "Navigator");
+ result.mEnableWriteNavMeshToFile = ::Settings::Manager::getBool("enable write nav mesh to file", "Navigator");
+ result.mRecastMeshPathPrefix = ::Settings::Manager::getString("recast mesh path prefix", "Navigator");
+ result.mNavMeshPathPrefix = ::Settings::Manager::getString("nav mesh path prefix", "Navigator");
+ result.mEnableRecastMeshFileNameRevision = ::Settings::Manager::getBool("enable recast mesh file name revision", "Navigator");
+ result.mEnableNavMeshFileNameRevision = ::Settings::Manager::getBool("enable nav mesh file name revision", "Navigator");
+ result.mMinUpdateInterval = std::chrono::milliseconds(::Settings::Manager::getInt("min update interval ms", "Navigator"));
+ result.mNavMeshVersion = ::Settings::Manager::getInt("nav mesh version", "Navigator");
+ result.mEnableNavMeshDiskCache = ::Settings::Manager::getBool("enable nav mesh disk cache", "Navigator");
+ result.mWriteToNavMeshDb = ::Settings::Manager::getBool("write to navmeshdb", "Navigator");
+
+ return result;
}
}
diff --git a/components/detournavigator/settings.hpp b/components/detournavigator/settings.hpp
index 800ad6b2bb..e6be8017d5 100644
--- a/components/detournavigator/settings.hpp
+++ b/components/detournavigator/settings.hpp
@@ -2,17 +2,12 @@
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SETTINGS_H
#include <chrono>
-#include <optional>
#include <string>
namespace DetourNavigator
{
- struct Settings
+ struct RecastSettings
{
- bool mEnableWriteRecastMeshToFile = false;
- bool mEnableWriteNavMeshToFile = false;
- bool mEnableRecastMeshFileNameRevision = false;
- bool mEnableNavMeshFileNameRevision = false;
float mCellHeight = 0;
float mCellSize = 0;
float mDetailSampleDist = 0;
@@ -24,23 +19,44 @@ namespace DetourNavigator
float mSwimHeightScale = 0;
int mBorderSize = 0;
int mMaxEdgeLen = 0;
- int mMaxNavMeshQueryNodes = 0;
- int mMaxPolys = 0;
- int mMaxTilesNumber = 0;
int mMaxVertsPerPoly = 0;
- int mRegionMergeSize = 0;
- int mRegionMinSize = 0;
+ int mRegionMergeArea = 0;
+ int mRegionMinArea = 0;
int mTileSize = 0;
+ };
+
+ struct DetourSettings
+ {
+ int mMaxPolys = 0;
+ int mMaxNavMeshQueryNodes = 0;
+ std::size_t mMaxPolygonPathSize = 0;
+ std::size_t mMaxSmoothPathSize = 0;
+ };
+
+ struct Settings
+ {
+ bool mEnableWriteRecastMeshToFile = false;
+ bool mEnableWriteNavMeshToFile = false;
+ bool mEnableRecastMeshFileNameRevision = false;
+ bool mEnableNavMeshFileNameRevision = false;
+ bool mEnableNavMeshDiskCache = false;
+ bool mWriteToNavMeshDb = false;
+ RecastSettings mRecast;
+ DetourSettings mDetour;
int mWaitUntilMinDistanceToPlayer = 0;
+ int mMaxTilesNumber = 0;
std::size_t mAsyncNavMeshUpdaterThreads = 0;
std::size_t mMaxNavMeshTilesCacheSize = 0;
- std::size_t mMaxPolygonPathSize = 0;
- std::size_t mMaxSmoothPathSize = 0;
std::string mRecastMeshPathPrefix;
std::string mNavMeshPathPrefix;
std::chrono::milliseconds mMinUpdateInterval;
+ std::int64_t mNavMeshVersion = 0;
};
+ RecastSettings makeRecastSettingsFromSettingsManager();
+
+ DetourSettings makeDetourSettingsFromSettingsManager();
+
Settings makeSettingsFromSettingsManager();
}
diff --git a/components/detournavigator/settingsutils.hpp b/components/detournavigator/settingsutils.hpp
index 258eb64c65..285920e5a0 100644
--- a/components/detournavigator/settingsutils.hpp
+++ b/components/detournavigator/settingsutils.hpp
@@ -4,12 +4,8 @@
#include "settings.hpp"
#include "tilebounds.hpp"
#include "tileposition.hpp"
-#include "tilebounds.hpp"
-
-#include <LinearMath/btTransform.h>
#include <osg/Vec2f>
-#include <osg/Vec2i>
#include <osg/Vec3f>
#include <algorithm>
@@ -17,38 +13,31 @@
namespace DetourNavigator
{
- inline float getHeight(const Settings& settings,const osg::Vec3f& agentHalfExtents)
- {
- return 2.0f * agentHalfExtents.z() * settings.mRecastScaleFactor;
- }
-
- inline float getMaxClimb(const Settings& settings)
- {
- return settings.mMaxClimb * settings.mRecastScaleFactor;
- }
-
- inline float getRadius(const Settings& settings, const osg::Vec3f& agentHalfExtents)
- {
- return std::max(agentHalfExtents.x(), agentHalfExtents.y()) * std::sqrt(2) * settings.mRecastScaleFactor;
- }
-
- inline float toNavMeshCoordinates(const Settings& settings, float value)
+ inline float toNavMeshCoordinates(const RecastSettings& settings, float value)
{
return value * settings.mRecastScaleFactor;
}
- inline osg::Vec2f toNavMeshCoordinates(const Settings& settings, osg::Vec2f position)
+ inline osg::Vec2f toNavMeshCoordinates(const RecastSettings& settings, osg::Vec2f position)
{
return position * settings.mRecastScaleFactor;
}
- inline osg::Vec3f toNavMeshCoordinates(const Settings& settings, osg::Vec3f position)
+ inline osg::Vec3f toNavMeshCoordinates(const RecastSettings& settings, osg::Vec3f position)
{
std::swap(position.y(), position.z());
return position * settings.mRecastScaleFactor;
}
- inline osg::Vec3f fromNavMeshCoordinates(const Settings& settings, osg::Vec3f position)
+ inline TileBounds toNavMeshCoordinates(const RecastSettings& settings, const TileBounds& value)
+ {
+ return TileBounds {
+ toNavMeshCoordinates(settings, value.mMin),
+ toNavMeshCoordinates(settings, value.mMax)
+ };
+ }
+
+ inline osg::Vec3f fromNavMeshCoordinates(const RecastSettings& settings, osg::Vec3f position)
{
const auto factor = 1.0f / settings.mRecastScaleFactor;
position *= factor;
@@ -56,12 +45,12 @@ namespace DetourNavigator
return position;
}
- inline float getTileSize(const Settings& settings)
+ inline float getTileSize(const RecastSettings& settings)
{
return static_cast<float>(settings.mTileSize) * settings.mCellSize;
}
- inline TilePosition getTilePosition(const Settings& settings, const osg::Vec3f& position)
+ inline TilePosition getTilePosition(const RecastSettings& settings, const osg::Vec3f& position)
{
return TilePosition(
static_cast<int>(std::floor(position.x() / getTileSize(settings))),
@@ -69,7 +58,7 @@ namespace DetourNavigator
);
}
- inline TileBounds makeTileBounds(const Settings& settings, const TilePosition& tilePosition)
+ inline TileBounds makeTileBounds(const RecastSettings& settings, const TilePosition& tilePosition)
{
return TileBounds {
osg::Vec2f(tilePosition.x(), tilePosition.y()) * getTileSize(settings),
@@ -77,17 +66,12 @@ namespace DetourNavigator
};
}
- inline float getBorderSize(const Settings& settings)
+ inline float getBorderSize(const RecastSettings& settings)
{
return static_cast<float>(settings.mBorderSize) * settings.mCellSize;
}
- inline float getSwimLevel(const Settings& settings, const float waterLevel, const float agentHalfExtentsZ)
- {
- return waterLevel - settings.mSwimHeightScale * agentHalfExtentsZ - agentHalfExtentsZ;;
- }
-
- inline float getRealTileSize(const Settings& settings)
+ inline float getRealTileSize(const RecastSettings& settings)
{
return settings.mTileSize * settings.mCellSize / settings.mRecastScaleFactor;
}
@@ -96,6 +80,17 @@ namespace DetourNavigator
{
return std::floor(std::sqrt(settings.mMaxTilesNumber / osg::PI)) - 1;
}
+
+ inline TileBounds makeRealTileBoundsWithBorder(const RecastSettings& settings, const TilePosition& tilePosition)
+ {
+ TileBounds result = makeTileBounds(settings, tilePosition);
+ const float border = getBorderSize(settings);
+ result.mMin -= osg::Vec2f(border, border);
+ result.mMax += osg::Vec2f(border, border);
+ result.mMin /= settings.mRecastScaleFactor;
+ result.mMax /= settings.mRecastScaleFactor;
+ return result;
+ }
}
#endif
diff --git a/components/detournavigator/tilebounds.hpp b/components/detournavigator/tilebounds.hpp
index 8557045342..693a382740 100644
--- a/components/detournavigator/tilebounds.hpp
+++ b/components/detournavigator/tilebounds.hpp
@@ -2,6 +2,7 @@
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_TILEBOUNDS_H
#include <osg/Vec2f>
+#include <osg/Vec2i>
#include <algorithm>
#include <optional>
@@ -32,6 +33,14 @@ namespace DetourNavigator
return std::nullopt;
return TileBounds {osg::Vec2f(minX, minY), osg::Vec2f(maxX, maxY)};
}
+
+ inline TileBounds maxCellTileBounds(const osg::Vec2i& position, int size)
+ {
+ return TileBounds {
+ osg::Vec2f(position.x(), position.y()) * size,
+ osg::Vec2f(position.x() + 1, position.y() + 1) * size
+ };
+ }
}
#endif
diff --git a/components/detournavigator/tilecachedrecastmeshmanager.cpp b/components/detournavigator/tilecachedrecastmeshmanager.cpp
index d6e3e55005..bf3df92d6e 100644
--- a/components/detournavigator/tilecachedrecastmeshmanager.cpp
+++ b/components/detournavigator/tilecachedrecastmeshmanager.cpp
@@ -10,20 +10,34 @@
namespace DetourNavigator
{
- TileCachedRecastMeshManager::TileCachedRecastMeshManager(const Settings& settings)
+ TileCachedRecastMeshManager::TileCachedRecastMeshManager(const RecastSettings& settings)
: mSettings(settings)
{}
+ std::string TileCachedRecastMeshManager::getWorldspace() const
+ {
+ const std::lock_guard lock(mMutex);
+ return mWorldspace;
+ }
+
+ void TileCachedRecastMeshManager::setWorldspace(std::string_view worldspace)
+ {
+ const std::lock_guard lock(mMutex);
+ if (mWorldspace == worldspace)
+ return;
+ mTiles.clear();
+ mWorldspace = worldspace;
+ }
+
bool TileCachedRecastMeshManager::addObject(const ObjectId id, const CollisionShape& shape,
const btTransform& transform, const AreaType areaType)
{
std::vector<TilePosition> tilesPositions;
- const auto border = getBorderSize(mSettings);
{
- auto tiles = mTiles.lock();
+ const std::lock_guard lock(mMutex);
getTilesPositions(shape.getShape(), transform, mSettings, [&] (const TilePosition& tilePosition)
{
- if (addTile(id, shape, transform, areaType, tilePosition, border, tiles.get()))
+ if (addTile(id, shape, transform, areaType, tilePosition, mTiles))
tilesPositions.push_back(tilePosition);
});
}
@@ -42,10 +56,10 @@ namespace DetourNavigator
return std::nullopt;
std::optional<RemovedRecastMeshObject> result;
{
- auto tiles = mTiles.lock();
+ const std::lock_guard lock(mMutex);
for (const auto& tilePosition : object->second)
{
- const auto removed = removeTile(id, tilePosition, tiles.get());
+ const auto removed = removeTile(id, tilePosition, mTiles);
if (removed && !result)
result = removed;
}
@@ -55,21 +69,18 @@ namespace DetourNavigator
return result;
}
- bool TileCachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize,
- const osg::Vec3f& shift)
+ bool TileCachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, int cellSize, float level)
{
- const auto border = getBorderSize(mSettings);
-
auto& tilesPositions = mWaterTilesPositions[cellPosition];
bool result = false;
if (cellSize == std::numeric_limits<int>::max())
{
- const auto tiles = mTiles.lock();
- for (auto& tile : *tiles)
+ const std::lock_guard lock(mMutex);
+ for (auto& tile : mTiles)
{
- if (tile.second->addWater(cellPosition, cellSize, shift))
+ if (tile.second->addWater(cellPosition, cellSize, level))
{
tilesPositions.push_back(tile.first);
result = true;
@@ -78,19 +89,18 @@ namespace DetourNavigator
}
else
{
+ const btVector3 shift = Misc::Convert::toBullet(getWaterShift3d(cellPosition, cellSize, level));
getTilesPositions(cellSize, shift, mSettings, [&] (const TilePosition& tilePosition)
{
- const auto tiles = mTiles.lock();
- auto tile = tiles->find(tilePosition);
- if (tile == tiles->end())
+ const std::lock_guard lock(mMutex);
+ auto tile = mTiles.find(tilePosition);
+ if (tile == mTiles.end())
{
- auto tileBounds = makeTileBounds(mSettings, tilePosition);
- tileBounds.mMin -= osg::Vec2f(border, border);
- tileBounds.mMax += osg::Vec2f(border, border);
- tile = tiles->insert(std::make_pair(tilePosition,
- std::make_shared<CachedRecastMeshManager>(mSettings, tileBounds, mTilesGeneration))).first;
+ const TileBounds tileBounds = makeRealTileBoundsWithBorder(mSettings, tilePosition);
+ tile = mTiles.emplace_hint(tile, tilePosition,
+ std::make_shared<CachedRecastMeshManager>(tileBounds, mTilesGeneration));
}
- if (tile->second->addWater(cellPosition, cellSize, shift))
+ if (tile->second->addWater(cellPosition, cellSize, level))
{
tilesPositions.push_back(tilePosition);
result = true;
@@ -104,22 +114,22 @@ namespace DetourNavigator
return result;
}
- std::optional<Cell> TileCachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition)
+ std::optional<Water> TileCachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition)
{
const auto object = mWaterTilesPositions.find(cellPosition);
if (object == mWaterTilesPositions.end())
return std::nullopt;
- std::optional<Cell> result;
+ std::optional<Water> result;
for (const auto& tilePosition : object->second)
{
- const auto tiles = mTiles.lock();
- const auto tile = tiles->find(tilePosition);
- if (tile == tiles->end())
+ const std::lock_guard lock(mMutex);
+ const auto tile = mTiles.find(tilePosition);
+ if (tile == mTiles.end())
continue;
const auto tileResult = tile->second->removeWater(cellPosition);
if (tile->second->isEmpty())
{
- tiles->erase(tile);
+ mTiles.erase(tile);
++mTilesGeneration;
}
if (tileResult && !result)
@@ -131,27 +141,24 @@ namespace DetourNavigator
}
bool TileCachedRecastMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize,
- const osg::Vec3f& shift, const HeightfieldShape& shape)
+ const HeightfieldShape& shape)
{
- const auto border = getBorderSize(mSettings);
-
+ const btVector3 shift = getHeightfieldShift(shape, cellPosition, cellSize);
auto& tilesPositions = mHeightfieldTilesPositions[cellPosition];
bool result = false;
getTilesPositions(cellSize, shift, mSettings, [&] (const TilePosition& tilePosition)
{
- const auto tiles = mTiles.lock();
- auto tile = tiles->find(tilePosition);
- if (tile == tiles->end())
+ const std::lock_guard lock(mMutex);
+ auto tile = mTiles.find(tilePosition);
+ if (tile == mTiles.end())
{
- auto tileBounds = makeTileBounds(mSettings, tilePosition);
- tileBounds.mMin -= osg::Vec2f(border, border);
- tileBounds.mMax += osg::Vec2f(border, border);
- tile = tiles->insert(std::make_pair(tilePosition,
- std::make_shared<CachedRecastMeshManager>(mSettings, tileBounds, mTilesGeneration))).first;
+ const TileBounds tileBounds = makeRealTileBoundsWithBorder(mSettings, tilePosition);
+ tile = mTiles.emplace_hint(tile, tilePosition,
+ std::make_shared<CachedRecastMeshManager>(tileBounds, mTilesGeneration));
}
- if (tile->second->addHeightfield(cellPosition, cellSize, shift, shape))
+ if (tile->second->addHeightfield(cellPosition, cellSize, shape))
{
tilesPositions.push_back(tilePosition);
result = true;
@@ -164,22 +171,22 @@ namespace DetourNavigator
return result;
}
- std::optional<Cell> TileCachedRecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition)
+ std::optional<SizedHeightfieldShape> TileCachedRecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition)
{
const auto object = mHeightfieldTilesPositions.find(cellPosition);
if (object == mHeightfieldTilesPositions.end())
return std::nullopt;
- std::optional<Cell> result;
+ std::optional<SizedHeightfieldShape> result;
for (const auto& tilePosition : object->second)
{
- const auto tiles = mTiles.lock();
- const auto tile = tiles->find(tilePosition);
- if (tile == tiles->end())
+ const std::lock_guard lock(mMutex);
+ const auto tile = mTiles.find(tilePosition);
+ if (tile == mTiles.end())
continue;
const auto tileResult = tile->second->removeHeightfield(cellPosition);
if (tile->second->isEmpty())
{
- tiles->erase(tile);
+ mTiles.erase(tile);
++mTilesGeneration;
}
if (tileResult && !result)
@@ -190,20 +197,27 @@ namespace DetourNavigator
return result;
}
- std::shared_ptr<RecastMesh> TileCachedRecastMeshManager::getMesh(const TilePosition& tilePosition) const
+ std::shared_ptr<RecastMesh> TileCachedRecastMeshManager::getMesh(std::string_view worldspace, const TilePosition& tilePosition) const
{
- if (const auto manager = getManager(tilePosition))
+ if (const auto manager = getManager(worldspace, tilePosition))
return manager->getMesh();
return nullptr;
}
- std::shared_ptr<RecastMesh> TileCachedRecastMeshManager::getCachedMesh(const TilePosition& tilePosition) const
+ std::shared_ptr<RecastMesh> TileCachedRecastMeshManager::getCachedMesh(std::string_view worldspace, const TilePosition& tilePosition) const
{
- if (const auto manager = getManager(tilePosition))
+ if (const auto manager = getManager(worldspace, tilePosition))
return manager->getCachedMesh();
return nullptr;
}
+ std::shared_ptr<RecastMesh> TileCachedRecastMeshManager::getNewMesh(std::string_view worldspace, const TilePosition& tilePosition) const
+ {
+ if (const auto manager = getManager(worldspace, tilePosition))
+ return manager->getNewMesh();
+ return nullptr;
+ }
+
std::size_t TileCachedRecastMeshManager::getRevision() const
{
return mRevision;
@@ -211,25 +225,23 @@ namespace DetourNavigator
void TileCachedRecastMeshManager::reportNavMeshChange(const TilePosition& tilePosition, Version recastMeshVersion, Version navMeshVersion) const
{
- const auto tiles = mTiles.lockConst();
- const auto it = tiles->find(tilePosition);
- if (it == tiles->end())
+ const std::lock_guard lock(mMutex);
+ const auto it = mTiles.find(tilePosition);
+ if (it == mTiles.end())
return;
it->second->reportNavMeshChange(recastMeshVersion, navMeshVersion);
}
bool TileCachedRecastMeshManager::addTile(const ObjectId id, const CollisionShape& shape,
- const btTransform& transform, const AreaType areaType, const TilePosition& tilePosition, float border,
+ const btTransform& transform, const AreaType areaType, const TilePosition& tilePosition,
TilesMap& tiles)
{
auto tile = tiles.find(tilePosition);
if (tile == tiles.end())
{
- auto tileBounds = makeTileBounds(mSettings, tilePosition);
- tileBounds.mMin -= osg::Vec2f(border, border);
- tileBounds.mMax += osg::Vec2f(border, border);
- tile = tiles.insert(std::make_pair(
- tilePosition, std::make_shared<CachedRecastMeshManager>(mSettings, tileBounds, mTilesGeneration))).first;
+ const TileBounds tileBounds = makeRealTileBoundsWithBorder(mSettings, tilePosition);
+ tile = tiles.emplace_hint(tile, tilePosition,
+ std::make_shared<CachedRecastMeshManager>(tileBounds, mTilesGeneration));
}
return tile->second->addObject(id, shape, transform, areaType);
}
@@ -256,11 +268,14 @@ namespace DetourNavigator
return tileResult;
}
- std::shared_ptr<CachedRecastMeshManager> TileCachedRecastMeshManager::getManager(const TilePosition& tilePosition) const
+ std::shared_ptr<CachedRecastMeshManager> TileCachedRecastMeshManager::getManager(std::string_view worldspace,
+ const TilePosition& tilePosition) const
{
- const auto tiles = mTiles.lockConst();
- const auto it = tiles->find(tilePosition);
- if (it == tiles->end())
+ const std::lock_guard lock(mMutex);
+ if (mWorldspace != worldspace)
+ return nullptr;
+ const auto it = mTiles.find(tilePosition);
+ if (it == mTiles.end())
return nullptr;
return it->second;
}
diff --git a/components/detournavigator/tilecachedrecastmeshmanager.hpp b/components/detournavigator/tilecachedrecastmeshmanager.hpp
index 7b5480e006..23171f0925 100644
--- a/components/detournavigator/tilecachedrecastmeshmanager.hpp
+++ b/components/detournavigator/tilecachedrecastmeshmanager.hpp
@@ -8,8 +8,6 @@
#include "version.hpp"
#include "heightfieldshape.hpp"
-#include <components/misc/guarded.hpp>
-
#include <algorithm>
#include <map>
#include <mutex>
@@ -20,7 +18,11 @@ namespace DetourNavigator
class TileCachedRecastMeshManager
{
public:
- TileCachedRecastMeshManager(const Settings& settings);
+ explicit TileCachedRecastMeshManager(const RecastSettings& settings);
+
+ std::string getWorldspace() const;
+
+ void setWorldspace(std::string_view worldspace);
bool addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform,
const AreaType areaType);
@@ -33,23 +35,22 @@ namespace DetourNavigator
if (object == mObjectsTilesPositions.end())
return false;
auto& currentTiles = object->second;
- const auto border = getBorderSize(mSettings);
bool changed = false;
std::vector<TilePosition> newTiles;
{
- auto tiles = mTiles.lock();
+ const std::lock_guard lock(mMutex);
const auto onTilePosition = [&] (const TilePosition& tilePosition)
{
if (std::binary_search(currentTiles.begin(), currentTiles.end(), tilePosition))
{
newTiles.push_back(tilePosition);
- if (updateTile(id, transform, areaType, tilePosition, tiles.get()))
+ if (updateTile(id, transform, areaType, tilePosition, mTiles))
{
onChangedTile(tilePosition);
changed = true;
}
}
- else if (addTile(id, shape, transform, areaType, tilePosition, border, tiles.get()))
+ else if (addTile(id, shape, transform, areaType, tilePosition, mTiles))
{
newTiles.push_back(tilePosition);
onChangedTile(tilePosition);
@@ -60,7 +61,7 @@ namespace DetourNavigator
std::sort(newTiles.begin(), newTiles.end());
for (const auto& tile : currentTiles)
{
- if (!std::binary_search(newTiles.begin(), newTiles.end(), tile) && removeTile(id, tile, tiles.get()))
+ if (!std::binary_search(newTiles.begin(), newTiles.end(), tile) && removeTile(id, tile, mTiles))
{
onChangedTile(tile);
changed = true;
@@ -77,23 +78,25 @@ namespace DetourNavigator
std::optional<RemovedRecastMeshObject> removeObject(const ObjectId id);
- bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift);
+ bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level);
+
+ std::optional<Water> removeWater(const osg::Vec2i& cellPosition);
- std::optional<Cell> removeWater(const osg::Vec2i& cellPosition);
+ bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape);
- bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift,
- const HeightfieldShape& shape);
+ std::optional<SizedHeightfieldShape> removeHeightfield(const osg::Vec2i& cellPosition);
- std::optional<Cell> removeHeightfield(const osg::Vec2i& cellPosition);
+ std::shared_ptr<RecastMesh> getMesh(std::string_view worldspace, const TilePosition& tilePosition) const;
- std::shared_ptr<RecastMesh> getMesh(const TilePosition& tilePosition) const;
+ std::shared_ptr<RecastMesh> getCachedMesh(std::string_view worldspace, const TilePosition& tilePosition) const;
- std::shared_ptr<RecastMesh> getCachedMesh(const TilePosition& tilePosition) const;
+ std::shared_ptr<RecastMesh> getNewMesh(std::string_view worldspace, const TilePosition& tilePosition) const;
template <class Function>
void forEachTile(Function&& function) const
{
- for (auto& [tilePosition, recastMeshManager] : *mTiles.lockConst())
+ const std::lock_guard lock(mMutex);
+ for (auto& [tilePosition, recastMeshManager] : mTiles)
function(tilePosition, *recastMeshManager);
}
@@ -104,8 +107,10 @@ namespace DetourNavigator
private:
using TilesMap = std::map<TilePosition, std::shared_ptr<CachedRecastMeshManager>>;
- const Settings& mSettings;
- Misc::ScopeGuarded<TilesMap> mTiles;
+ const RecastSettings& mSettings;
+ mutable std::mutex mMutex;
+ std::string mWorldspace;
+ TilesMap mTiles;
std::unordered_map<ObjectId, std::vector<TilePosition>> mObjectsTilesPositions;
std::map<osg::Vec2i, std::vector<TilePosition>> mWaterTilesPositions;
std::map<osg::Vec2i, std::vector<TilePosition>> mHeightfieldTilesPositions;
@@ -113,7 +118,7 @@ namespace DetourNavigator
std::size_t mTilesGeneration = 0;
bool addTile(const ObjectId id, const CollisionShape& shape, const btTransform& transform,
- const AreaType areaType, const TilePosition& tilePosition, float border, TilesMap& tiles);
+ const AreaType areaType, const TilePosition& tilePosition, TilesMap& tiles);
bool updateTile(const ObjectId id, const btTransform& transform, const AreaType areaType,
const TilePosition& tilePosition, TilesMap& tiles);
@@ -121,7 +126,8 @@ namespace DetourNavigator
std::optional<RemovedRecastMeshObject> removeTile(const ObjectId id, const TilePosition& tilePosition,
TilesMap& tiles);
- inline std::shared_ptr<CachedRecastMeshManager> getManager(const TilePosition& tilePosition) const;
+ inline std::shared_ptr<CachedRecastMeshManager> getManager(std::string_view worldspace,
+ const TilePosition& tilePosition) const;
};
}
diff --git a/components/detournavigator/version.hpp b/components/detournavigator/version.hpp
index c9de98459d..792680a7d5 100644
--- a/components/detournavigator/version.hpp
+++ b/components/detournavigator/version.hpp
@@ -8,12 +8,32 @@ namespace DetourNavigator
{
struct Version
{
- std::size_t mGeneration;
- std::size_t mRevision;
+ std::size_t mGeneration = 0;
+ std::size_t mRevision = 0;
+
+ friend inline auto tie(const Version& value)
+ {
+ return std::tie(value.mGeneration, value.mRevision);
+ }
friend inline bool operator<(const Version& lhs, const Version& rhs)
{
- return std::tie(lhs.mGeneration, lhs.mRevision) < std::tie(rhs.mGeneration, rhs.mRevision);
+ return tie(lhs) < tie(rhs);
+ }
+
+ friend inline bool operator<=(const Version& lhs, const Version& rhs)
+ {
+ return tie(lhs) <= tie(rhs);
+ }
+
+ friend inline bool operator==(const Version& lhs, const Version& rhs)
+ {
+ return tie(lhs) == tie(rhs);
+ }
+
+ friend inline bool operator!=(const Version& lhs, const Version& rhs)
+ {
+ return !(lhs == rhs);
}
};
}
diff --git a/components/esm/aipackage.hpp b/components/esm/aipackage.hpp
index 026e65dd82..6993867da1 100644
--- a/components/esm/aipackage.hpp
+++ b/components/esm/aipackage.hpp
@@ -37,7 +37,8 @@ namespace ESM
struct AITravel
{
float mX, mY, mZ;
- int mUnk;
+ unsigned char mShouldRepeat;
+ unsigned char mPadding[3];
};
struct AITarget
@@ -45,13 +46,14 @@ namespace ESM
float mX, mY, mZ;
short mDuration;
NAME32 mId;
- short mUnk;
+ unsigned char mShouldRepeat;
+ unsigned char mPadding;
};
struct AIActivate
{
NAME32 mName;
- unsigned char mUnk;
+ unsigned char mShouldRepeat;
};
#pragma pack(pop)
diff --git a/components/esm/aisequence.cpp b/components/esm/aisequence.cpp
index ca4c21802c..b99cac3ade 100644
--- a/components/esm/aisequence.cpp
+++ b/components/esm/aisequence.cpp
@@ -3,6 +3,7 @@
#include "esmreader.hpp"
#include "esmwriter.hpp"
+#include <algorithm>
#include <memory>
namespace ESM
@@ -34,12 +35,16 @@ namespace AiSequence
{
esm.getHNT (mData, "DATA");
esm.getHNOT (mHidden, "HIDD");
+ mRepeat = false;
+ esm.getHNOT(mRepeat, "REPT");
}
void AiTravel::save(ESMWriter &esm) const
{
esm.writeHNT ("DATA", mData);
esm.writeHNT ("HIDD", mHidden);
+ if(mRepeat)
+ esm.writeHNT("REPT", mRepeat);
}
void AiEscort::load(ESMReader &esm)
@@ -50,6 +55,15 @@ namespace AiSequence
esm.getHNOT (mTargetActorId, "TAID");
esm.getHNT (mRemainingDuration, "DURA");
mCellId = esm.getHNOString ("CELL");
+ mRepeat = false;
+ esm.getHNOT(mRepeat, "REPT");
+ if(esm.getFormat() < 18)
+ {
+ // mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration.
+ // The exact value of mDuration only matters for repeating packages.
+ // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves.
+ mData.mDuration = std::max<float>(mRemainingDuration > 0, mRemainingDuration);
+ }
}
void AiEscort::save(ESMWriter &esm) const
@@ -60,6 +74,8 @@ namespace AiSequence
esm.writeHNT ("DURA", mRemainingDuration);
if (!mCellId.empty())
esm.writeHNString ("CELL", mCellId);
+ if(mRepeat)
+ esm.writeHNT("REPT", mRepeat);
}
void AiFollow::load(ESMReader &esm)
@@ -75,6 +91,15 @@ namespace AiSequence
esm.getHNOT (mCommanded, "CMND");
mActive = false;
esm.getHNOT (mActive, "ACTV");
+ mRepeat = false;
+ esm.getHNOT(mRepeat, "REPT");
+ if(esm.getFormat() < 18)
+ {
+ // mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration.
+ // The exact value of mDuration only matters for repeating packages.
+ // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves.
+ mData.mDuration = std::max<float>(mRemainingDuration > 0, mRemainingDuration);
+ }
}
void AiFollow::save(ESMWriter &esm) const
@@ -89,16 +114,22 @@ namespace AiSequence
esm.writeHNT ("CMND", mCommanded);
if (mActive)
esm.writeHNT("ACTV", mActive);
+ if(mRepeat)
+ esm.writeHNT("REPT", mRepeat);
}
void AiActivate::load(ESMReader &esm)
{
mTargetId = esm.getHNString("TARG");
+ mRepeat = false;
+ esm.getHNOT(mRepeat, "REPT");
}
void AiActivate::save(ESMWriter &esm) const
{
esm.writeHNString("TARG", mTargetId);
+ if(mRepeat)
+ esm.writeHNT("REPT", mRepeat);
}
void AiCombat::load(ESMReader &esm)
@@ -166,6 +197,7 @@ namespace AiSequence
void AiSequence::load(ESMReader &esm)
{
+ int count = 0;
while (esm.isNextSub("AIPK"))
{
int type;
@@ -181,6 +213,7 @@ namespace AiSequence
std::unique_ptr<AiWander> ptr = std::make_unique<AiWander>();
ptr->load(esm);
mPackages.back().mPackage = ptr.release();
+ ++count;
break;
}
case Ai_Travel:
@@ -188,6 +221,7 @@ namespace AiSequence
std::unique_ptr<AiTravel> ptr = std::make_unique<AiTravel>();
ptr->load(esm);
mPackages.back().mPackage = ptr.release();
+ ++count;
break;
}
case Ai_Escort:
@@ -195,6 +229,7 @@ namespace AiSequence
std::unique_ptr<AiEscort> ptr = std::make_unique<AiEscort>();
ptr->load(esm);
mPackages.back().mPackage = ptr.release();
+ ++count;
break;
}
case Ai_Follow:
@@ -202,6 +237,7 @@ namespace AiSequence
std::unique_ptr<AiFollow> ptr = std::make_unique<AiFollow>();
ptr->load(esm);
mPackages.back().mPackage = ptr.release();
+ ++count;
break;
}
case Ai_Activate:
@@ -209,6 +245,7 @@ namespace AiSequence
std::unique_ptr<AiActivate> ptr = std::make_unique<AiActivate>();
ptr->load(esm);
mPackages.back().mPackage = ptr.release();
+ ++count;
break;
}
case Ai_Combat:
@@ -231,6 +268,23 @@ namespace AiSequence
}
esm.getHNOT (mLastAiPackage, "LAST");
+
+ if(count > 1 && esm.getFormat() < 18)
+ {
+ for(auto& pkg : mPackages)
+ {
+ if(pkg.mType == Ai_Wander)
+ static_cast<AiWander*>(pkg.mPackage)->mData.mShouldRepeat = true;
+ else if(pkg.mType == Ai_Travel)
+ static_cast<AiTravel*>(pkg.mPackage)->mRepeat = true;
+ else if(pkg.mType == Ai_Escort)
+ static_cast<AiEscort*>(pkg.mPackage)->mRepeat = true;
+ else if(pkg.mType == Ai_Follow)
+ static_cast<AiFollow*>(pkg.mPackage)->mRepeat = true;
+ else if(pkg.mType == Ai_Activate)
+ static_cast<AiActivate*>(pkg.mPackage)->mRepeat = true;
+ }
+ }
}
}
}
diff --git a/components/esm/aisequence.hpp b/components/esm/aisequence.hpp
index d8c20185f8..00c1316d9c 100644
--- a/components/esm/aisequence.hpp
+++ b/components/esm/aisequence.hpp
@@ -81,6 +81,7 @@ namespace ESM
{
AiTravelData mData;
bool mHidden;
+ bool mRepeat;
void load(ESMReader &esm);
void save(ESMWriter &esm) const;
@@ -94,6 +95,7 @@ namespace ESM
std::string mTargetId;
std::string mCellId;
float mRemainingDuration;
+ bool mRepeat;
void load(ESMReader &esm);
void save(ESMWriter &esm) const;
@@ -112,6 +114,7 @@ namespace ESM
bool mCommanded;
bool mActive;
+ bool mRepeat;
void load(ESMReader &esm);
void save(ESMWriter &esm) const;
@@ -120,6 +123,7 @@ namespace ESM
struct AiActivate : AiPackage
{
std::string mTargetId;
+ bool mRepeat;
void load(ESMReader &esm);
void save(ESMWriter &esm) const;
diff --git a/components/esm/cellref.cpp b/components/esm/cellref.cpp
index 7126459871..002a885d92 100644
--- a/components/esm/cellref.cpp
+++ b/components/esm/cellref.cpp
@@ -5,11 +5,6 @@
#include "esmreader.hpp"
#include "esmwriter.hpp"
-namespace ESM
-{
- int GroundcoverIndex = std::numeric_limits<int>::max();
-}
-
void ESM::RefNum::load (ESMReader& esm, bool wide, const std::string& tag)
{
if (wide)
diff --git a/components/esm/cellref.hpp b/components/esm/cellref.hpp
index f6eff24cbf..0013329ccc 100644
--- a/components/esm/cellref.hpp
+++ b/components/esm/cellref.hpp
@@ -12,7 +12,6 @@ namespace ESM
class ESMReader;
const int UnbreakableLock = std::numeric_limits<int>::max();
- extern int GroundcoverIndex;
struct RefNum
{
@@ -27,10 +26,6 @@ namespace ESM
inline bool isSet() const { return mIndex != 0 || mContentFile != -1; }
inline void unset() { *this = {0, -1}; }
-
- // Note: this method should not be used for objects with invalid RefNum
- // (for example, for objects from disabled plugins in savegames).
- inline bool fromGroundcoverFile() const { return mContentFile >= GroundcoverIndex; }
};
/* Cell reference. This represents ONE object (of many) inside the
diff --git a/components/esm/creaturestats.cpp b/components/esm/creaturestats.cpp
index d5030a6580..0f404ba58b 100644
--- a/components/esm/creaturestats.cpp
+++ b/components/esm/creaturestats.cpp
@@ -2,6 +2,8 @@
#include "esmreader.hpp"
#include "esmwriter.hpp"
+#include <limits>
+
void ESM::CreatureStats::load (ESMReader &esm)
{
bool intFallback = esm.getFormat() < 11;
@@ -163,6 +165,13 @@ void ESM::CreatureStats::load (ESMReader &esm)
mCorprusSpells[id] = stats;
}
+ if(esm.getFormat() <= 18)
+ mMissingACDT = mGoldPool == std::numeric_limits<int>::min();
+ else
+ {
+ mMissingACDT = false;
+ esm.getHNOT(mMissingACDT, "NOAC");
+ }
}
void ESM::CreatureStats::save (ESMWriter &esm) const
@@ -174,7 +183,7 @@ void ESM::CreatureStats::save (ESMWriter &esm) const
mDynamic[i].save (esm);
if (mGoldPool)
- esm.writeHNT ("GOLD", mGoldPool);
+ esm.writeHNT("GOLD", mGoldPool);
if (mTradeTime.mDay != 0 || mTradeTime.mHour != 0)
esm.writeHNT ("TIME", mTradeTime);
@@ -246,6 +255,8 @@ void ESM::CreatureStats::save (ESMWriter &esm) const
for (int i=0; i<4; ++i)
mAiSettings[i].save(esm);
}
+ if (mMissingACDT)
+ esm.writeHNT("NOAC", mMissingACDT);
}
void ESM::CreatureStats::blank()
@@ -274,4 +285,5 @@ void ESM::CreatureStats::blank()
mDeathAnimation = -1;
mLevel = 1;
mCorprusSpells.clear();
+ mMissingACDT = false;
}
diff --git a/components/esm/creaturestats.hpp b/components/esm/creaturestats.hpp
index 651b126d0e..7b261e3dd3 100644
--- a/components/esm/creaturestats.hpp
+++ b/components/esm/creaturestats.hpp
@@ -85,6 +85,7 @@ namespace ESM
signed char mDeathAnimation;
ESM::TimeStamp mTimeOfDeath;
int mLevel;
+ bool mMissingACDT;
std::map<std::string, CorprusStats> mCorprusSpells;
SpellState mSpells;
diff --git a/components/esm/defs.hpp b/components/esm/defs.hpp
index 254e66ec3a..f87a67405b 100644
--- a/components/esm/defs.hpp
+++ b/components/esm/defs.hpp
@@ -3,6 +3,8 @@
#include <stdint.h>
+#include <tuple>
+
#include <osg/Vec3f>
namespace ESM
@@ -59,6 +61,12 @@ struct Position
{
return osg::Vec3f(rot[0], rot[1], rot[2]);
}
+
+ friend inline bool operator<(const Position& l, const Position& r)
+ {
+ const auto tuple = [] (const Position& v) { return std::tuple(v.asVec3(), v.asRotationVec3()); };
+ return tuple(l) < tuple(r);
+ }
};
#pragma pack(pop)
@@ -88,7 +96,7 @@ struct FourCC
static constexpr unsigned int value = (((((d << 8) | c) << 8) | b) << 8) | a;
};
-enum RecNameInts
+enum RecNameInts : unsigned int
{
// format 0 / legacy
REC_ACTI = FourCC<'A','C','T','I'>::value,
diff --git a/components/esm/esmreader.cpp b/components/esm/esmreader.cpp
index 316748b53a..416e8a9be2 100644
--- a/components/esm/esmreader.cpp
+++ b/components/esm/esmreader.cpp
@@ -24,6 +24,7 @@ ESMReader::ESMReader()
, mFileSize(0)
{
clearCtx();
+ mCtx.index = 0;
}
void ESMReader::restoreContext(const ESM_Context &rc)
@@ -69,7 +70,7 @@ void ESMReader::resolveParentFileIndices(const std::vector<ESMReader>& allPlugin
const ESMReader& reader = allPlugins.at(i);
if (reader.getFileSize() == 0)
continue; // Content file in non-ESM format
- const std::string candidate = reader.getName();
+ const std::string& candidate = reader.getName();
std::string fnamecandidate = boost::filesystem::path(candidate).filename().string();
if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) {
index = i;
diff --git a/components/esm/esmreader.hpp b/components/esm/esmreader.hpp
index 92f2a6673b..d7eb6ff0a1 100644
--- a/components/esm/esmreader.hpp
+++ b/components/esm/esmreader.hpp
@@ -32,14 +32,14 @@ public:
int getVer() const { return mHeader.mData.version; }
int getRecordCount() const { return mHeader.mData.records; }
float getFVer() const { return (mHeader.mData.version == VER_12) ? 1.2f : 1.3f; }
- const std::string getAuthor() const { return mHeader.mData.author; }
- const std::string getDesc() const { return mHeader.mData.desc; }
+ const std::string& getAuthor() const { return mHeader.mData.author; }
+ const std::string& getDesc() const { return mHeader.mData.desc; }
const std::vector<Header::MasterData> &getGameFiles() const { return mHeader.mMaster; }
const Header& getHeader() const { return mHeader; }
int getFormat() const { return mHeader.mFormat; };
const NAME &retSubName() const { return mCtx.subName; }
uint32_t getSubSize() const { return mCtx.leftSub; }
- std::string getName() const {return mCtx.filename; };
+ const std::string& getName() const { return mCtx.filename; };
/*************************************************************************
*
diff --git a/components/esm/loadland.cpp b/components/esm/loadland.cpp
index 3c4a1e0b07..e97ad6b759 100644
--- a/components/esm/loadland.cpp
+++ b/components/esm/loadland.cpp
@@ -169,7 +169,7 @@ namespace ESM
{
float height = mLandData->mHeights[int(row * vertMult) * ESM::Land::LAND_SIZE + int(col * vertMult)];
height /= height > 0 ? 128.f : 16.f;
- height = std::min(max, std::max(min, height));
+ height = std::clamp(height, min, max);
wnam[row * LAND_GLOBAL_MAP_LOD_SIZE_SQRT + col] = static_cast<signed char>(height);
}
}
diff --git a/components/esm/loadmgef.cpp b/components/esm/loadmgef.cpp
index badbbb2133..7aff249aeb 100644
--- a/components/esm/loadmgef.cpp
+++ b/components/esm/loadmgef.cpp
@@ -281,14 +281,12 @@ short MagicEffect::getResistanceEffect(short effect)
effects[DisintegrateArmor] = Sanctuary;
effects[DisintegrateWeapon] = Sanctuary;
- for (int i=0; i<5; ++i)
- effects[DrainAttribute+i] = ResistMagicka;
- for (int i=0; i<5; ++i)
- effects[DamageAttribute+i] = ResistMagicka;
- for (int i=0; i<5; ++i)
- effects[AbsorbAttribute+i] = ResistMagicka;
- for (int i=0; i<10; ++i)
- effects[WeaknessToFire+i] = ResistMagicka;
+ for (int i = DrainAttribute; i <= DamageSkill; ++i)
+ effects[i] = ResistMagicka;
+ for (int i = AbsorbAttribute; i <= AbsorbSkill; ++i)
+ effects[i] = ResistMagicka;
+ for (int i = WeaknessToFire; i <= WeaknessToNormalWeapons; ++i)
+ effects[i] = ResistMagicka;
effects[Burden] = ResistMagicka;
effects[Charm] = ResistMagicka;
@@ -326,14 +324,12 @@ short MagicEffect::getWeaknessEffect(short effect)
static std::map<short, short> effects;
if (effects.empty())
{
- for (int i=0; i<5; ++i)
- effects[DrainAttribute+i] = WeaknessToMagicka;
- for (int i=0; i<5; ++i)
- effects[DamageAttribute+i] = WeaknessToMagicka;
- for (int i=0; i<5; ++i)
- effects[AbsorbAttribute+i] = WeaknessToMagicka;
- for (int i=0; i<10; ++i)
- effects[WeaknessToFire+i] = WeaknessToMagicka;
+ for (int i = DrainAttribute; i <= DamageSkill; ++i)
+ effects[i] = WeaknessToMagicka;
+ for (int i = AbsorbAttribute; i <= AbsorbSkill; ++i)
+ effects[i] = WeaknessToMagicka;
+ for (int i = WeaknessToFire; i <= WeaknessToNormalWeapons; ++i)
+ effects[i] = WeaknessToMagicka;
effects[Burden] = WeaknessToMagicka;
effects[Charm] = WeaknessToMagicka;
diff --git a/components/esm/luascripts.cpp b/components/esm/luascripts.cpp
index c831cbbbfc..53beb02d82 100644
--- a/components/esm/luascripts.cpp
+++ b/components/esm/luascripts.cpp
@@ -72,7 +72,7 @@ void ESM::LuaScripts::load(ESMReader& esm)
{
esm.getSubHeader();
LuaTimer timer;
- esm.getT(timer.mUnit);
+ esm.getT(timer.mType);
esm.getT(timer.mTime);
timer.mCallbackName = esm.getHNString("LUAC");
timer.mCallbackArgument = loadLuaBinaryData(esm);
@@ -91,7 +91,7 @@ void ESM::LuaScripts::save(ESMWriter& esm) const
for (const LuaTimer& timer : script.mTimers)
{
esm.startSubRecord("LUAT");
- esm.writeT(timer.mUnit);
+ esm.writeT(timer.mType);
esm.writeT(timer.mTime);
esm.endRecord("LUAT");
esm.writeHNString("LUAC", timer.mCallbackName);
diff --git a/components/esm/luascripts.hpp b/components/esm/luascripts.hpp
index e6f7113c16..985d756fce 100644
--- a/components/esm/luascripts.hpp
+++ b/components/esm/luascripts.hpp
@@ -51,13 +51,13 @@ namespace ESM
struct LuaTimer
{
- enum class TimeUnit : bool
+ enum class Type : bool
{
- SECONDS = 0,
- HOURS = 1,
+ SIMULATION_TIME = 0,
+ GAME_TIME = 1,
};
- TimeUnit mUnit;
+ Type mType;
double mTime;
std::string mCallbackName;
std::string mCallbackArgument; // Serialized Lua table. It is a binary data. Can contain '\0'.
diff --git a/components/esm/player.cpp b/components/esm/player.cpp
index e2e9219e22..028a042809 100644
--- a/components/esm/player.cpp
+++ b/components/esm/player.cpp
@@ -44,13 +44,43 @@ void ESM::Player::load (ESMReader &esm)
checkPrevItems = false;
}
- bool intFallback = esm.getFormat() < 11;
- if (esm.hasMoreSubs())
+ if(esm.getFormat() < 19)
{
- for (int i=0; i<ESM::Attribute::Length; ++i)
- mSaveAttributes[i].load(esm, intFallback);
- for (int i=0; i<ESM::Skill::Length; ++i)
- mSaveSkills[i].load(esm, intFallback);
+ bool intFallback = esm.getFormat() < 11;
+ bool clearModified = esm.getFormat() < 17 && !mObject.mNpcStats.mIsWerewolf;
+ if (esm.hasMoreSubs())
+ {
+ for (int i=0; i<ESM::Attribute::Length; ++i)
+ {
+ StatState<float> attribute;
+ attribute.load(esm, intFallback);
+ if (clearModified)
+ attribute.mMod = 0.f;
+ mSaveAttributes[i] = attribute.mBase + attribute.mMod - attribute.mDamage;
+ if (mObject.mNpcStats.mIsWerewolf)
+ mObject.mCreatureStats.mAttributes[i] = attribute;
+ }
+ for (int i=0; i<ESM::Skill::Length; ++i)
+ {
+ StatState<float> skill;
+ skill.load(esm, intFallback);
+ if (clearModified)
+ skill.mMod = 0.f;
+ mSaveSkills[i] = skill.mBase + skill.mMod - skill.mDamage;
+ if (mObject.mNpcStats.mIsWerewolf)
+ {
+ if(i == ESM::Skill::Acrobatics)
+ mSetWerewolfAcrobatics = mObject.mNpcStats.mSkills[i].mBase != skill.mBase;
+ mObject.mNpcStats.mSkills[i] = skill;
+ }
+ }
+ }
+ }
+ else
+ {
+ mSetWerewolfAcrobatics = false;
+ esm.getHNT(mSaveAttributes, "WWAT");
+ esm.getHNT(mSaveSkills, "WWSK");
}
}
@@ -79,8 +109,6 @@ void ESM::Player::save (ESMWriter &esm) const
esm.writeHNString ("PREV", it->second);
}
- for (int i=0; i<ESM::Attribute::Length; ++i)
- mSaveAttributes[i].save(esm);
- for (int i=0; i<ESM::Skill::Length; ++i)
- mSaveSkills[i].save(esm);
+ esm.writeHNT("WWAT", mSaveAttributes);
+ esm.writeHNT("WWSK", mSaveSkills);
}
diff --git a/components/esm/player.hpp b/components/esm/player.hpp
index 78bd5ab6e7..bea29cf74a 100644
--- a/components/esm/player.hpp
+++ b/components/esm/player.hpp
@@ -23,6 +23,7 @@ namespace ESM
CellId mCellId;
float mLastKnownExteriorPosition[3];
unsigned char mHasMark;
+ bool mSetWerewolfAcrobatics;
ESM::Position mMarkedPosition;
CellId mMarkedCell;
std::string mBirthsign;
@@ -30,8 +31,8 @@ namespace ESM
int mCurrentCrimeId;
int mPaidCrimeId;
- StatState<float> mSaveAttributes[ESM::Attribute::Length];
- StatState<float> mSaveSkills[ESM::Skill::Length];
+ float mSaveAttributes[ESM::Attribute::Length];
+ float mSaveSkills[ESM::Skill::Length];
typedef std::map<std::string, std::string> PreviousItems; // previous equipped items, needed for bound spells
PreviousItems mPreviousItems;
diff --git a/components/esm/savedgame.cpp b/components/esm/savedgame.cpp
index 8a98a63419..6cecf26489 100644
--- a/components/esm/savedgame.cpp
+++ b/components/esm/savedgame.cpp
@@ -4,7 +4,7 @@
#include "esmwriter.hpp"
unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE;
-int ESM::SavedGame::sCurrentFormat = 17;
+int ESM::SavedGame::sCurrentFormat = 19;
void ESM::SavedGame::load (ESMReader &esm)
{
diff --git a/components/esmloader/load.cpp b/components/esmloader/load.cpp
index 9879f33274..842cc8003a 100644
--- a/components/esmloader/load.cpp
+++ b/components/esmloader/load.cpp
@@ -22,6 +22,7 @@
#include <algorithm>
#include <cstddef>
#include <map>
+#include <set>
#include <string>
#include <string_view>
#include <type_traits>
@@ -206,15 +207,33 @@ namespace EsmLoader
{
ShallowContent result;
+ const std::set<std::string> supportedFormats {
+ ".esm",
+ ".esp",
+ ".omwgame",
+ ".omwaddon",
+ ".project",
+ };
+
for (std::size_t i = 0; i < contentFiles.size(); ++i)
{
const std::string &file = contentFiles[i];
- const Files::MultiDirCollection& collection = fileCollections.getCollection(boost::filesystem::path(file).extension().string());
+ const std::string extension = Misc::StringUtils::lowerCase(boost::filesystem::path(file).extension().string());
+
+ if (supportedFormats.find(extension) == supportedFormats.end())
+ {
+ Log(Debug::Warning) << "Skipping unsupported content file: " << file;
+ continue;
+ }
+
+ const Files::MultiDirCollection& collection = fileCollections.getCollection(extension);
ESM::ESMReader& reader = readers[i];
reader.setEncoder(encoder);
reader.setIndex(static_cast<int>(i));
reader.open(collection.getPath(file).string());
+ if (query.mLoadCells)
+ reader.resolveParentFileIndices(readers);
loadEsm(query, readers[i], result);
}
diff --git a/components/fallback/validate.cpp b/components/fallback/validate.cpp
index a482d40faf..6f5d529e05 100644
--- a/components/fallback/validate.cpp
+++ b/components/fallback/validate.cpp
@@ -11,13 +11,12 @@ void Fallback::validate(boost::any& v, std::vector<std::string> const& tokens, F
for (const auto& token : tokens)
{
- std::string temp = Files::EscapeHashString::processString(token);
- size_t sep = temp.find(',');
- if (sep < 1 || sep == temp.length() - 1 || sep == std::string::npos)
+ size_t sep = token.find(',');
+ if (sep < 1 || sep == token.length() - 1 || sep == std::string::npos)
throw boost::program_options::validation_error(boost::program_options::validation_error::invalid_option_value);
- std::string key(temp.substr(0, sep));
- std::string value(temp.substr(sep + 1));
+ std::string key(token.substr(0, sep));
+ std::string value(token.substr(sep + 1));
map->mMap[key] = value;
}
diff --git a/components/fallback/validate.hpp b/components/fallback/validate.hpp
index 96690f50a5..2b9af88da2 100644
--- a/components/fallback/validate.hpp
+++ b/components/fallback/validate.hpp
@@ -3,8 +3,6 @@
#include <boost/program_options.hpp>
-#include <components/files/escape.hpp>
-
// Parses and validates a fallback map from boost program_options.
// Note: for boost to pick up the validate function, you need to pull in the namespace e.g.
// by using namespace Fallback;
diff --git a/components/files/configfileparser.cpp b/components/files/configfileparser.cpp
new file mode 100644
index 0000000000..7fcc033157
--- /dev/null
+++ b/components/files/configfileparser.cpp
@@ -0,0 +1,297 @@
+// This file's contents is largely lifted from boost::program_options with only minor modification.
+// Its original preamble (without updated dates) from those source files is below:
+
+// Copyright Vladimir Prus 2002-2004.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt
+// or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#include "configfileparser.hpp"
+
+#include <boost/program_options/detail/config_file.hpp>
+#include <boost/program_options/detail/convert.hpp>
+
+namespace Files
+{
+ namespace
+ {
+ /** Standalone parser for config files in ini-line format.
+ The parser is a model of single-pass lvalue iterator, and
+ default constructor creates past-the-end-iterator. The typical usage is:
+ config_file_iterator i(is, ... set of options ...), e;
+ for(; i !=e; ++i) {
+ *i;
+ }
+
+ Syntax conventions:
+
+ - config file can not contain positional options
+ - '#' is comment character: it is ignored together with
+ the rest of the line.
+ - variable assignments are in the form
+ name '=' value.
+ spaces around '=' are trimmed.
+ - Section names are given in brackets.
+
+ The actual option name is constructed by combining current section
+ name and specified option name, with dot between. If section_name
+ already contains dot at the end, new dot is not inserted. For example:
+ @verbatim
+ [gui.accessibility]
+ visual_bell=yes
+ @endverbatim
+ will result in option "gui.accessibility.visual_bell" with value
+ "yes" been returned.
+
+ TODO: maybe, we should just accept a pointer to options_description
+ class.
+ */
+ class common_config_file_iterator
+ : public boost::eof_iterator<common_config_file_iterator, bpo::option>
+ {
+ public:
+ common_config_file_iterator() { found_eof(); }
+ common_config_file_iterator(
+ const std::set<std::string>& allowed_options,
+ bool allow_unregistered = false);
+
+ virtual ~common_config_file_iterator() {}
+
+ public: // Method required by eof_iterator
+
+ void get();
+
+#if BOOST_WORKAROUND(_MSC_VER, <= 1900)
+ void decrement() {}
+ void advance(difference_type) {}
+#endif
+
+ protected: // Stubs for derived classes
+
+ // Obtains next line from the config file
+ // Note: really, this design is a bit ugly
+ // The most clean thing would be to pass 'line_iterator' to
+ // constructor of this class, but to avoid templating this class
+ // we'd need polymorphic iterator, which does not exist yet.
+ virtual bool getline(std::string&) { return false; }
+
+ protected:
+ /** Adds another allowed option. If the 'name' ends with
+ '*', then all options with the same prefix are
+ allowed. For example, if 'name' is 'foo*', then 'foo1' and
+ 'foo_bar' are allowed. */
+ void add_option(const char* name);
+
+ // Returns true if 's' is a registered option name.
+ bool allowed_option(const std::string& s) const;
+
+ // That's probably too much data for iterator, since
+ // it will be copied, but let's not bother for now.
+ std::set<std::string> allowed_options;
+ // Invariant: no element is prefix of other element.
+ std::set<std::string> allowed_prefixes;
+ std::string m_prefix;
+ bool m_allow_unregistered;
+ };
+
+ common_config_file_iterator::common_config_file_iterator(
+ const std::set<std::string>& allowed_options,
+ bool allow_unregistered)
+ : allowed_options(allowed_options),
+ m_allow_unregistered(allow_unregistered)
+ {
+ for (std::set<std::string>::const_iterator i = allowed_options.begin();
+ i != allowed_options.end();
+ ++i)
+ {
+ add_option(i->c_str());
+ }
+ }
+
+ void common_config_file_iterator::add_option(const char* name)
+ {
+ std::string s(name);
+ assert(!s.empty());
+ if (*s.rbegin() == '*') {
+ s.resize(s.size() - 1);
+ bool bad_prefixes(false);
+ // If 's' is a prefix of one of allowed suffix, then
+ // lower_bound will return that element.
+ // If some element is prefix of 's', then lower_bound will
+ // return the next element.
+ std::set<std::string>::iterator i = allowed_prefixes.lower_bound(s);
+ if (i != allowed_prefixes.end()) {
+ if (i->find(s) == 0)
+ bad_prefixes = true;
+ }
+ if (i != allowed_prefixes.begin()) {
+ --i;
+ if (s.find(*i) == 0)
+ bad_prefixes = true;
+ }
+ if (bad_prefixes)
+ boost::throw_exception(bpo::error("options '" + std::string(name) + "' and '" +
+ *i + "*' will both match the same "
+ "arguments from the configuration file"));
+ allowed_prefixes.insert(s);
+ }
+ }
+
+ std::string trim_ws(const std::string& s)
+ {
+ std::string::size_type n, n2;
+ n = s.find_first_not_of(" \t\r\n");
+ if (n == std::string::npos)
+ return std::string();
+ else {
+ n2 = s.find_last_not_of(" \t\r\n");
+ return s.substr(n, n2 - n + 1);
+ }
+ }
+
+ void common_config_file_iterator::get()
+ {
+ std::string s;
+ std::string::size_type n;
+ bool found = false;
+
+ while (this->getline(s)) {
+
+ // strip '#' comments and whitespace
+ if (s.find('#') == s.find_first_not_of(" \t\r\n"))
+ continue;
+ s = trim_ws(s);
+
+ if (!s.empty()) {
+ // Handle section name
+ if (*s.begin() == '[' && *s.rbegin() == ']') {
+ m_prefix = s.substr(1, s.size() - 2);
+ if (*m_prefix.rbegin() != '.')
+ m_prefix += '.';
+ }
+ else if ((n = s.find('=')) != std::string::npos) {
+
+ std::string name = m_prefix + trim_ws(s.substr(0, n));
+ std::string value = trim_ws(s.substr(n + 1));
+
+ bool registered = allowed_option(name);
+ if (!registered && !m_allow_unregistered)
+ boost::throw_exception(bpo::unknown_option(name));
+
+ found = true;
+ this->value().string_key = name;
+ this->value().value.clear();
+ this->value().value.push_back(value);
+ this->value().unregistered = !registered;
+ this->value().original_tokens.clear();
+ this->value().original_tokens.push_back(name);
+ this->value().original_tokens.push_back(value);
+ break;
+
+ } else {
+ boost::throw_exception(bpo::invalid_config_file_syntax(s, bpo::invalid_syntax::unrecognized_line));
+ }
+ }
+ }
+ if (!found)
+ found_eof();
+ }
+
+ bool common_config_file_iterator::allowed_option(const std::string& s) const
+ {
+ std::set<std::string>::const_iterator i = allowed_options.find(s);
+ if (i != allowed_options.end())
+ return true;
+ // If s is "pa" where "p" is allowed prefix then
+ // lower_bound should find the element after "p".
+ // This depends on 'allowed_prefixes' invariant.
+ i = allowed_prefixes.lower_bound(s);
+ if (i != allowed_prefixes.begin() && s.find(*--i) == 0)
+ return true;
+ return false;
+ }
+
+
+
+ template<class charT>
+ class basic_config_file_iterator : public Files::common_config_file_iterator {
+ public:
+ basic_config_file_iterator()
+ {
+ found_eof();
+ }
+
+ /** Creates a config file parser for the specified stream.
+ */
+ basic_config_file_iterator(std::basic_istream<charT>& is,
+ const std::set<std::string>& allowed_options,
+ bool allow_unregistered = false);
+
+ private: // base overrides
+
+ bool getline(std::string&);
+
+ private: // internal data
+ std::shared_ptr<std::basic_istream<charT> > is;
+ };
+
+ template<class charT>
+ basic_config_file_iterator<charT>::
+ basic_config_file_iterator(std::basic_istream<charT>& is,
+ const std::set<std::string>& allowed_options,
+ bool allow_unregistered)
+ : common_config_file_iterator(allowed_options, allow_unregistered)
+ {
+ this->is.reset(&is, bpo::detail::null_deleter());
+ get();
+ }
+
+ template<class charT>
+ bool basic_config_file_iterator<charT>::getline(std::string& s)
+ {
+ std::basic_string<charT> in;
+ if (std::getline(*is, in)) {
+ s = bpo::to_internal(in);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ template<class charT>
+ bpo::basic_parsed_options<charT>
+ parse_config_file(std::basic_istream<charT>& is,
+ const bpo::options_description& desc,
+ bool allow_unregistered)
+ {
+ std::set<std::string> allowed_options;
+
+ const std::vector<boost::shared_ptr<bpo::option_description> >& options = desc.options();
+ for (unsigned i = 0; i < options.size(); ++i)
+ {
+ const bpo::option_description& d = *options[i];
+
+ if (d.long_name().empty())
+ boost::throw_exception(
+ bpo::error("abbreviated option names are not permitted in options configuration files"));
+
+ allowed_options.insert(d.long_name());
+ }
+
+ // Parser return char strings
+ bpo::parsed_options result(&desc);
+ copy(basic_config_file_iterator<charT>(
+ is, allowed_options, allow_unregistered),
+ basic_config_file_iterator<charT>(),
+ back_inserter(result.options));
+ // Convert char strings into desired type.
+ return bpo::basic_parsed_options<charT>(result);
+ }
+
+ template
+ bpo::basic_parsed_options<char>
+ parse_config_file(std::basic_istream<char>& is,
+ const bpo::options_description& desc,
+ bool allow_unregistered);
+}
diff --git a/components/files/configfileparser.hpp b/components/files/configfileparser.hpp
new file mode 100644
index 0000000000..60ab27a6d4
--- /dev/null
+++ b/components/files/configfileparser.hpp
@@ -0,0 +1,17 @@
+#ifndef COMPONENTS_FILES_CONFIGFILEPARSER_HPP
+#define COMPONENTS_FILES_CONFIGFILEPARSER_HPP
+
+#include <boost/program_options/parsers.hpp>
+
+namespace Files
+{
+
+namespace bpo = boost::program_options;
+
+template<class charT>
+bpo::basic_parsed_options<charT> parse_config_file(std::basic_istream<charT>&, const bpo::options_description&,
+ bool allow_unregistered = false);
+
+}
+
+#endif // COMPONENTS_FILES_CONFIGFILEPARSER_HPP \ No newline at end of file
diff --git a/components/files/configurationmanager.cpp b/components/files/configurationmanager.cpp
index b5d8b415f5..c2cd44960f 100644
--- a/components/files/configurationmanager.cpp
+++ b/components/files/configurationmanager.cpp
@@ -1,7 +1,7 @@
#include "configurationmanager.hpp"
#include <components/debug/debuglog.hpp>
-#include <components/files/escape.hpp>
+#include <components/files/configfileparser.hpp>
#include <components/fallback/validate.hpp>
#include <boost/filesystem/fstream.hpp>
@@ -109,7 +109,7 @@ void mergeComposingVariables(boost::program_options::variables_map& first, boost
auto replace = second["replace"];
if (!replace.defaulted() && !replace.empty())
{
- std::vector<std::string> replaceVector = replace.as<Files::EscapeStringVector>().toStdStringVector();
+ std::vector<std::string> replaceVector = replace.as<std::vector<std::string>>();
replacedVariables.insert(replaceVector.begin(), replaceVector.end());
}
}
@@ -138,19 +138,19 @@ void mergeComposingVariables(boost::program_options::variables_map& first, boost
boost::any& firstValue = firstPosition->second.value();
const boost::any& secondValue = second[name].value();
- if (firstValue.type() == typeid(Files::EscapePathContainer))
+ if (firstValue.type() == typeid(Files::MaybeQuotedPathContainer))
{
- auto& firstPathContainer = boost::any_cast<Files::EscapePathContainer&>(firstValue);
- const auto& secondPathContainer = boost::any_cast<const Files::EscapePathContainer&>(secondValue);
+ auto& firstPathContainer = boost::any_cast<Files::MaybeQuotedPathContainer&>(firstValue);
+ const auto& secondPathContainer = boost::any_cast<const Files::MaybeQuotedPathContainer&>(secondValue);
firstPathContainer.insert(firstPathContainer.end(), secondPathContainer.begin(), secondPathContainer.end());
}
- else if (firstValue.type() == typeid(Files::EscapeStringVector))
+ else if (firstValue.type() == typeid(std::vector<std::string>))
{
- auto& firstVector = boost::any_cast<Files::EscapeStringVector&>(firstValue);
- const auto& secondVector = boost::any_cast<const Files::EscapeStringVector&>(secondValue);
+ auto& firstVector = boost::any_cast<std::vector<std::string>&>(firstValue);
+ const auto& secondVector = boost::any_cast<const std::vector<std::string>&>(secondValue);
- firstVector.mVector.insert(firstVector.mVector.end(), secondVector.mVector.begin(), secondVector.mVector.end());
+ firstVector.insert(firstVector.end(), secondVector.begin(), secondVector.end());
}
else if (firstValue.type() == typeid(Fallback::FallbackMap))
{
@@ -235,11 +235,11 @@ bool ConfigurationManager::loadConfig(const boost::filesystem::path& path,
if (!mSilent)
Log(Debug::Info) << "Loading config file: " << cfgFile.string();
- boost::filesystem::ifstream configFileStreamUnfiltered(cfgFile);
+ boost::filesystem::ifstream configFileStream(cfgFile);
- if (configFileStreamUnfiltered.is_open())
+ if (configFileStream.is_open())
{
- parseConfig(configFileStreamUnfiltered, variables, description);
+ parseConfig(configFileStream, variables, description);
return true;
}
@@ -311,14 +311,37 @@ void parseArgs(int argc, const char* const argv[], boost::program_options::varia
void parseConfig(std::istream& stream, boost::program_options::variables_map& variables,
boost::program_options::options_description& description)
{
- boost::iostreams::filtering_istream configFileStream;
- configFileStream.push(escape_hash_filter());
- configFileStream.push(stream);
-
boost::program_options::store(
- boost::program_options::parse_config_file(configFileStream, description, true),
+ Files::parse_config_file(stream, description, true),
variables
);
}
+std::istream& operator>> (std::istream& istream, MaybeQuotedPath& MaybeQuotedPath)
+{
+ // If the stream starts with a double quote, read from stream using boost::filesystem::path rules, then discard anything remaining.
+ // This prevents boost::program_options getting upset that we've not consumed the whole stream.
+ // If it doesn't start with a double quote, read the whole thing verbatim
+ if (istream.peek() == '"')
+ {
+ istream >> static_cast<boost::filesystem::path&>(MaybeQuotedPath);
+ if (istream && !istream.eof() && istream.peek() != EOF)
+ {
+ std::string remainder{std::istreambuf_iterator(istream), {}};
+ Log(Debug::Warning) << "Trailing data in path setting. Used '" << MaybeQuotedPath.string() << "' but '" << remainder << "' remained";
+ }
+ }
+ else
+ {
+ std::string intermediate{std::istreambuf_iterator(istream), {}};
+ static_cast<boost::filesystem::path&>(MaybeQuotedPath) = intermediate;
+ }
+ return istream;
+}
+
+PathContainer asPathContainer(const MaybeQuotedPathContainer& MaybeQuotedPathContainer)
+{
+ return PathContainer(MaybeQuotedPathContainer.begin(), MaybeQuotedPathContainer.end());
+}
+
} /* namespace Cfg */
diff --git a/components/files/configurationmanager.hpp b/components/files/configurationmanager.hpp
index 99e65a7658..4b641c12fd 100644
--- a/components/files/configurationmanager.hpp
+++ b/components/files/configurationmanager.hpp
@@ -77,6 +77,16 @@ void parseArgs(int argc, const char* const argv[], boost::program_options::varia
void parseConfig(std::istream& stream, boost::program_options::variables_map& variables,
boost::program_options::options_description& description);
+class MaybeQuotedPath : public boost::filesystem::path
+{
+};
+
+std::istream& operator>> (std::istream& istream, MaybeQuotedPath& MaybeQuotedPath);
+
+typedef std::vector<MaybeQuotedPath> MaybeQuotedPathContainer;
+
+PathContainer asPathContainer(const MaybeQuotedPathContainer& MaybeQuotedPathContainer);
+
} /* namespace Cfg */
#endif /* COMPONENTS_FILES_CONFIGURATIONMANAGER_HPP */
diff --git a/components/files/escape.cpp b/components/files/escape.cpp
deleted file mode 100644
index fcbcc04a16..0000000000
--- a/components/files/escape.cpp
+++ /dev/null
@@ -1,145 +0,0 @@
-#include "escape.hpp"
-
-#include <components/misc/stringops.hpp>
-
-namespace Files
-{
- const int escape_hash_filter::sEscape = '@';
- const int escape_hash_filter::sEscapeIdentifier = 'a';
- const int escape_hash_filter::sHashIdentifier = 'h';
-
- escape_hash_filter::escape_hash_filter() : mSeenNonWhitespace(false), mFinishLine(false)
- {
- }
-
- escape_hash_filter::~escape_hash_filter()
- {
- }
-
- unescape_hash_filter::unescape_hash_filter() : expectingIdentifier(false)
- {
- }
-
- unescape_hash_filter::~unescape_hash_filter()
- {
- }
-
- std::string EscapeHashString::processString(const std::string & str)
- {
- std::string temp = str;
-
- static const char hash[] = { escape_hash_filter::sEscape, escape_hash_filter::sHashIdentifier };
- Misc::StringUtils::replaceAll(temp, std::string_view(hash, 2), "#");
-
- static const char escape[] = { escape_hash_filter::sEscape, escape_hash_filter::sEscapeIdentifier };
- Misc::StringUtils::replaceAll(temp, std::string_view(escape, 2), "@");
-
- return temp;
- }
-
- EscapeHashString::EscapeHashString() : mData()
- {
- }
-
- EscapeHashString::EscapeHashString(const std::string & str) : mData(EscapeHashString::processString(str))
- {
- }
-
- EscapeHashString::EscapeHashString(const std::string & str, size_t pos, size_t len) : mData(EscapeHashString::processString(str), pos, len)
- {
- }
-
- EscapeHashString::EscapeHashString(const char * s) : mData(EscapeHashString::processString(std::string(s)))
- {
- }
-
- EscapeHashString::EscapeHashString(const char * s, size_t n) : mData(EscapeHashString::processString(std::string(s)), 0, n)
- {
- }
-
- EscapeHashString::EscapeHashString(size_t n, char c) : mData(n, c)
- {
- }
-
- template <class InputIterator>
- EscapeHashString::EscapeHashString(InputIterator first, InputIterator last) : mData(EscapeHashString::processString(std::string(first, last)))
- {
- }
-
- std::string EscapeHashString::toStdString() const
- {
- return std::string(mData);
- }
-
- std::istream & operator>> (std::istream & is, EscapeHashString & eHS)
- {
- std::string temp;
- is >> temp;
- eHS = EscapeHashString(temp);
- return is;
- }
-
- std::ostream & operator<< (std::ostream & os, const EscapeHashString & eHS)
- {
- os << eHS.mData;
- return os;
- }
-
- EscapeStringVector::EscapeStringVector() : mVector()
- {
- }
-
- EscapeStringVector::~EscapeStringVector()
- {
- }
-
- std::vector<std::string> EscapeStringVector::toStdStringVector() const
- {
- std::vector<std::string> temp = std::vector<std::string>();
- for (std::vector<EscapeHashString>::const_iterator it = mVector.begin(); it != mVector.end(); ++it)
- {
- temp.push_back(it->toStdString());
- }
- return temp;
- }
-
- // boost program options validation
-
- void validate(boost::any &v, const std::vector<std::string> &tokens, Files::EscapeHashString * eHS, int a)
- {
- boost::program_options::validators::check_first_occurrence(v);
-
- if (v.empty())
- v = boost::any(EscapeHashString(boost::program_options::validators::get_single_string(tokens)));
- }
-
- void validate(boost::any &v, const std::vector<std::string> &tokens, EscapeStringVector *, int)
- {
- if (v.empty())
- v = boost::any(EscapeStringVector());
-
- EscapeStringVector * eSV = boost::any_cast<EscapeStringVector>(&v);
-
- for (std::vector<std::string>::const_iterator it = tokens.begin(); it != tokens.end(); ++it)
- eSV->mVector.emplace_back(*it);
- }
-
- PathContainer EscapePath::toPathContainer(const EscapePathContainer & escapePathContainer)
- {
- PathContainer temp;
- for (EscapePathContainer::const_iterator it = escapePathContainer.begin(); it != escapePathContainer.end(); ++it)
- temp.push_back(it->mPath);
- return temp;
- }
-
- std::istream & operator>> (std::istream & istream, EscapePath & escapePath)
- {
- boost::iostreams::filtering_istream filteredStream;
- filteredStream.push(unescape_hash_filter());
- filteredStream.push(istream);
-
- filteredStream >> escapePath.mPath;
-
- return istream;
- }
-}
diff --git a/components/files/escape.hpp b/components/files/escape.hpp
deleted file mode 100644
index d01bd8d980..0000000000
--- a/components/files/escape.hpp
+++ /dev/null
@@ -1,191 +0,0 @@
-#ifndef COMPONENTS_FILES_ESCAPE_HPP
-#define COMPONENTS_FILES_ESCAPE_HPP
-
-#include <queue>
-
-#include <components/files/multidircollection.hpp>
-
-#include <boost/iostreams/filtering_stream.hpp>
-#include <boost/filesystem/path.hpp>
-#include <boost/program_options.hpp>
-
-/**
- * \namespace Files
- */
-namespace Files
-{
- /**
- * \struct escape_hash_filter
- */
- struct escape_hash_filter : public boost::iostreams::input_filter
- {
- static const int sEscape;
- static const int sHashIdentifier;
- static const int sEscapeIdentifier;
-
- escape_hash_filter();
- virtual ~escape_hash_filter();
-
- template <typename Source> int get(Source & src);
-
- private:
- std::queue<int> mNext;
-
- bool mSeenNonWhitespace;
- bool mFinishLine;
- };
-
- template <typename Source>
- int escape_hash_filter::get(Source & src)
- {
- if (mNext.empty())
- {
- int character = boost::iostreams::get(src);
- if (character == boost::iostreams::WOULD_BLOCK)
- {
- mNext.push(character);
- }
- else if (character == EOF)
- {
- mSeenNonWhitespace = false;
- mFinishLine = false;
- mNext.push(character);
- }
- else if (character == '\n')
- {
- mSeenNonWhitespace = false;
- mFinishLine = false;
- mNext.push(character);
- }
- else if (mFinishLine)
- {
- mNext.push(character);
- }
- else if (character == '#')
- {
- if (mSeenNonWhitespace)
- {
- mNext.push(sEscape);
- mNext.push(sHashIdentifier);
- }
- else
- {
- //it's fine being interpreted by Boost as a comment, and so is anything afterwards
- mNext.push(character);
- mFinishLine = true;
- }
- }
- else if (character == sEscape)
- {
- mNext.push(sEscape);
- mNext.push(sEscapeIdentifier);
- }
- else
- {
- mNext.push(character);
- }
- if (!mSeenNonWhitespace && !isspace(character))
- mSeenNonWhitespace = true;
- }
- int retval = mNext.front();
- mNext.pop();
- return retval;
- }
-
- struct unescape_hash_filter : public boost::iostreams::input_filter
- {
- unescape_hash_filter();
- virtual ~unescape_hash_filter();
-
- template <typename Source> int get(Source & src);
-
- private:
- bool expectingIdentifier;
- };
-
- template <typename Source>
- int unescape_hash_filter::get(Source & src)
- {
- int character;
- if (!expectingIdentifier)
- character = boost::iostreams::get(src);
- else
- {
- character = escape_hash_filter::sEscape;
- expectingIdentifier = false;
- }
- if (character == escape_hash_filter::sEscape)
- {
- int nextChar = boost::iostreams::get(src);
- int intended;
- if (nextChar == escape_hash_filter::sEscapeIdentifier)
- intended = escape_hash_filter::sEscape;
- else if (nextChar == escape_hash_filter::sHashIdentifier)
- intended = '#';
- else if (nextChar == boost::iostreams::WOULD_BLOCK)
- {
- expectingIdentifier = true;
- intended = nextChar;
- }
- else
- intended = '?';
- return intended;
- }
- else
- return character;
- }
-
- /**
- * \class EscapeHashString
- */
- class EscapeHashString
- {
- private:
- std::string mData;
- public:
- static std::string processString(const std::string & str);
-
- EscapeHashString();
- EscapeHashString(const std::string & str);
- EscapeHashString(const std::string & str, size_t pos, size_t len = std::string::npos);
- EscapeHashString(const char * s);
- EscapeHashString(const char * s, size_t n);
- EscapeHashString(size_t n, char c);
- template <class InputIterator>
- EscapeHashString(InputIterator first, InputIterator last);
-
- std::string toStdString() const;
-
- friend std::ostream & operator<< (std::ostream & os, const EscapeHashString & eHS);
- };
-
- std::istream & operator>> (std::istream & is, EscapeHashString & eHS);
-
- struct EscapeStringVector
- {
- std::vector<Files::EscapeHashString> mVector;
-
- EscapeStringVector();
- virtual ~EscapeStringVector();
-
- std::vector<std::string> toStdStringVector() const;
- };
-
- //boost program options validation
-
- void validate(boost::any &v, const std::vector<std::string> &tokens, Files::EscapeHashString * eHS, int a);
-
- void validate(boost::any &v, const std::vector<std::string> &tokens, EscapeStringVector *, int);
-
- struct EscapePath
- {
- boost::filesystem::path mPath;
-
- static PathContainer toPathContainer(const std::vector<EscapePath> & escapePathContainer);
- };
-
- typedef std::vector<EscapePath> EscapePathContainer;
-
- std::istream & operator>> (std::istream & istream, EscapePath & escapePath);
-} /* namespace Files */
-#endif /* COMPONENTS_FILES_ESCAPE_HPP */
diff --git a/components/files/hash.cpp b/components/files/hash.cpp
new file mode 100644
index 0000000000..079a169ae5
--- /dev/null
+++ b/components/files/hash.cpp
@@ -0,0 +1,41 @@
+#include "hash.hpp"
+
+#include <extern/smhasher/MurmurHash3.h>
+
+#include <array>
+#include <cstdint>
+#include <istream>
+#include <string>
+
+namespace Files
+{
+ std::array<std::uint64_t, 2> getHash(const std::string& fileName, std::istream& stream)
+ {
+ std::array<std::uint64_t, 2> hash {0, 0};
+ try
+ {
+ const auto start = stream.tellg();
+ const auto exceptions = stream.exceptions();
+ stream.exceptions(std::ios_base::badbit);
+ while (stream)
+ {
+ std::array<char, 4096> value;
+ stream.read(value.data(), value.size());
+ const std::streamsize read = stream.gcount();
+ if (read == 0)
+ break;
+ std::array<std::uint64_t, 2> blockHash {0, 0};
+ MurmurHash3_x64_128(value.data(), static_cast<int>(read), hash.data(), blockHash.data());
+ hash = blockHash;
+ }
+ stream.exceptions(exceptions);
+ stream.clear();
+ stream.seekg(start);
+ }
+ catch (const std::exception& e)
+ {
+ throw std::runtime_error("Error while reading \"" + fileName + "\" to get hash: " + std::string(e.what()));
+ }
+ return hash;
+ }
+}
diff --git a/components/files/hash.hpp b/components/files/hash.hpp
new file mode 100644
index 0000000000..13d56d5824
--- /dev/null
+++ b/components/files/hash.hpp
@@ -0,0 +1,14 @@
+#ifndef COMPONENTS_FILES_HASH_H
+#define COMPONENTS_FILES_HASH_H
+
+#include <array>
+#include <cstdint>
+#include <istream>
+#include <string>
+
+namespace Files
+{
+ std::array<std::uint64_t, 2> getHash(const std::string& fileName, std::istream& stream);
+}
+
+#endif
diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp
index da43cc38ec..76f554bec7 100644
--- a/components/fontloader/fontloader.cpp
+++ b/components/fontloader/fontloader.cpp
@@ -145,7 +145,7 @@ namespace Gui
FontLoader::FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, const std::string& userDataPath, float scalingFactor)
: mVFS(vfs)
, mUserDataPath(userDataPath)
- , mFontHeight(16)
+ , mFontHeight(std::clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20))
, mScalingFactor(scalingFactor)
{
if (encoding == ToUTF8::WINDOWS_1252)
@@ -153,9 +153,6 @@ namespace Gui
else
mEncoding = encoding;
- int fontSize = Settings::Manager::getInt("font size", "GUI");
- mFontHeight = std::min(std::max(12, fontSize), 20);
-
MyGUI::ResourceManager::getInstance().unregisterLoadXmlDelegate("Resource");
MyGUI::ResourceManager::getInstance().registerLoadXmlDelegate("Resource") = MyGUI::newDelegate(this, &FontLoader::loadFontFromXml);
}
@@ -549,7 +546,7 @@ namespace Gui
// to allow to configure font size via config file, without need to edit XML files.
// Also we should take UI scaling factor in account.
int resolution = Settings::Manager::getInt("ttf resolution", "GUI");
- resolution = std::min(960, std::max(48, resolution)) * mScalingFactor;
+ resolution = std::clamp(resolution, 48, 960) * mScalingFactor;
MyGUI::xml::ElementPtr resolutionNode = resourceNode->createChild("Property");
resolutionNode->addAttribute("key", "Resolution");
@@ -591,7 +588,7 @@ namespace Gui
// setup separate fonts with different Resolution to fit these windows.
// These fonts have an internal prefix.
int resolution = Settings::Manager::getInt("ttf resolution", "GUI");
- resolution = std::min(960, std::max(48, resolution));
+ resolution = std::clamp(resolution, 48, 960);
float currentX = Settings::Manager::getInt("resolution x", "Video");
float currentY = Settings::Manager::getInt("resolution y", "Video");
diff --git a/components/interpreter/defines.cpp b/components/interpreter/defines.cpp
index 48d2ceaa45..164e7b2126 100644
--- a/components/interpreter/defines.cpp
+++ b/components/interpreter/defines.cpp
@@ -172,8 +172,7 @@ namespace Interpreter{
for(unsigned int j = 0; j < globals.size(); j++){
if(globals[j].length() > temp.length()){ // Just in case there's a global with a huuuge name
- temp = text.substr(i+1, globals[j].length());
- transform(temp.begin(), temp.end(), temp.begin(), ::tolower);
+ temp = Misc::StringUtils::lowerCase(text.substr(i+1, globals[j].length()));
}
found = check(temp, globals[j], &i, &start);
diff --git a/components/lua/configuration.cpp b/components/lua/configuration.cpp
index 4598ed2508..172d958029 100644
--- a/components/lua/configuration.cpp
+++ b/components/lua/configuration.cpp
@@ -30,6 +30,11 @@ namespace LuaUtil
{"POTION", ESM::LuaScriptCfg::sPotion},
{"WEAPON", ESM::LuaScriptCfg::sWeapon},
};
+
+ bool isSpace(char c)
+ {
+ return std::isspace(static_cast<unsigned char>(c));
+ }
}
const std::vector<int> ScriptsConfiguration::sEmpty;
@@ -101,11 +106,11 @@ namespace LuaUtil
if (!line.empty() && line.back() == '\r')
line = line.substr(0, line.size() - 1);
- while (!line.empty() && std::isspace(line[0]))
+ while (!line.empty() && isSpace(line[0]))
line = line.substr(1);
if (line.empty() || line[0] == '#') // Skip empty lines and comments
continue;
- while (!line.empty() && std::isspace(line.back()))
+ while (!line.empty() && isSpace(line.back()))
line = line.substr(0, line.size() - 1);
if (!Misc::StringUtils::ciEndsWith(line, ".lua"))
@@ -118,7 +123,7 @@ namespace LuaUtil
throw std::runtime_error(Misc::StringUtils::format("No flags found in: %s", std::string(line)));
std::string_view flagsStr = line.substr(0, semicolonPos);
std::string_view scriptPath = line.substr(semicolonPos + 1);
- while (std::isspace(scriptPath[0]))
+ while (isSpace(scriptPath[0]))
scriptPath = scriptPath.substr(1);
// Parse flags
@@ -126,10 +131,10 @@ namespace LuaUtil
size_t flagsPos = 0;
while (true)
{
- while (flagsPos < flagsStr.size() && (std::isspace(flagsStr[flagsPos]) || flagsStr[flagsPos] == ','))
+ while (flagsPos < flagsStr.size() && (isSpace(flagsStr[flagsPos]) || flagsStr[flagsPos] == ','))
flagsPos++;
size_t startPos = flagsPos;
- while (flagsPos < flagsStr.size() && !std::isspace(flagsStr[flagsPos]) && flagsStr[flagsPos] != ',')
+ while (flagsPos < flagsStr.size() && !isSpace(flagsStr[flagsPos]) && flagsStr[flagsPos] != ',')
flagsPos++;
if (startPos == flagsPos)
break;
diff --git a/components/lua/i18n.cpp b/components/lua/i18n.cpp
new file mode 100644
index 0000000000..e4d8e3ba78
--- /dev/null
+++ b/components/lua/i18n.cpp
@@ -0,0 +1,111 @@
+#include "i18n.hpp"
+
+#include <components/debug/debuglog.hpp>
+
+namespace sol
+{
+ template <>
+ struct is_automagical<LuaUtil::I18nManager::Context> : std::false_type {};
+}
+
+namespace LuaUtil
+{
+
+ void I18nManager::init()
+ {
+ mPreferredLanguages.push_back("en");
+ sol::usertype<Context> ctx = mLua->sol().new_usertype<Context>("I18nContext");
+ ctx[sol::meta_function::call] = &Context::translate;
+ try
+ {
+ mI18nLoader = mLua->loadInternalLib("i18n");
+ sol::set_environment(mLua->newInternalLibEnvironment(), mI18nLoader);
+ }
+ catch (std::exception& e)
+ {
+ Log(Debug::Error) << "LuaUtil::I18nManager initialization failed: " << e.what();
+ }
+ }
+
+ void I18nManager::setPreferredLanguages(const std::vector<std::string>& langs)
+ {
+ {
+ Log msg(Debug::Info);
+ msg << "I18n preferred languages:";
+ for (const std::string& l : langs)
+ msg << " " << l;
+ }
+ mPreferredLanguages = langs;
+ for (auto& [_, context] : mContexts)
+ context.updateLang(this);
+ }
+
+ void I18nManager::Context::readLangData(I18nManager* manager, const std::string& lang)
+ {
+ std::string path = "i18n/";
+ path.append(mName);
+ path.append("/");
+ path.append(lang);
+ path.append(".lua");
+ if (!manager->mVFS->exists(path))
+ return;
+ try
+ {
+ sol::protected_function dataFn = manager->mLua->loadFromVFS(path);
+ sol::environment emptyEnv(manager->mLua->sol(), sol::create);
+ sol::set_environment(emptyEnv, dataFn);
+ sol::table data = manager->mLua->newTable();
+ data[lang] = call(dataFn);
+ call(mI18n["load"], data);
+ mLoadedLangs[lang] = true;
+ }
+ catch (std::exception& e)
+ {
+ Log(Debug::Error) << "Can not load " << path << ": " << e.what();
+ }
+ }
+
+ sol::object I18nManager::Context::translate(std::string_view key, const sol::object& data)
+ {
+ sol::object res = call(mI18n["translate"], key, data);
+ if (res != sol::nil)
+ return res;
+
+ // If not found in a language file - register the key itself as a message.
+ std::string composedKey = call(mI18n["getLocale"]).get<std::string>();
+ composedKey.push_back('.');
+ composedKey.append(key);
+ call(mI18n["set"], composedKey, key);
+ return call(mI18n["translate"], key, data);
+ }
+
+ void I18nManager::Context::updateLang(I18nManager* manager)
+ {
+ for (const std::string& lang : manager->mPreferredLanguages)
+ {
+ if (mLoadedLangs[lang] == sol::nil)
+ readLangData(manager, lang);
+ if (mLoadedLangs[lang] != sol::nil)
+ {
+ Log(Debug::Verbose) << "Language file \"i18n/" << mName << "/" << lang << ".lua\" is enabled";
+ call(mI18n["setLocale"], lang);
+ return;
+ }
+ }
+ Log(Debug::Warning) << "No language files for the preferred languages found in \"i18n/" << mName << "\"";
+ }
+
+ sol::object I18nManager::getContext(const std::string& contextName)
+ {
+ if (mI18nLoader == sol::nil)
+ throw std::runtime_error("LuaUtil::I18nManager is not initialized");
+ auto it = mContexts.find(contextName);
+ if (it != mContexts.end())
+ return sol::make_object(mLua->sol(), it->second);
+ Context ctx{contextName, mLua->newTable(), call(mI18nLoader, "i18n.init")};
+ ctx.updateLang(this);
+ mContexts.emplace(contextName, ctx);
+ return sol::make_object(mLua->sol(), ctx);
+ }
+
+}
diff --git a/components/lua/i18n.hpp b/components/lua/i18n.hpp
new file mode 100644
index 0000000000..4bc7c624f1
--- /dev/null
+++ b/components/lua/i18n.hpp
@@ -0,0 +1,41 @@
+#ifndef COMPONENTS_LUA_I18N_H
+#define COMPONENTS_LUA_I18N_H
+
+#include "luastate.hpp"
+
+namespace LuaUtil
+{
+
+ class I18nManager
+ {
+ public:
+ I18nManager(const VFS::Manager* vfs, LuaState* lua) : mVFS(vfs), mLua(lua) {}
+ void init();
+
+ void setPreferredLanguages(const std::vector<std::string>& langs);
+ const std::vector<std::string>& getPreferredLanguages() const { return mPreferredLanguages; }
+
+ sol::object getContext(const std::string& contextName);
+
+ private:
+ struct Context
+ {
+ std::string mName;
+ sol::table mLoadedLangs;
+ sol::table mI18n;
+
+ void updateLang(I18nManager* manager);
+ void readLangData(I18nManager* manager, const std::string& lang);
+ sol::object translate(std::string_view key, const sol::object& data);
+ };
+
+ const VFS::Manager* mVFS;
+ LuaState* mLua;
+ sol::object mI18nLoader = sol::nil;
+ std::vector<std::string> mPreferredLanguages;
+ std::map<std::string, Context> mContexts;
+ };
+
+}
+
+#endif // COMPONENTS_LUA_I18N_H \ No newline at end of file
diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp
index e78f7bed06..bfe9cb513c 100644
--- a/components/lua/luastate.cpp
+++ b/components/lua/luastate.cpp
@@ -4,17 +4,44 @@
#include <luajit.h>
#endif // NO_LUAJIT
+#include <filesystem>
+
#include <components/debug/debuglog.hpp>
namespace LuaUtil
{
- static std::string packageNameToPath(std::string_view packageName)
+ static std::string packageNameToVfsPath(std::string_view packageName, const VFS::Manager* vfs)
{
- std::string res(packageName);
- std::replace(res.begin(), res.end(), '.', '/');
- res.append(".lua");
- return res;
+ std::string path(packageName);
+ std::replace(path.begin(), path.end(), '.', '/');
+ std::string pathWithInit = path + "/init.lua";
+ path.append(".lua");
+ if (vfs->exists(path))
+ return path;
+ else if (vfs->exists(pathWithInit))
+ return pathWithInit;
+ else
+ throw std::runtime_error("module not found: " + std::string(packageName));
+ }
+
+ static std::string packageNameToPath(std::string_view packageName, const std::vector<std::string>& searchDirs)
+ {
+ std::string path(packageName);
+ std::replace(path.begin(), path.end(), '.', '/');
+ std::string pathWithInit = path + "/init.lua";
+ path.append(".lua");
+ for (const std::string& dir : searchDirs)
+ {
+ std::filesystem::path base(dir);
+ std::filesystem::path p1 = base / path;
+ if (std::filesystem::exists(p1))
+ return p1.string();
+ std::filesystem::path p2 = base / pathWithInit;
+ if (std::filesystem::exists(p2))
+ return p2.string();
+ }
+ throw std::runtime_error("module not found: " + std::string(packageName));
}
static const std::string safeFunctions[] = {
@@ -25,10 +52,10 @@ namespace LuaUtil
LuaState::LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf) : mConf(conf), mVFS(vfs)
{
mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math,
- sol::lib::string, sol::lib::table, sol::lib::debug);
+ sol::lib::string, sol::lib::table, sol::lib::os, sol::lib::debug);
mLua["math"]["randomseed"](static_cast<unsigned>(std::time(nullptr)));
- mLua["math"]["randomseed"] = sol::nil;
+ mLua["math"]["randomseed"] = []{};
mLua["writeToLog"] = [](std::string_view s) { Log(Debug::Level::Info) << s; };
mLua.script(R"(printToLog = function(name, ...)
@@ -58,6 +85,11 @@ namespace LuaUtil
if (mLua[s] == sol::nil) throw std::logic_error("Lua package not found: " + s);
mCommonPackages[s] = mSandboxEnv[s] = makeReadOnly(mLua[s]);
}
+ mCommonPackages["os"] = mSandboxEnv["os"] = makeReadOnly(tableFromPairs<std::string_view, sol::function>({
+ {"date", mLua["os"]["date"]},
+ {"difftime", mLua["os"]["difftime"]},
+ {"time", mLua["os"]["time"]}
+ }));
}
LuaState::~LuaState()
@@ -76,9 +108,8 @@ namespace LuaUtil
lua_State* lua = table.lua_state();
table[sol::meta_function::index] = table;
- sol::stack::push(lua, std::move(table));
lua_newuserdata(lua, 0);
- lua_pushvalue(lua, -2);
+ sol::stack::push(lua, std::move(table));
lua_setmetatable(lua, -2);
return sol::stack::pop<sol::table>(lua);
}
@@ -106,7 +137,7 @@ namespace LuaUtil
const std::string& path, const std::string& namePrefix,
const std::map<std::string, sol::object>& packages, const sol::object& hiddenData)
{
- sol::protected_function script = loadScript(path);
+ sol::protected_function script = loadScriptAndCache(path);
sol::environment env(mLua, sol::create, mSandboxEnv);
std::string envName = namePrefix + "[" + path + "]:";
@@ -123,9 +154,9 @@ namespace LuaUtil
sol::object package = packages[packageName];
if (package == sol::nil)
{
- sol::protected_function packageLoader = loadScript(packageNameToPath(packageName));
+ sol::protected_function packageLoader = loadScriptAndCache(packageNameToVfsPath(packageName, mVFS));
sol::set_environment(env, packageLoader);
- package = throwIfError(packageLoader());
+ package = call(packageLoader, packageName);
if (!package.is<sol::table>())
throw std::runtime_error("Lua package must return a table.");
packages[packageName] = package;
@@ -139,6 +170,24 @@ namespace LuaUtil
return call(script);
}
+ sol::environment LuaState::newInternalLibEnvironment()
+ {
+ sol::environment env(mLua, sol::create, mSandboxEnv);
+ sol::table loaded(mLua, sol::create);
+ for (const std::string& s : safePackages)
+ loaded[s] = static_cast<sol::object>(mSandboxEnv[s]);
+ env["require"] = [this, loaded, env](const std::string& module) mutable
+ {
+ if (loaded[module] != sol::nil)
+ return loaded[module];
+ sol::protected_function initializer = loadInternalLib(module);
+ sol::set_environment(env, initializer);
+ loaded[module] = call(initializer, module);
+ return loaded[module];
+ };
+ return env;
+ }
+
sol::protected_function_result LuaState::throwIfError(sol::protected_function_result&& res)
{
if (!res.valid() && static_cast<int>(res.get_type()) == LUA_TSTRING)
@@ -147,17 +196,31 @@ namespace LuaUtil
return std::move(res);
}
- sol::function LuaState::loadScript(const std::string& path)
+ sol::function LuaState::loadScriptAndCache(const std::string& path)
{
auto iter = mCompiledScripts.find(path);
if (iter != mCompiledScripts.end())
return mLua.load(iter->second.as_string_view(), path, sol::load_mode::binary);
+ sol::function res = loadFromVFS(path);
+ mCompiledScripts[path] = res.dump();
+ return res;
+ }
+ sol::function LuaState::loadFromVFS(const std::string& path)
+ {
std::string fileContent(std::istreambuf_iterator<char>(*mVFS->get(path)), {});
sol::load_result res = mLua.load(fileContent, path, sol::load_mode::text);
if (!res.valid())
throw std::runtime_error("Lua error: " + res.get<std::string>());
- mCompiledScripts[path] = res.get<sol::function>().dump();
+ return res;
+ }
+
+ sol::function LuaState::loadInternalLib(std::string_view libName)
+ {
+ std::string path = packageNameToPath(libName, mLibSearchPaths);
+ sol::load_result res = mLua.load_file(path, sol::load_mode::text);
+ if (!res.valid())
+ throw std::runtime_error("Lua error: " + res.get<std::string>());
return res;
}
diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp
index 7ac5af0b1b..f9be5e9e99 100644
--- a/components/lua/luastate.hpp
+++ b/components/lua/luastate.hpp
@@ -35,9 +35,23 @@ namespace LuaUtil
// Returns underlying sol::state.
sol::state& sol() { return mLua; }
+ // Can be used by a C++ function that is called from Lua to get the Lua traceback.
+ // Makes no sense if called not from Lua code.
+ // Note: It is a slow function, should be used for debug purposes only.
+ std::string debugTraceback() { return mLua["debug"]["traceback"]().get<std::string>(); }
+
// A shortcut to create a new Lua table.
sol::table newTable() { return sol::table(mLua, sol::create); }
+ template <typename Key, typename Value>
+ sol::table tableFromPairs(std::initializer_list<std::pair<Key, Value>> list)
+ {
+ sol::table res(mLua, sol::create);
+ for (const auto& [k, v] : list)
+ res[k] = v;
+ return res;
+ }
+
// Registers a package that will be available from every sandbox via `require(name)`.
// The package can be either a sol::table with an API or a sol::function. If it is a function,
// it will be evaluated (once per sandbox) the first time when requested. If the package
@@ -62,12 +76,18 @@ namespace LuaUtil
const ScriptsConfiguration& getConfiguration() const { return *mConf; }
+ // Load internal Lua library. All libraries are loaded in one sandbox and shouldn't be exposed to scripts directly.
+ void addInternalLibSearchPath(const std::string& path) { mLibSearchPaths.push_back(path); }
+ sol::function loadInternalLib(std::string_view libName);
+ sol::function loadFromVFS(const std::string& path);
+ sol::environment newInternalLibEnvironment();
+
private:
static sol::protected_function_result throwIfError(sol::protected_function_result&&);
template <typename... Args>
friend sol::protected_function_result call(const sol::protected_function& fn, Args&&... args);
- sol::function loadScript(const std::string& path);
+ sol::function loadScriptAndCache(const std::string& path);
sol::state mLua;
const ScriptsConfiguration* mConf;
@@ -75,6 +95,7 @@ namespace LuaUtil
std::map<std::string, sol::bytecode> mCompiledScripts;
std::map<std::string, sol::object> mCommonPackages;
const VFS::Manager* mVFS;
+ std::vector<std::string> mLibSearchPaths;
};
// Should be used for every call of every Lua function.
@@ -105,6 +126,17 @@ namespace LuaUtil
// String representation of a Lua object. Should be used for debugging/logging purposes only.
std::string toString(const sol::object&);
+ template <class T>
+ T getValueOrDefault(const sol::object& obj, const T& defaultValue)
+ {
+ if (obj == sol::nil)
+ return defaultValue;
+ if (obj.is<T>())
+ return obj.as<T>();
+ else
+ throw std::logic_error(std::string("Value \"") + toString(obj) + std::string("\" has unexpected type"));
+ }
+
// Makes a table read only (when accessed from Lua) by wrapping it with an empty userdata.
// Needed to forbid any changes in common resources that can be accessed from different sandboxes.
sol::table makeReadOnly(sol::table);
diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp
index 517ad5f788..780d10cebd 100644
--- a/components/lua/scriptscontainer.cpp
+++ b/components/lua/scriptscontainer.cpp
@@ -309,21 +309,21 @@ namespace LuaUtil
void ScriptsContainer::save(ESM::LuaScripts& data)
{
std::map<int, std::vector<ESM::LuaTimer>> timers;
- auto saveTimerFn = [&](const Timer& timer, TimeUnit timeUnit)
+ auto saveTimerFn = [&](const Timer& timer, TimerType timerType)
{
if (!timer.mSerializable)
return;
ESM::LuaTimer savedTimer;
savedTimer.mTime = timer.mTime;
- savedTimer.mUnit = timeUnit;
+ savedTimer.mType = timerType;
savedTimer.mCallbackName = std::get<std::string>(timer.mCallback);
savedTimer.mCallbackArgument = timer.mSerializedArg;
timers[timer.mScriptId].push_back(std::move(savedTimer));
};
- for (const Timer& timer : mSecondsTimersQueue)
- saveTimerFn(timer, TimeUnit::SECONDS);
- for (const Timer& timer : mHoursTimersQueue)
- saveTimerFn(timer, TimeUnit::HOURS);
+ for (const Timer& timer : mSimulationTimersQueue)
+ saveTimerFn(timer, TimerType::SIMULATION_TIME);
+ for (const Timer& timer : mGameTimersQueue)
+ saveTimerFn(timer, TimerType::GAME_TIME);
data.mScripts.clear();
for (auto& [scriptId, script] : mScripts)
{
@@ -408,17 +408,17 @@ namespace LuaUtil
// updates refnums, so timer.mSerializedArg may be not equal to savedTimer.mCallbackArgument.
timer.mSerializedArg = serialize(timer.mArg, mSerializer);
- if (savedTimer.mUnit == TimeUnit::HOURS)
- mHoursTimersQueue.push_back(std::move(timer));
+ if (savedTimer.mType == TimerType::GAME_TIME)
+ mGameTimersQueue.push_back(std::move(timer));
else
- mSecondsTimersQueue.push_back(std::move(timer));
+ mSimulationTimersQueue.push_back(std::move(timer));
}
catch (std::exception& e) { printError(scriptId, "can not load timer", e); }
}
}
- std::make_heap(mSecondsTimersQueue.begin(), mSecondsTimersQueue.end());
- std::make_heap(mHoursTimersQueue.begin(), mHoursTimersQueue.end());
+ std::make_heap(mSimulationTimersQueue.begin(), mSimulationTimersQueue.end());
+ std::make_heap(mGameTimersQueue.begin(), mGameTimersQueue.end());
}
ScriptsContainer::~ScriptsContainer()
@@ -437,8 +437,8 @@ namespace LuaUtil
for (auto& [_, handlers] : mEngineHandlers)
handlers->mList.clear();
mEventHandlers.clear();
- mSecondsTimersQueue.clear();
- mHoursTimersQueue.clear();
+ mSimulationTimersQueue.clear();
+ mGameTimersQueue.clear();
mPublicInterfaces.clear();
// Assigned by LuaUtil::makeReadOnly, but `clear` removes it, so we need to assign it again.
@@ -464,7 +464,7 @@ namespace LuaUtil
std::push_heap(timerQueue.begin(), timerQueue.end());
}
- void ScriptsContainer::setupSerializableTimer(TimeUnit timeUnit, double time, int scriptId,
+ void ScriptsContainer::setupSerializableTimer(TimerType type, double time, int scriptId,
std::string_view callbackName, sol::object callbackArg)
{
Timer t;
@@ -474,10 +474,10 @@ namespace LuaUtil
t.mTime = time;
t.mArg = callbackArg;
t.mSerializedArg = serialize(t.mArg, mSerializer);
- insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t));
+ insertTimer(type == TimerType::GAME_TIME ? mGameTimersQueue : mSimulationTimersQueue, std::move(t));
}
- void ScriptsContainer::setupUnsavableTimer(TimeUnit timeUnit, double time, int scriptId, sol::function callback)
+ void ScriptsContainer::setupUnsavableTimer(TimerType type, double time, int scriptId, sol::function callback)
{
Timer t;
t.mScriptId = scriptId;
@@ -488,7 +488,7 @@ namespace LuaUtil
getScript(t.mScriptId).mTemporaryCallbacks.emplace(mTemporaryCallbackCounter, std::move(callback));
mTemporaryCallbackCounter++;
- insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t));
+ insertTimer(type == TimerType::GAME_TIME ? mGameTimersQueue : mSimulationTimersQueue, std::move(t));
}
void ScriptsContainer::callTimer(const Timer& t)
@@ -524,19 +524,10 @@ namespace LuaUtil
}
}
- void ScriptsContainer::processTimers(double gameSeconds, double gameHours)
+ void ScriptsContainer::processTimers(double simulationTime, double gameTime)
{
- updateTimerQueue(mSecondsTimersQueue, gameSeconds);
- updateTimerQueue(mHoursTimersQueue, gameHours);
- }
-
- void Callback::operator()(sol::object arg) const
- {
- if (mHiddenData[ScriptsContainer::sScriptIdKey] != sol::nil)
- LuaUtil::call(mFunc, std::move(arg));
- else
- Log(Debug::Debug) << "Ignored callback to the removed script "
- << mHiddenData.get<std::string>(ScriptsContainer::sScriptDebugNameKey);
+ updateTimerQueue(mSimulationTimersQueue, simulationTime);
+ updateTimerQueue(mGameTimersQueue, gameTime);
}
}
diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp
index e934868d08..fcbd2ba0b7 100644
--- a/components/lua/scriptscontainer.hpp
+++ b/components/lua/scriptscontainer.hpp
@@ -73,7 +73,7 @@ namespace LuaUtil
ScriptsContainer* mContainer;
int mIndex; // index in LuaUtil::ScriptsConfiguration
};
- using TimeUnit = ESM::LuaTimer::TimeUnit;
+ using TimerType = ESM::LuaTimer::Type;
// `namePrefix` is a common prefix for all scripts in the container. Used in logs for error messages and `print` output.
// `autoStartMode` specifies the list of scripts that should be autostarted in this container; the list itself is
@@ -99,8 +99,7 @@ namespace LuaUtil
bool hasScript(int scriptId) const { return mScripts.count(scriptId) != 0; }
void removeScript(int scriptId);
- // Processes timers. gameSeconds and gameHours are time (in seconds and in game hours) passed from the game start.
- void processTimers(double gameSeconds, double gameHours);
+ void processTimers(double simulationTime, double gameTime);
// Calls `onUpdate` (if present) for every script in the container.
// Handlers are called in the same order as scripts were added.
@@ -134,17 +133,17 @@ namespace LuaUtil
void registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback);
// Sets up a timer, that can be automatically saved and loaded.
- // timeUnit - game seconds (TimeUnit::Seconds) or game hours (TimeUnit::Hours).
+ // type - the type of timer, either SIMULATION_TIME or GAME_TIME.
// time - the absolute game time (in seconds or in hours) when the timer should be executed.
// scriptPath - script path in VFS is used as script id. The script with the given path should already present in the container.
// callbackName - callback (should be registered in advance) for this timer.
// callbackArg - parameter for the callback (should be serializable).
- void setupSerializableTimer(TimeUnit timeUnit, double time, int scriptId,
+ void setupSerializableTimer(TimerType type, double time, int scriptId,
std::string_view callbackName, sol::object callbackArg);
- // Creates a timer. `callback` is an arbitrary Lua function. This type of timers is called "unsavable"
- // because it can not be stored in saves. I.e. loading a saved game will not fully restore the state.
- void setupUnsavableTimer(TimeUnit timeUnit, double time, int scriptId, sol::function callback);
+ // Creates a timer. `callback` is an arbitrary Lua function. These timers are called "unsavable"
+ // because they can not be stored in saves. I.e. loading a saved game will not fully restore the state.
+ void setupUnsavableTimer(TimerType type, double time, int scriptId, sol::function callback);
protected:
struct Handler
@@ -237,12 +236,12 @@ namespace LuaUtil
std::map<std::string_view, EngineHandlerList*> mEngineHandlers;
std::map<std::string, EventHandlerList, std::less<>> mEventHandlers;
- std::vector<Timer> mSecondsTimersQueue;
- std::vector<Timer> mHoursTimersQueue;
+ std::vector<Timer> mSimulationTimersQueue;
+ std::vector<Timer> mGameTimersQueue;
int64_t mTemporaryCallbackCounter = 0;
};
- // Wrapper for a single-argument Lua function.
+ // Wrapper for a Lua function.
// Holds information about the script the function belongs to.
// Needed to prevent callback calls if the script was removed.
struct Callback
@@ -250,7 +249,15 @@ namespace LuaUtil
sol::function mFunc;
sol::table mHiddenData; // same object as Script::mHiddenData in ScriptsContainer
- void operator()(sol::object arg) const;
+ template <typename... Args>
+ void operator()(Args&&... args) const
+ {
+ if (mHiddenData[ScriptsContainer::sScriptIdKey] != sol::nil)
+ LuaUtil::call(mFunc, std::forward<Args>(args)...);
+ else
+ Log(Debug::Debug) << "Ignored callback to the removed script "
+ << mHiddenData.get<std::string>(ScriptsContainer::sScriptDebugNameKey);
+ }
};
}
diff --git a/components/lua/serialization.cpp b/components/lua/serialization.cpp
index 2e13cfe29f..e8f66c698c 100644
--- a/components/lua/serialization.cpp
+++ b/components/lua/serialization.cpp
@@ -1,10 +1,16 @@
#include "serialization.hpp"
+#include <osg/Matrixf>
+#include <osg/Quat>
#include <osg/Vec2f>
#include <osg/Vec3f>
+#include <osg/Vec4f>
#include <components/misc/endianness.hpp>
+#include "luastate.hpp"
+#include "utilpackage.hpp"
+
namespace LuaUtil
{
@@ -20,6 +26,8 @@ namespace LuaUtil
VEC2 = 0x10,
VEC3 = 0x11,
+ TRANSFORM_M = 0x12,
+ TRANSFORM_Q = 0x13,
// All values should be lesser than 0x20 (SHORT_STRING_FLAG).
};
@@ -91,17 +99,34 @@ namespace LuaUtil
{
appendType(out, SerializedType::VEC2);
osg::Vec2f v = data.as<osg::Vec2f>();
- appendValue<float>(out, v.x());
- appendValue<float>(out, v.y());
+ appendValue<double>(out, v.x());
+ appendValue<double>(out, v.y());
return;
}
if (data.is<osg::Vec3f>())
{
appendType(out, SerializedType::VEC3);
osg::Vec3f v = data.as<osg::Vec3f>();
- appendValue<float>(out, v.x());
- appendValue<float>(out, v.y());
- appendValue<float>(out, v.z());
+ appendValue<double>(out, v.x());
+ appendValue<double>(out, v.y());
+ appendValue<double>(out, v.z());
+ return;
+ }
+ if (data.is<TransformM>())
+ {
+ appendType(out, SerializedType::TRANSFORM_M);
+ osg::Matrixf matrix = data.as<TransformM>().mM;
+ for (size_t i = 0; i < 4; i++)
+ for (size_t j = 0; j < 4; j++)
+ appendValue<double>(out, matrix(i,j));
+ return;
+ }
+ if (data.is<TransformQ>())
+ {
+ appendType(out, SerializedType::TRANSFORM_Q);
+ osg::Quat quat = data.as<TransformQ>().mQ;
+ for(size_t i = 0; i < 4; i++)
+ appendValue<double>(out, quat[i]);
return;
}
if (customSerializer && customSerializer->serialize(out, data))
@@ -147,7 +172,8 @@ namespace LuaUtil
throw std::runtime_error("Unknown Lua type.");
}
- static void deserializeImpl(sol::state& lua, std::string_view& binaryData, const UserdataSerializer* customSerializer)
+ static void deserializeImpl(lua_State* lua, std::string_view& binaryData,
+ const UserdataSerializer* customSerializer, bool readOnly)
{
if (binaryData.empty())
throw std::runtime_error("Unexpected end of serialized data.");
@@ -176,22 +202,22 @@ namespace LuaUtil
if (type & SHORT_STRING_FLAG)
{
size_t size = type & 0x1f;
- sol::stack::push<std::string_view>(lua.lua_state(), binaryData.substr(0, size));
+ sol::stack::push<std::string_view>(lua, binaryData.substr(0, size));
binaryData = binaryData.substr(size);
return;
}
switch (static_cast<SerializedType>(type))
{
case SerializedType::NUMBER:
- sol::stack::push<double>(lua.lua_state(), getValue<double>(binaryData));
+ sol::stack::push<double>(lua, getValue<double>(binaryData));
return;
case SerializedType::BOOLEAN:
- sol::stack::push<bool>(lua.lua_state(), getValue<char>(binaryData) != 0);
+ sol::stack::push<bool>(lua, getValue<char>(binaryData) != 0);
return;
case SerializedType::LONG_STRING:
{
uint32_t size = getValue<uint32_t>(binaryData);
- sol::stack::push<std::string_view>(lua.lua_state(), binaryData.substr(0, size));
+ sol::stack::push<std::string_view>(lua, binaryData.substr(0, size));
binaryData = binaryData.substr(size);
return;
}
@@ -200,30 +226,49 @@ namespace LuaUtil
lua_createtable(lua, 0, 0);
while (!binaryData.empty() && binaryData[0] != char(SerializedType::TABLE_END))
{
- deserializeImpl(lua, binaryData, customSerializer);
- deserializeImpl(lua, binaryData, customSerializer);
+ deserializeImpl(lua, binaryData, customSerializer, readOnly);
+ deserializeImpl(lua, binaryData, customSerializer, readOnly);
lua_settable(lua, -3);
}
if (binaryData.empty())
throw std::runtime_error("Unexpected end of serialized data.");
binaryData = binaryData.substr(1);
+ if (readOnly)
+ sol::stack::push(lua, makeReadOnly(sol::stack::pop<sol::table>(lua)));
return;
}
case SerializedType::TABLE_END:
throw std::runtime_error("Unexpected end of table during deserialization.");
case SerializedType::VEC2:
{
- float x = getValue<float>(binaryData);
- float y = getValue<float>(binaryData);
- sol::stack::push<osg::Vec2f>(lua.lua_state(), osg::Vec2f(x, y));
+ float x = getValue<double>(binaryData);
+ float y = getValue<double>(binaryData);
+ sol::stack::push<osg::Vec2f>(lua, osg::Vec2f(x, y));
return;
}
case SerializedType::VEC3:
{
- float x = getValue<float>(binaryData);
- float y = getValue<float>(binaryData);
- float z = getValue<float>(binaryData);
- sol::stack::push<osg::Vec3f>(lua.lua_state(), osg::Vec3f(x, y, z));
+ float x = getValue<double>(binaryData);
+ float y = getValue<double>(binaryData);
+ float z = getValue<double>(binaryData);
+ sol::stack::push<osg::Vec3f>(lua, osg::Vec3f(x, y, z));
+ return;
+ }
+ case SerializedType::TRANSFORM_M:
+ {
+ osg::Matrixf mat;
+ for (int i = 0; i < 4; i++)
+ for (int j = 0; j < 4; j++)
+ mat(i, j) = getValue<double>(binaryData);
+ sol::stack::push<TransformM>(lua, asTransform(mat));
+ return;
+ }
+ case SerializedType::TRANSFORM_Q:
+ {
+ osg::Quat q;
+ for (int i = 0; i < 4; i++)
+ q[i] = getValue<double>(binaryData);
+ sol::stack::push<TransformQ>(lua, asTransform(q));
return;
}
}
@@ -240,7 +285,8 @@ namespace LuaUtil
return res;
}
- sol::object deserialize(sol::state& lua, std::string_view binaryData, const UserdataSerializer* customSerializer)
+ sol::object deserialize(lua_State* lua, std::string_view binaryData,
+ const UserdataSerializer* customSerializer, bool readOnly)
{
if (binaryData.empty())
return sol::nil;
@@ -248,10 +294,10 @@ namespace LuaUtil
throw std::runtime_error("Incorrect version of Lua serialization format: " +
std::to_string(static_cast<unsigned>(binaryData[0])));
binaryData = binaryData.substr(1);
- deserializeImpl(lua, binaryData, customSerializer);
+ deserializeImpl(lua, binaryData, customSerializer, readOnly);
if (!binaryData.empty())
throw std::runtime_error("Unexpected data after serialized object");
- return sol::stack::pop<sol::object>(lua.lua_state());
+ return sol::stack::pop<sol::object>(lua);
}
}
diff --git a/components/lua/serialization.hpp b/components/lua/serialization.hpp
index fddae2cfb4..d685bb2ad6 100644
--- a/components/lua/serialization.hpp
+++ b/components/lua/serialization.hpp
@@ -1,7 +1,6 @@
#ifndef COMPONENTS_LUA_SERIALIZATION_H
#define COMPONENTS_LUA_SERIALIZATION_H
-#include <limits> // missing from sol/sol.hpp
#include <sol/sol.hpp>
namespace LuaUtil
@@ -21,14 +20,15 @@ namespace LuaUtil
// Deserializes userdata of type "typeName" from binaryData. Should push the result on stack using sol::stack::push.
// Returns false if this type is not supported by this serializer.
- virtual bool deserialize(std::string_view typeName, std::string_view binaryData, sol::state&) const = 0;
+ virtual bool deserialize(std::string_view typeName, std::string_view binaryData, lua_State*) const = 0;
protected:
static void append(BinaryData&, std::string_view typeName, const void* data, size_t dataSize);
};
BinaryData serialize(const sol::object&, const UserdataSerializer* customSerializer = nullptr);
- sol::object deserialize(sol::state& lua, std::string_view binaryData, const UserdataSerializer* customSerializer = nullptr);
+ sol::object deserialize(lua_State* lua, std::string_view binaryData,
+ const UserdataSerializer* customSerializer = nullptr, bool readOnly = false);
}
diff --git a/components/lua/storage.cpp b/components/lua/storage.cpp
new file mode 100644
index 0000000000..def38fdd67
--- /dev/null
+++ b/components/lua/storage.cpp
@@ -0,0 +1,198 @@
+#include "storage.hpp"
+
+#include <filesystem>
+#include <fstream>
+
+#include <components/debug/debuglog.hpp>
+
+namespace sol
+{
+ template <>
+ struct is_automagical<LuaUtil::LuaStorage::SectionMutableView> : std::false_type {};
+ template <>
+ struct is_automagical<LuaUtil::LuaStorage::SectionReadOnlyView> : std::false_type {};
+}
+
+namespace LuaUtil
+{
+ LuaStorage::Value LuaStorage::Section::sEmpty;
+
+ sol::object LuaStorage::Value::getCopy(lua_State* L) const
+ {
+ return deserialize(L, mSerializedValue);
+ }
+
+ sol::object LuaStorage::Value::getReadOnly(lua_State* L) const
+ {
+ if (mReadOnlyValue == sol::nil && !mSerializedValue.empty())
+ mReadOnlyValue = deserialize(L, mSerializedValue, nullptr, true);
+ return mReadOnlyValue;
+ }
+
+ const LuaStorage::Value& LuaStorage::Section::get(std::string_view key) const
+ {
+ auto it = mValues.find(key);
+ if (it != mValues.end())
+ return it->second;
+ else
+ return sEmpty;
+ }
+
+ void LuaStorage::Section::set(std::string_view key, const sol::object& value)
+ {
+ mValues[std::string(key)] = Value(value);
+ mChangeCounter++;
+ if (mStorage->mListener)
+ (*mStorage->mListener)(mSectionName, key, value);
+ }
+
+ bool LuaStorage::Section::wasChanged(int64_t& lastCheck)
+ {
+ bool res = lastCheck < mChangeCounter;
+ lastCheck = mChangeCounter;
+ return res;
+ }
+
+ sol::table LuaStorage::Section::asTable()
+ {
+ sol::table res(mStorage->mLua, sol::create);
+ for (const auto& [k, v] : mValues)
+ res[k] = v.getCopy(mStorage->mLua);
+ return res;
+ }
+
+ void LuaStorage::initLuaBindings(lua_State* L)
+ {
+ sol::state_view lua(L);
+ sol::usertype<SectionReadOnlyView> roView = lua.new_usertype<SectionReadOnlyView>("ReadOnlySection");
+ sol::usertype<SectionMutableView> mutableView = lua.new_usertype<SectionMutableView>("MutableSection");
+ roView["get"] = [](sol::this_state s, SectionReadOnlyView& section, std::string_view key)
+ {
+ return section.mSection->get(key).getReadOnly(s);
+ };
+ roView["getCopy"] = [](sol::this_state s, SectionReadOnlyView& section, std::string_view key)
+ {
+ return section.mSection->get(key).getCopy(s);
+ };
+ roView["wasChanged"] = [](SectionReadOnlyView& section) { return section.mSection->wasChanged(section.mLastCheck); };
+ roView["asTable"] = [](SectionReadOnlyView& section) { return section.mSection->asTable(); };
+ mutableView["get"] = [](sol::this_state s, SectionMutableView& section, std::string_view key)
+ {
+ return section.mSection->get(key).getReadOnly(s);
+ };
+ mutableView["getCopy"] = [](sol::this_state s, SectionMutableView& section, std::string_view key)
+ {
+ return section.mSection->get(key).getCopy(s);
+ };
+ mutableView["wasChanged"] = [](SectionMutableView& section) { return section.mSection->wasChanged(section.mLastCheck); };
+ mutableView["asTable"] = [](SectionMutableView& section) { return section.mSection->asTable(); };
+ mutableView["reset"] = [](SectionMutableView& section, sol::optional<sol::table> newValues)
+ {
+ section.mSection->mValues.clear();
+ if (newValues)
+ {
+ for (const auto& [k, v] : *newValues)
+ {
+ try
+ {
+ section.mSection->set(k.as<std::string_view>(), v);
+ }
+ catch (std::exception& e)
+ {
+ Log(Debug::Error) << "LuaUtil::LuaStorage::Section::reset(table): " << e.what();
+ }
+ }
+ }
+ section.mSection->mChangeCounter++;
+ section.mLastCheck = section.mSection->mChangeCounter;
+ };
+ mutableView["removeOnExit"] = [](SectionMutableView& section) { section.mSection->mPermanent = false; };
+ mutableView["set"] = [](SectionMutableView& section, std::string_view key, const sol::object& value)
+ {
+ if (section.mLastCheck == section.mSection->mChangeCounter)
+ section.mLastCheck++;
+ section.mSection->set(key, value);
+ };
+ }
+
+ void LuaStorage::clearTemporary()
+ {
+ auto it = mData.begin();
+ while (it != mData.end())
+ {
+ if (!it->second->mPermanent)
+ it = mData.erase(it);
+ else
+ ++it;
+ }
+ }
+
+ void LuaStorage::load(const std::string& path)
+ {
+ mData.clear();
+ try
+ {
+ Log(Debug::Info) << "Loading Lua storage \"" << path << "\" (" << std::filesystem::file_size(path) << " bytes)";
+ std::ifstream fin(path, std::fstream::binary);
+ std::string serializedData((std::istreambuf_iterator<char>(fin)), std::istreambuf_iterator<char>());
+ sol::table data = deserialize(mLua, serializedData);
+ for (const auto& [sectionName, sectionTable] : data)
+ {
+ Section* section = getSection(sectionName.as<std::string_view>());
+ for (const auto& [key, value] : sol::table(sectionTable))
+ section->set(key.as<std::string_view>(), value);
+ }
+ }
+ catch (std::exception& e)
+ {
+ Log(Debug::Error) << "Can not read \"" << path << "\": " << e.what();
+ }
+ }
+
+ void LuaStorage::save(const std::string& path) const
+ {
+ sol::table data(mLua, sol::create);
+ for (const auto& [sectionName, section] : mData)
+ {
+ if (section->mPermanent)
+ data[sectionName] = section->asTable();
+ }
+ std::string serializedData = serialize(data);
+ Log(Debug::Info) << "Saving Lua storage \"" << path << "\" (" << serializedData.size() << " bytes)";
+ std::ofstream fout(path, std::fstream::binary);
+ fout.write(serializedData.data(), serializedData.size());
+ fout.close();
+ }
+
+ LuaStorage::Section* LuaStorage::getSection(std::string_view sectionName)
+ {
+ auto it = mData.find(sectionName);
+ if (it != mData.end())
+ return it->second.get();
+ auto section = std::make_unique<Section>(this, std::string(sectionName));
+ sectionName = section->mSectionName;
+ auto [newIt, _] = mData.emplace(sectionName, std::move(section));
+ return newIt->second.get();
+ }
+
+ sol::object LuaStorage::getReadOnlySection(std::string_view sectionName)
+ {
+ Section* section = getSection(sectionName);
+ return sol::make_object<SectionReadOnlyView>(mLua, SectionReadOnlyView{section, section->mChangeCounter});
+ }
+
+ sol::object LuaStorage::getMutableSection(std::string_view sectionName)
+ {
+ Section* section = getSection(sectionName);
+ return sol::make_object<SectionMutableView>(mLua, SectionMutableView{section, section->mChangeCounter});
+ }
+
+ sol::table LuaStorage::getAllSections()
+ {
+ sol::table res(mLua, sol::create);
+ for (const auto& [sectionName, _] : mData)
+ res[sectionName] = getMutableSection(sectionName);
+ return res;
+ }
+
+}
diff --git a/components/lua/storage.hpp b/components/lua/storage.hpp
new file mode 100644
index 0000000000..e847604aeb
--- /dev/null
+++ b/components/lua/storage.hpp
@@ -0,0 +1,81 @@
+#ifndef COMPONENTS_LUA_STORAGE_H
+#define COMPONENTS_LUA_STORAGE_H
+
+#include <map>
+#include <sol/sol.hpp>
+
+#include "serialization.hpp"
+
+namespace LuaUtil
+{
+
+ class LuaStorage
+ {
+ public:
+ static void initLuaBindings(lua_State*);
+
+ explicit LuaStorage(lua_State* lua) : mLua(lua) {}
+
+ void clearTemporary();
+ void load(const std::string& path);
+ void save(const std::string& path) const;
+
+ sol::object getReadOnlySection(std::string_view sectionName);
+ sol::object getMutableSection(std::string_view sectionName);
+ sol::table getAllSections();
+
+ void set(std::string_view section, std::string_view key, const sol::object& value) { getSection(section)->set(key, value); }
+
+ using ListenerFn = std::function<void(std::string_view, std::string_view, const sol::object&)>;
+ void setListener(ListenerFn fn) { mListener = std::move(fn); }
+
+ private:
+ class Value
+ {
+ public:
+ Value() {}
+ Value(const sol::object& value) : mSerializedValue(serialize(value)) {}
+ sol::object getCopy(lua_State* L) const;
+ sol::object getReadOnly(lua_State* L) const;
+
+ private:
+ std::string mSerializedValue;
+ mutable sol::object mReadOnlyValue = sol::nil;
+ };
+
+ struct Section
+ {
+ explicit Section(LuaStorage* storage, std::string name) : mStorage(storage), mSectionName(std::move(name)) {}
+ const Value& get(std::string_view key) const;
+ void set(std::string_view key, const sol::object& value);
+ bool wasChanged(int64_t& lastCheck);
+ sol::table asTable();
+
+ LuaStorage* mStorage;
+ std::string mSectionName;
+ std::map<std::string, Value, std::less<>> mValues;
+ bool mPermanent = true;
+ int64_t mChangeCounter = 0;
+ static Value sEmpty;
+ };
+ struct SectionMutableView
+ {
+ Section* mSection = nullptr;
+ int64_t mLastCheck = 0;
+ };
+ struct SectionReadOnlyView
+ {
+ Section* mSection = nullptr;
+ int64_t mLastCheck = 0;
+ };
+
+ Section* getSection(std::string_view sectionName);
+
+ lua_State* mLua;
+ std::map<std::string_view, std::unique_ptr<Section>> mData;
+ std::optional<ListenerFn> mListener;
+ };
+
+}
+
+#endif // COMPONENTS_LUA_STORAGE_H
diff --git a/components/lua/utilpackage.cpp b/components/lua/utilpackage.cpp
index 17cb64461a..abb680a6bc 100644
--- a/components/lua/utilpackage.cpp
+++ b/components/lua/utilpackage.cpp
@@ -105,9 +105,9 @@ namespace LuaUtil
[](const Vec3& v) { return TransformM{osg::Matrixf::scale(v)}; },
[](float x, float y, float z) { return TransformM{osg::Matrixf::scale(x, y, z)}; });
transforms["rotate"] = [](float angle, const Vec3& axis) { return TransformQ{osg::Quat(angle, axis)}; };
- transforms["rotateX"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(1, 0, 0))}; };
- transforms["rotateY"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, 1, 0))}; };
- transforms["rotateZ"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, 0, 1))}; };
+ transforms["rotateX"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(-1, 0, 0))}; };
+ transforms["rotateY"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, -1, 0))}; };
+ transforms["rotateZ"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, 0, -1))}; };
transMType[sol::meta_function::multiplication] = sol::overload(
[](const TransformM& a, const Vec3& b) { return a.mM.preMult(b); },
@@ -175,6 +175,7 @@ namespace LuaUtil
util["clamp"] = [](float value, float from, float to) { return std::clamp(value, from, to); };
// NOTE: `util["clamp"] = std::clamp<float>` causes error 'AddressSanitizer: stack-use-after-scope'
util["normalizeAngle"] = &Misc::normalizeAngle;
+ util["makeReadOnly"] = &makeReadOnly;
return util;
}
diff --git a/components/lua_ui/content.cpp b/components/lua_ui/content.cpp
new file mode 100644
index 0000000000..6f9cf61f2f
--- /dev/null
+++ b/components/lua_ui/content.cpp
@@ -0,0 +1,106 @@
+#include "content.hpp"
+
+namespace LuaUi
+{
+ Content::Content(const sol::table& table)
+ {
+ size_t size = table.size();
+ for (size_t index = 0; index < size; ++index)
+ {
+ sol::object value = table.get<sol::object>(index + 1);
+ if (value.is<sol::table>())
+ assign(index, value.as<sol::table>());
+ else
+ throw std::logic_error("UI Content children must all be tables.");
+ }
+ }
+
+ void Content::assign(size_t index, const sol::table& table)
+ {
+ if (mOrdered.size() < index)
+ throw std::logic_error("Can't have gaps in UI Content.");
+ if (index == mOrdered.size())
+ mOrdered.push_back(table);
+ else
+ {
+ sol::optional<std::string> oldName = mOrdered[index]["name"];
+ if (oldName.has_value())
+ mNamed.erase(oldName.value());
+ mOrdered[index] = table;
+ }
+ sol::optional<std::string> name = table["name"];
+ if (name.has_value())
+ mNamed[name.value()] = index;
+ }
+
+ void Content::assign(std::string_view name, const sol::table& table)
+ {
+ auto it = mNamed.find(name);
+ if (it != mNamed.end())
+ assign(it->second, table);
+ else
+ throw std::logic_error(std::string("Can't find a UI Content child with name ") += name);
+ }
+
+ void Content::insert(size_t index, const sol::table& table)
+ {
+ size_t size = mOrdered.size();
+ if (size < index)
+ throw std::logic_error("Can't have gaps in UI Content.");
+ mOrdered.insert(mOrdered.begin() + index, table);
+ for (size_t i = index; i < size; ++i)
+ {
+ sol::optional<std::string> name = mOrdered[i]["name"];
+ if (name.has_value())
+ mNamed[name.value()] = index;
+ }
+ }
+
+ sol::table Content::at(size_t index) const
+ {
+ if (index > size())
+ throw std::logic_error("Invalid UI Content index.");
+ return mOrdered.at(index);
+ }
+
+ sol::table Content::at(std::string_view name) const
+ {
+ auto it = mNamed.find(name);
+ if (it == mNamed.end())
+ throw std::logic_error("Invalid UI Content name.");
+ return mOrdered.at(it->second);
+ }
+
+ size_t Content::remove(size_t index)
+ {
+ sol::table table = at(index);
+ sol::optional<std::string> name = table["name"];
+ if (name.has_value())
+ {
+ auto it = mNamed.find(name.value());
+ if (it != mNamed.end())
+ mNamed.erase(it);
+ }
+ mOrdered.erase(mOrdered.begin() + index);
+ return index;
+ }
+
+ size_t Content::remove(std::string_view name)
+ {
+ auto it = mNamed.find(name);
+ if (it == mNamed.end())
+ throw std::logic_error("Invalid UI Content name.");
+ size_t index = it->second;
+ remove(index);
+ return index;
+ }
+
+ size_t Content::indexOf(const sol::table& table)
+ {
+ auto it = std::find(mOrdered.begin(), mOrdered.end(), table);
+ if (it == mOrdered.end())
+ return size();
+ else
+ return it - mOrdered.begin();
+ }
+}
diff --git a/components/lua_ui/content.hpp b/components/lua_ui/content.hpp
new file mode 100644
index 0000000000..e970744b3d
--- /dev/null
+++ b/components/lua_ui/content.hpp
@@ -0,0 +1,41 @@
+#ifndef COMPONENTS_LUAUI_CONTENT
+#define COMPONENTS_LUAUI_CONTENT
+
+#include <map>
+#include <string>
+
+#include <sol/sol.hpp>
+
+namespace LuaUi
+{
+ class Content
+ {
+ public:
+ using iterator = std::vector<sol::table>::iterator;
+
+ Content() {}
+
+ // expects a Lua array - a table with keys from 1 to n without any nil values in between
+ // any other keys are ignored
+ explicit Content(const sol::table&);
+
+ size_t size() const { return mOrdered.size(); }
+
+ void assign(std::string_view name, const sol::table& table);
+ void assign(size_t index, const sol::table& table);
+ void insert(size_t index, const sol::table& table);
+
+ sol::table at(size_t index) const;
+ sol::table at(std::string_view name) const;
+ size_t remove(size_t index);
+ size_t remove(std::string_view name);
+ size_t indexOf(const sol::table& table);
+
+ private:
+ std::map<std::string, size_t, std::less<>> mNamed;
+ std::vector<sol::table> mOrdered;
+ };
+
+}
+
+#endif // COMPONENTS_LUAUI_CONTENT
diff --git a/components/lua_ui/element.cpp b/components/lua_ui/element.cpp
new file mode 100644
index 0000000000..5147068a7e
--- /dev/null
+++ b/components/lua_ui/element.cpp
@@ -0,0 +1,165 @@
+#include "element.hpp"
+
+#include <MyGUI_Gui.h>
+
+#include "content.hpp"
+#include "widgetlist.hpp"
+
+namespace LuaUi
+{
+
+ std::string widgetType(const sol::table& layout)
+ {
+ return layout.get_or("type", std::string("LuaWidget"));
+ }
+
+ Content content(const sol::table& layout)
+ {
+ auto optional = layout.get<sol::optional<Content>>("content");
+ if (optional.has_value())
+ return optional.value();
+ else
+ return Content();
+ }
+
+ void setProperties(LuaUi::WidgetExtension* ext, const sol::table& layout)
+ {
+ ext->setProperties(layout.get<sol::object>("props"));
+ }
+
+ void setEventCallbacks(LuaUi::WidgetExtension* ext, const sol::table& layout)
+ {
+ ext->clearCallbacks();
+ auto events = layout.get<sol::optional<sol::table>>("events");
+ if (events.has_value())
+ {
+ events.value().for_each([ext](const sol::object& name, const sol::object& callback)
+ {
+ if (name.is<std::string>() && callback.is<LuaUtil::Callback>())
+ ext->setCallback(name.as<std::string>(), callback.as<LuaUtil::Callback>());
+ else if (!name.is<std::string>())
+ Log(Debug::Warning) << "UI event key must be a string";
+ else if (!callback.is<LuaUtil::Callback>())
+ Log(Debug::Warning) << "UI event handler for key \"" << name.as<std::string>()
+ << "\" must be an openmw.async.callback";
+ });
+ }
+ }
+
+ void setLayout(LuaUi::WidgetExtension* ext, const sol::table& layout)
+ {
+ ext->setLayout(layout);
+ }
+
+ LuaUi::WidgetExtension* createWidget(const sol::table& layout, LuaUi::WidgetExtension* parent)
+ {
+ std::string type = widgetType(layout);
+ std::string skin = layout.get_or("skin", std::string());
+ std::string name = layout.get_or("name", std::string());
+
+ static auto widgetTypeMap = widgetTypeToName();
+ if (widgetTypeMap.find(type) == widgetTypeMap.end())
+ throw std::logic_error(std::string("Invalid widget type ") += type);
+
+ MyGUI::Widget* widget = MyGUI::Gui::getInstancePtr()->createWidgetT(
+ type, skin,
+ MyGUI::IntCoord(), MyGUI::Align::Default,
+ std::string(), name);
+
+ LuaUi::WidgetExtension* ext = dynamic_cast<LuaUi::WidgetExtension*>(widget);
+ if (!ext)
+ throw std::runtime_error("Invalid widget!");
+
+ ext->create(layout.lua_state(), widget);
+ if (parent != nullptr)
+ widget->attachToWidget(parent->widget());
+
+ setEventCallbacks(ext, layout);
+ setProperties(ext, layout);
+ setLayout(ext, layout);
+
+ Content cont = content(layout);
+ for (size_t i = 0; i < cont.size(); i++)
+ ext->addChild(createWidget(cont.at(i), ext));
+
+ return ext;
+ }
+
+ void destroyWidget(LuaUi::WidgetExtension* ext)
+ {
+ ext->destroy();
+ MyGUI::Gui::getInstancePtr()->destroyWidget(ext->widget());
+ }
+
+ void updateWidget(const sol::table& layout, LuaUi::WidgetExtension* ext)
+ {
+ setEventCallbacks(ext, layout);
+ setProperties(ext, layout);
+ setLayout(ext, layout);
+
+ Content newContent = content(layout);
+
+ size_t oldSize = ext->childCount();
+ size_t newSize = newContent.size();
+ size_t minSize = std::min(oldSize, newSize);
+ for (size_t i = 0; i < minSize; i++)
+ {
+ LuaUi::WidgetExtension* oldWidget = ext->childAt(i);
+ sol::table newChild = newContent.at(i);
+
+ if (oldWidget->widget()->getTypeName() != widgetType(newChild))
+ {
+ destroyWidget(oldWidget);
+ ext->assignChild(i, createWidget(newChild, ext));
+ }
+ else
+ updateWidget(newChild, oldWidget);
+ }
+
+ for (size_t i = minSize; i < oldSize; i++)
+ destroyWidget(ext->eraseChild(i));
+
+ for (size_t i = minSize; i < newSize; i++)
+ ext->addChild(createWidget(newContent.at(i), ext));
+ }
+
+ void setLayer(const sol::table& layout, LuaUi::WidgetExtension* ext)
+ {
+ MyGUI::ILayer* layerNode = ext->widget()->getLayer();
+ std::string currentLayer = layerNode ? layerNode->getName() : std::string();
+ std::string newLayer = layout.get_or("layer", std::string());
+ if (!newLayer.empty() && !MyGUI::LayerManager::getInstance().isExist(newLayer))
+ throw std::logic_error(std::string("Layer ") += newLayer += " doesn't exist");
+ else if (newLayer != currentLayer)
+ {
+ MyGUI::LayerManager::getInstance().attachToLayerNode(newLayer, ext->widget());
+ }
+ }
+
+ void Element::create()
+ {
+ assert(!mRoot);
+ if (!mRoot)
+ {
+ mRoot = createWidget(mLayout, nullptr);
+ setLayer(mLayout, mRoot);
+ }
+ }
+
+ void Element::update()
+ {
+ if (mRoot && mUpdate)
+ {
+ updateWidget(mLayout, mRoot);
+ setLayer(mLayout, mRoot);
+ }
+ mUpdate = false;
+ }
+
+ void Element::destroy()
+ {
+ if (mRoot)
+ destroyWidget(mRoot);
+ mRoot = nullptr;
+ }
+}
diff --git a/components/lua_ui/element.hpp b/components/lua_ui/element.hpp
new file mode 100644
index 0000000000..10e10d8960
--- /dev/null
+++ b/components/lua_ui/element.hpp
@@ -0,0 +1,31 @@
+#ifndef OPENMW_LUAUI_ELEMENT
+#define OPENMW_LUAUI_ELEMENT
+
+#include "widget.hpp"
+
+namespace LuaUi
+{
+ struct Element
+ {
+ Element(sol::table layout)
+ : mRoot{ nullptr }
+ , mLayout{ layout }
+ , mUpdate{ false }
+ , mDestroy{ false }
+ {
+ }
+
+ LuaUi::WidgetExtension* mRoot;
+ sol::table mLayout;
+ bool mUpdate;
+ bool mDestroy;
+
+ void create();
+
+ void update();
+
+ void destroy();
+ };
+}
+
+#endif // !OPENMW_LUAUI_ELEMENT
diff --git a/components/lua_ui/layers.hpp b/components/lua_ui/layers.hpp
new file mode 100644
index 0000000000..6fe7fc9c16
--- /dev/null
+++ b/components/lua_ui/layers.hpp
@@ -0,0 +1,55 @@
+#ifndef OPENMW_LUAUI_LAYERS
+#define OPENMW_LUAUI_LAYERS
+
+#include <string>
+#include <string_view>
+
+#include <MyGUI_LayerManager.h>
+#include <MyGUI_OverlappedLayer.h>
+
+namespace LuaUi
+{
+ namespace Layers
+ {
+ struct Options {
+ bool mInteractive;
+ };
+
+ size_t size()
+ {
+ return MyGUI::LayerManager::getInstance().getLayerCount();
+ }
+
+ std::string at(size_t index)
+ {
+ if (index >= size())
+ throw std::logic_error("Invalid layer index");
+ return MyGUI::LayerManager::getInstance().getLayer(index)->getName();
+ }
+
+ size_t indexOf(std::string_view name)
+ {
+ for (size_t i = 0; i < size(); i++)
+ if (at(i) == name)
+ return i;
+ return size();
+ }
+
+ void insert(size_t index, std::string_view name, Options options)
+ {
+ if (index > size())
+ throw std::logic_error("Invalid layer index");
+ if (indexOf(name) < size())
+ Log(Debug::Error) << "Layer \"" << name << "\" already exists";
+ else
+ {
+ auto layer = MyGUI::LayerManager::getInstance()
+ .createLayerAt(std::string(name), "OverlappedLayer", index);
+ auto overlappedLayer = dynamic_cast<MyGUI::OverlappedLayer*>(layer);
+ overlappedLayer->setPick(options.mInteractive);
+ }
+ }
+ }
+}
+
+#endif // OPENMW_LUAUI_LAYERS
diff --git a/components/lua_ui/properties.hpp b/components/lua_ui/properties.hpp
new file mode 100644
index 0000000000..7f23a5ce6c
--- /dev/null
+++ b/components/lua_ui/properties.hpp
@@ -0,0 +1,64 @@
+#ifndef OPENMW_LUAUI_PROPERTIES
+#define OPENMW_LUAUI_PROPERTIES
+
+#include <sol/sol.hpp>
+#include <MyGUI_Types.h>
+#include <osg/Vec2>
+
+#include <components/lua/luastate.hpp>
+
+namespace LuaUi
+{
+ template <typename T>
+ sol::optional<T> getProperty(sol::object from, std::string_view field) {
+ sol::object value = LuaUtil::getFieldOrNil(from, field);
+ if (value == sol::nil)
+ return sol::nullopt;
+ if (value.is<T>())
+ return value.as<T>();
+ std::string error("Property \"");
+ error += field;
+ error += "\" has an invalid value \"";
+ error += LuaUtil::toString(value);
+ error += "\"";
+ throw std::logic_error(error);
+ }
+
+ template<typename T>
+ T parseProperty(sol::object from, std::string_view field, const T& defaultValue)
+ {
+ sol::optional<T> opt = getProperty<T>(from, field);
+ if (opt.has_value())
+ return opt.value();
+ else
+ return defaultValue;
+ }
+
+ template <typename T>
+ MyGUI::types::TPoint<T> parseProperty(
+ sol::object from,
+ std::string_view field,
+ const MyGUI::types::TPoint<T>& defaultValue)
+ {
+ auto v = getProperty<osg::Vec2f>(from, field);
+ if (v.has_value())
+ return MyGUI::types::TPoint<T>(v.value().x(), v.value().y());
+ else
+ return defaultValue;
+ }
+
+ template <typename T>
+ MyGUI::types::TSize<T> parseProperty(
+ sol::object from,
+ std::string_view field,
+ const MyGUI::types::TSize<T>& defaultValue)
+ {
+ auto v = getProperty<osg::Vec2f>(from, field);
+ if (v.has_value())
+ return MyGUI::types::TSize<T>(v.value().x(), v.value().y());
+ else
+ return defaultValue;
+ }
+}
+
+#endif // !OPENMW_LUAUI_PROPERTIES
diff --git a/components/lua_ui/text.cpp b/components/lua_ui/text.cpp
new file mode 100644
index 0000000000..4ae9865ac3
--- /dev/null
+++ b/components/lua_ui/text.cpp
@@ -0,0 +1,29 @@
+
+#include "text.hpp"
+
+namespace LuaUi
+{
+ LuaText::LuaText()
+ : mAutoSized(true)
+ {}
+
+ void LuaText::initialize()
+ {
+ WidgetExtension::initialize();
+ }
+
+ void LuaText::setProperties(sol::object props)
+ {
+ setCaption(parseProperty(props, "caption", std::string()));
+ mAutoSized = parseProperty(props, "autoSize", true);
+ WidgetExtension::setProperties(props);
+ }
+
+ MyGUI::IntSize LuaText::calculateSize()
+ {
+ if (mAutoSized)
+ return getTextSize();
+ else
+ return WidgetExtension::calculateSize();
+ }
+}
diff --git a/components/lua_ui/text.hpp b/components/lua_ui/text.hpp
new file mode 100644
index 0000000000..d87a9001a2
--- /dev/null
+++ b/components/lua_ui/text.hpp
@@ -0,0 +1,27 @@
+#ifndef OPENMW_LUAUI_TEXT
+#define OPENMW_LUAUI_TEXT
+
+#include <MyGUI_TextBox.h>
+
+#include "widget.hpp"
+
+namespace LuaUi
+{
+ class LuaText : public MyGUI::TextBox, public WidgetExtension
+ {
+ MYGUI_RTTI_DERIVED(LuaText)
+
+ public:
+ LuaText();
+ virtual void initialize() override;
+ virtual void setProperties(sol::object) override;
+
+ private:
+ bool mAutoSized;
+
+ protected:
+ virtual MyGUI::IntSize calculateSize() override;
+ };
+}
+
+#endif // OPENMW_LUAUI_TEXT
diff --git a/components/lua_ui/textedit.cpp b/components/lua_ui/textedit.cpp
new file mode 100644
index 0000000000..9cdc716ce4
--- /dev/null
+++ b/components/lua_ui/textedit.cpp
@@ -0,0 +1,10 @@
+#include "textedit.hpp"
+
+namespace LuaUi
+{
+ void LuaTextEdit::setProperties(sol::object props)
+ {
+ setCaption(parseProperty(props, "caption", std::string()));
+ WidgetExtension::setProperties(props);
+ }
+}
diff --git a/components/lua_ui/textedit.hpp b/components/lua_ui/textedit.hpp
new file mode 100644
index 0000000000..d14f54d659
--- /dev/null
+++ b/components/lua_ui/textedit.hpp
@@ -0,0 +1,18 @@
+#ifndef OPENMW_LUAUI_TEXTEDIT
+#define OPENMW_LUAUI_TEXTEDIT
+
+#include <MyGUI_EditBox.h>
+
+#include "widget.hpp"
+
+namespace LuaUi
+{
+ class LuaTextEdit : public MyGUI::EditBox, public WidgetExtension
+ {
+ MYGUI_RTTI_DERIVED(LuaTextEdit)
+
+ virtual void setProperties(sol::object) override;
+ };
+}
+
+#endif // OPENMW_LUAUI_TEXTEDIT
diff --git a/components/lua_ui/widget.cpp b/components/lua_ui/widget.cpp
new file mode 100644
index 0000000000..6d7bb5063c
--- /dev/null
+++ b/components/lua_ui/widget.cpp
@@ -0,0 +1,264 @@
+#include "widget.hpp"
+
+#include <SDL_events.h>
+#include <components/sdlutil/sdlmappings.hpp>
+
+#include "text.hpp"
+#include "textedit.hpp"
+#include "window.hpp"
+
+namespace LuaUi
+{
+ WidgetExtension::WidgetExtension()
+ : mForcedCoord()
+ , mAbsoluteCoord()
+ , mRelativeCoord()
+ , mAnchor()
+ , mLua{ nullptr }
+ , mWidget{ nullptr }
+ , mLayout{ sol::nil }
+ {}
+
+ void WidgetExtension::triggerEvent(std::string_view name, const sol::object& argument = sol::nil) const
+ {
+ auto it = mCallbacks.find(name);
+ if (it != mCallbacks.end())
+ it->second(argument, mLayout);
+ }
+
+ void WidgetExtension::create(lua_State* lua, MyGUI::Widget* self)
+ {
+ mLua = lua;
+ mWidget = self;
+
+ mWidget->eventChangeCoord += MyGUI::newDelegate(this, &WidgetExtension::updateChildrenCoord);
+
+ initialize();
+ }
+
+ void WidgetExtension::initialize()
+ {
+ // \todo might be more efficient to only register these if there are Lua callbacks
+ mWidget->eventKeyButtonPressed += MyGUI::newDelegate(this, &WidgetExtension::keyPress);
+ mWidget->eventKeyButtonReleased += MyGUI::newDelegate(this, &WidgetExtension::keyRelease);
+ mWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &WidgetExtension::mouseClick);
+ mWidget->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &WidgetExtension::mouseDoubleClick);
+ mWidget->eventMouseButtonPressed += MyGUI::newDelegate(this, &WidgetExtension::mousePress);
+ mWidget->eventMouseButtonReleased += MyGUI::newDelegate(this, &WidgetExtension::mouseRelease);
+ mWidget->eventMouseMove += MyGUI::newDelegate(this, &WidgetExtension::mouseMove);
+ mWidget->eventMouseDrag += MyGUI::newDelegate(this, &WidgetExtension::mouseDrag);
+
+ mWidget->eventMouseSetFocus += MyGUI::newDelegate(this, &WidgetExtension::focusGain);
+ mWidget->eventMouseLostFocus += MyGUI::newDelegate(this, &WidgetExtension::focusLoss);
+ mWidget->eventKeySetFocus += MyGUI::newDelegate(this, &WidgetExtension::focusGain);
+ mWidget->eventKeyLostFocus += MyGUI::newDelegate(this, &WidgetExtension::focusLoss);
+ }
+
+ void WidgetExtension::destroy()
+ {
+ clearCallbacks();
+ deinitialize();
+
+ for (WidgetExtension* child : mContent)
+ child->destroy();
+ }
+
+ void WidgetExtension::deinitialize()
+ {
+ mWidget->eventKeyButtonPressed.clear();
+ mWidget->eventKeyButtonReleased.clear();
+ mWidget->eventMouseButtonClick.clear();
+ mWidget->eventMouseButtonDoubleClick.clear();
+ mWidget->eventMouseButtonPressed.clear();
+ mWidget->eventMouseButtonReleased.clear();
+ mWidget->eventMouseMove.clear();
+ mWidget->eventMouseDrag.m_event.clear();
+
+ mWidget->eventMouseSetFocus.clear();
+ mWidget->eventMouseLostFocus.clear();
+ mWidget->eventKeySetFocus.clear();
+ mWidget->eventKeyLostFocus.clear();
+ }
+
+ sol::table WidgetExtension::makeTable() const
+ {
+ return sol::table(mLua, sol::create);
+ }
+
+ sol::object WidgetExtension::keyEvent(MyGUI::KeyCode code) const
+ {
+ SDL_Keysym keySym;
+ keySym.sym = SDLUtil::myGuiKeyToSdl(code);
+ keySym.scancode = SDL_GetScancodeFromKey(keySym.sym);
+ keySym.mod = SDL_GetModState();
+ return sol::make_object(mLua, keySym);
+ }
+
+ sol::object WidgetExtension::mouseEvent(int left, int top, MyGUI::MouseButton button = MyGUI::MouseButton::None) const
+ {
+ auto position = osg::Vec2f(left, top);
+ auto absolutePosition = mWidget->getAbsolutePosition();
+ auto offset = position - osg::Vec2f(absolutePosition.left, absolutePosition.top);
+ sol::table table = makeTable();
+ table["position"] = position;
+ table["offset"] = offset;
+ table["button"] = SDLUtil::myGuiMouseButtonToSdl(button);
+ return table;
+ }
+
+ void WidgetExtension::addChild(WidgetExtension* ext)
+ {
+ mContent.push_back(ext);
+ }
+
+ WidgetExtension* WidgetExtension::childAt(size_t index) const
+ {
+ return mContent.at(index);
+ }
+
+ void WidgetExtension::assignChild(size_t index, WidgetExtension* ext)
+ {
+ if (mContent.size() <= index)
+ throw std::logic_error("Invalid widget child index");
+ mContent[index] = ext;
+ }
+
+ WidgetExtension* WidgetExtension::eraseChild(size_t index)
+ {
+ if (mContent.size() <= index)
+ throw std::logic_error("Invalid widget child index");
+ auto it = mContent.begin() + index;
+ WidgetExtension* ext = *it;
+ mContent.erase(it);
+ return ext;
+ }
+
+ void WidgetExtension::setCallback(const std::string& name, const LuaUtil::Callback& callback)
+ {
+ mCallbacks[name] = callback;
+ }
+
+ void WidgetExtension::clearCallbacks()
+ {
+ mCallbacks.clear();
+ }
+
+ MyGUI::IntCoord WidgetExtension::forcedCoord()
+ {
+ return mForcedCoord;
+ }
+
+ void WidgetExtension::setForcedCoord(const MyGUI::IntCoord& offset)
+ {
+ mForcedCoord = offset;
+ }
+
+ void WidgetExtension::updateCoord()
+ {
+ mWidget->setCoord(calculateCoord());
+ }
+
+ void WidgetExtension::setProperties(sol::object props)
+ {
+ mAbsoluteCoord = parseProperty(props, "position", MyGUI::IntPoint());
+ mAbsoluteCoord = parseProperty(props, "size", MyGUI::IntSize());
+ mRelativeCoord = parseProperty(props, "relativePosition", MyGUI::FloatPoint());
+ mRelativeCoord = parseProperty(props, "relativeSize", MyGUI::FloatSize());
+ mAnchor = parseProperty(props, "anchor", MyGUI::FloatSize());
+ mWidget->setVisible(parseProperty(props, "visible", true));
+
+ updateCoord();
+ }
+
+ void WidgetExtension::updateChildrenCoord(MyGUI::Widget* _widget)
+ {
+ for (auto& child : mContent)
+ child->updateCoord();
+ }
+
+ MyGUI::IntSize WidgetExtension::calculateSize()
+ {
+ const MyGUI::IntSize& parentSize = mWidget->getParentSize();
+ MyGUI::IntSize newSize;
+ newSize = mAbsoluteCoord.size() + mForcedCoord.size();
+ newSize.width += mRelativeCoord.width * parentSize.width;
+ newSize.height += mRelativeCoord.height * parentSize.height;
+ return newSize;
+ }
+
+ MyGUI::IntPoint WidgetExtension::calculatePosition(const MyGUI::IntSize& size)
+ {
+ const MyGUI::IntSize& parentSize = mWidget->getParentSize();
+ MyGUI::IntPoint newPosition;
+ newPosition = mAbsoluteCoord.point() + mForcedCoord.point();
+ newPosition.left += mRelativeCoord.left * parentSize.width - mAnchor.width * size.width;
+ newPosition.top += mRelativeCoord.top * parentSize.height - mAnchor.height * size.height;
+ return newPosition;
+ }
+
+ MyGUI::IntCoord WidgetExtension::calculateCoord()
+ {
+ MyGUI::IntCoord newCoord;
+ newCoord = calculateSize();
+ newCoord = calculatePosition(newCoord.size());
+ return newCoord;
+ }
+
+ void WidgetExtension::keyPress(MyGUI::Widget*, MyGUI::KeyCode code, MyGUI::Char ch)
+ {
+ if (code == MyGUI::KeyCode::None)
+ {
+ // \todo decide how to handle unicode strings in Lua
+ MyGUI::UString uString;
+ uString.push_back(static_cast<MyGUI::UString::unicode_char>(ch));
+ triggerEvent("textInput", sol::make_object(mLua, uString.asUTF8()));
+ }
+ else
+ triggerEvent("keyPress", keyEvent(code));
+ }
+
+ void WidgetExtension::keyRelease(MyGUI::Widget*, MyGUI::KeyCode code)
+ {
+ triggerEvent("keyRelease", keyEvent(code));
+ }
+
+ void WidgetExtension::mouseMove(MyGUI::Widget*, int left, int top)
+ {
+ triggerEvent("mouseMove", mouseEvent(left, top));
+ }
+
+ void WidgetExtension::mouseDrag(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button)
+ {
+ triggerEvent("mouseMove", mouseEvent(left, top, button));
+ }
+
+ void WidgetExtension::mouseClick(MyGUI::Widget* _widget)
+ {
+ triggerEvent("mouseClick");
+ }
+
+ void WidgetExtension::mouseDoubleClick(MyGUI::Widget* _widget)
+ {
+ triggerEvent("mouseDoubleClick");
+ }
+
+ void WidgetExtension::mousePress(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button)
+ {
+ triggerEvent("mousePress", mouseEvent(left, top, button));
+ }
+
+ void WidgetExtension::mouseRelease(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button)
+ {
+ triggerEvent("mouseRelease", mouseEvent(left, top, button));
+ }
+
+ void WidgetExtension::focusGain(MyGUI::Widget*, MyGUI::Widget*)
+ {
+ triggerEvent("focusGain");
+ }
+
+ void WidgetExtension::focusLoss(MyGUI::Widget*, MyGUI::Widget*)
+ {
+ triggerEvent("focusLoss");
+ }
+}
diff --git a/components/lua_ui/widget.hpp b/components/lua_ui/widget.hpp
new file mode 100644
index 0000000000..99d430f71c
--- /dev/null
+++ b/components/lua_ui/widget.hpp
@@ -0,0 +1,99 @@
+#ifndef OPENMW_LUAUI_WIDGET
+#define OPENMW_LUAUI_WIDGET
+
+#include <map>
+
+#include <MyGUI_Widget.h>
+#include <sol/sol.hpp>
+
+#include <components/lua/scriptscontainer.hpp>
+
+#include "properties.hpp"
+
+namespace LuaUi
+{
+ /*
+ * extends MyGUI::Widget and its child classes
+ * memory ownership is controlled by MyGUI
+ * it is important not to call any WidgetExtension methods after destroying the MyGUI::Widget
+ */
+ class WidgetExtension
+ {
+ public:
+ WidgetExtension();
+ // must be called after creating the underlying MyGUI::Widget
+ void create(lua_State* lua, MyGUI::Widget* self);
+ // must be called after before destroying the underlying MyGUI::Widget
+ void destroy();
+
+ void addChild(WidgetExtension* ext);
+ WidgetExtension* childAt(size_t index) const;
+ void assignChild(size_t index, WidgetExtension* ext);
+ WidgetExtension* eraseChild(size_t index);
+ size_t childCount() const { return mContent.size(); }
+
+ MyGUI::Widget* widget() const { return mWidget; }
+
+ void setCallback(const std::string&, const LuaUtil::Callback&);
+ void clearCallbacks();
+
+ virtual void setProperties(sol::object);
+
+ MyGUI::IntCoord forcedCoord();
+ void setForcedCoord(const MyGUI::IntCoord& offset);
+ void updateCoord();
+
+ void setLayout(const sol::table& layout) { mLayout = layout; }
+
+ protected:
+ sol::table makeTable() const;
+ sol::object keyEvent(MyGUI::KeyCode) const;
+ sol::object mouseEvent(int left, int top, MyGUI::MouseButton button) const;
+ virtual void initialize();
+ virtual void deinitialize();
+ virtual MyGUI::IntSize calculateSize();
+ virtual MyGUI::IntPoint calculatePosition(const MyGUI::IntSize& size);
+ MyGUI::IntCoord calculateCoord();
+
+ void triggerEvent(std::string_view name, const sol::object& argument) const;
+
+ // offsets the position and size, used only in C++ widget code
+ MyGUI::IntCoord mForcedCoord;
+ // position and size in pixels
+ MyGUI::IntCoord mAbsoluteCoord;
+ // position and size as a ratio of parent size
+ MyGUI::FloatCoord mRelativeCoord;
+ // negative position offset as a ratio of this widget's size
+ // used in combination with relative coord to align the widget, e. g. center it
+ MyGUI::FloatSize mAnchor;
+
+ private:
+ // use lua_State* instead of sol::state_view because MyGUI requires a default constructor
+ lua_State* mLua;
+ MyGUI::Widget* mWidget;
+
+ std::vector<WidgetExtension*> mContent;
+ std::map<std::string, LuaUtil::Callback, std::less<>> mCallbacks;
+ sol::table mLayout;
+
+ void updateChildrenCoord(MyGUI::Widget*);
+
+ void keyPress(MyGUI::Widget*, MyGUI::KeyCode, MyGUI::Char);
+ void keyRelease(MyGUI::Widget*, MyGUI::KeyCode);
+ void mouseMove(MyGUI::Widget*, int, int);
+ void mouseDrag(MyGUI::Widget*, int, int, MyGUI::MouseButton);
+ void mouseClick(MyGUI::Widget*);
+ void mouseDoubleClick(MyGUI::Widget*);
+ void mousePress(MyGUI::Widget*, int, int, MyGUI::MouseButton);
+ void mouseRelease(MyGUI::Widget*, int, int, MyGUI::MouseButton);
+ void focusGain(MyGUI::Widget*, MyGUI::Widget*);
+ void focusLoss(MyGUI::Widget*, MyGUI::Widget*);
+ };
+
+ class LuaWidget : public MyGUI::Widget, public WidgetExtension
+ {
+ MYGUI_RTTI_DERIVED(LuaWidget)
+ };
+}
+
+#endif // !OPENMW_LUAUI_WIDGET
diff --git a/components/lua_ui/widgetlist.cpp b/components/lua_ui/widgetlist.cpp
new file mode 100644
index 0000000000..c2a9bef990
--- /dev/null
+++ b/components/lua_ui/widgetlist.cpp
@@ -0,0 +1,31 @@
+#include "widgetlist.hpp"
+
+#include <MyGUI_FactoryManager.h>
+
+#include "widget.hpp"
+#include "text.hpp"
+#include "textedit.hpp"
+#include "window.hpp"
+
+namespace LuaUi
+{
+
+ void registerAllWidgets()
+ {
+ MyGUI::FactoryManager::getInstance().registerFactory<LuaWidget>("Widget");
+ MyGUI::FactoryManager::getInstance().registerFactory<LuaText>("Widget");
+ MyGUI::FactoryManager::getInstance().registerFactory<LuaTextEdit>("Widget");
+ MyGUI::FactoryManager::getInstance().registerFactory<LuaWindow>("Widget");
+ }
+
+ const std::unordered_map<std::string, std::string>& widgetTypeToName()
+ {
+ static std::unordered_map<std::string, std::string> types{
+ { "LuaWidget", "Widget" },
+ { "LuaText", "Text" },
+ { "LuaTextEdit", "TextEdit" },
+ { "LuaWindow", "Window" },
+ };
+ return types;
+ }
+}
diff --git a/components/lua_ui/widgetlist.hpp b/components/lua_ui/widgetlist.hpp
new file mode 100644
index 0000000000..ff033fb6ca
--- /dev/null
+++ b/components/lua_ui/widgetlist.hpp
@@ -0,0 +1,14 @@
+#ifndef OPENMW_LUAUI_WIDGETLIST
+#define OPENMW_LUAUI_WIDGETLIST
+
+#include <unordered_map>
+#include <string>
+
+namespace LuaUi
+{
+ void registerAllWidgets();
+
+ const std::unordered_map<std::string, std::string>& widgetTypeToName();
+}
+
+#endif // OPENMW_LUAUI_WIDGETLIST
diff --git a/components/lua_ui/window.cpp b/components/lua_ui/window.cpp
new file mode 100644
index 0000000000..5d34bf8212
--- /dev/null
+++ b/components/lua_ui/window.cpp
@@ -0,0 +1,96 @@
+#include "window.hpp"
+
+#include <MyGUI_InputManager.h>
+
+namespace LuaUi
+{
+ LuaWindow::LuaWindow()
+ : mCaption()
+ , mPreviousMouse()
+ , mChangeScale()
+ , mMoveResize()
+ {}
+
+ void LuaWindow::initialize()
+ {
+ WidgetExtension::initialize();
+
+ assignWidget(mCaption, "Caption");
+ if (mCaption)
+ {
+ mCaption->eventMouseButtonPressed += MyGUI::newDelegate(this, &LuaWindow::notifyMousePress);
+ mCaption->eventMouseDrag += MyGUI::newDelegate(this, &LuaWindow::notifyMouseDrag);
+ }
+ for (auto w : getSkinWidgetsByName("Action"))
+ {
+ w->eventMouseButtonPressed += MyGUI::newDelegate(this, &LuaWindow::notifyMousePress);
+ w->eventMouseDrag += MyGUI::newDelegate(this, &LuaWindow::notifyMouseDrag);
+ }
+ }
+
+ void LuaWindow::deinitialize()
+ {
+ WidgetExtension::deinitialize();
+
+ if (mCaption)
+ {
+ mCaption->eventMouseButtonPressed.clear();
+ mCaption->eventMouseDrag.m_event.clear();
+ }
+ for (auto w : getSkinWidgetsByName("Action"))
+ {
+ w->eventMouseButtonPressed.clear();
+ w->eventMouseDrag.m_event.clear();
+ }
+ }
+
+ void LuaWindow::setProperties(sol::object props)
+ {
+ if (mCaption)
+ mCaption->setCaption(parseProperty(props, "caption", std::string()));
+ mMoveResize = MyGUI::IntCoord();
+ setForcedCoord(mMoveResize);
+ WidgetExtension::setProperties(props);
+ }
+
+ void LuaWindow::notifyMousePress(MyGUI::Widget* sender, int left, int top, MyGUI::MouseButton id)
+ {
+ if (id != MyGUI::MouseButton::Left)
+ return;
+
+ mPreviousMouse.left = left;
+ mPreviousMouse.top = top;
+
+ if (sender->isUserString("Scale"))
+ mChangeScale = MyGUI::IntCoord::parse(sender->getUserString("Scale"));
+ else
+ mChangeScale = MyGUI::IntCoord(1, 1, 0, 0);
+ }
+
+ void LuaWindow::notifyMouseDrag(MyGUI::Widget* sender, int left, int top, MyGUI::MouseButton id)
+ {
+ if (id != MyGUI::MouseButton::Left)
+ return;
+
+ MyGUI::IntCoord change = mChangeScale;
+ change.left *= (left - mPreviousMouse.left);
+ change.top *= (top - mPreviousMouse.top);
+ change.width *= (left - mPreviousMouse.left);
+ change.height *= (top - mPreviousMouse.top);
+
+ mMoveResize = mMoveResize + change.size();
+ setForcedCoord(mMoveResize);
+ // position can change based on size changes
+ mMoveResize = mMoveResize + change.point() + getPosition() - calculateCoord().point();
+ setForcedCoord(mMoveResize);
+ updateCoord();
+
+ mPreviousMouse.left = left;
+ mPreviousMouse.top = top;
+
+ sol::table table = makeTable();
+ table["position"] = osg::Vec2f(mCoord.left, mCoord.top);
+ table["size"] = osg::Vec2f(mCoord.width, mCoord.height);
+ triggerEvent("windowDrag", table);
+ }
+}
diff --git a/components/lua_ui/window.hpp b/components/lua_ui/window.hpp
new file mode 100644
index 0000000000..f34779c8f7
--- /dev/null
+++ b/components/lua_ui/window.hpp
@@ -0,0 +1,37 @@
+#ifndef OPENMW_LUAUI_WINDOW
+#define OPENMW_LUAUI_WINDOW
+
+#include <optional>
+
+#include <MyGUI_TextBox.h>
+
+#include "widget.hpp"
+
+namespace LuaUi
+{
+ class LuaWindow : public MyGUI::Widget, public WidgetExtension
+ {
+ MYGUI_RTTI_DERIVED(LuaWindow)
+
+ public:
+ LuaWindow();
+ virtual void setProperties(sol::object) override;
+
+ private:
+ // \todo replace with LuaText when skins are properly implemented
+ MyGUI::TextBox* mCaption;
+ MyGUI::IntPoint mPreviousMouse;
+ MyGUI::IntCoord mChangeScale;
+
+ MyGUI::IntCoord mMoveResize;
+
+ protected:
+ virtual void initialize() override;
+ virtual void deinitialize() override;
+
+ void notifyMousePress(MyGUI::Widget*, int, int, MyGUI::MouseButton);
+ void notifyMouseDrag(MyGUI::Widget*, int, int, MyGUI::MouseButton);
+ };
+}
+
+#endif // OPENMW_LUAUI_WINDOW
diff --git a/components/misc/convert.hpp b/components/misc/convert.hpp
index 81270c0c0b..45f3504dc1 100644
--- a/components/misc/convert.hpp
+++ b/components/misc/convert.hpp
@@ -19,11 +19,6 @@ namespace Convert
return osg::Vec3f(values[0], values[1], values[2]);
}
- inline osg::Vec3f makeOsgVec3f(const btVector3& value)
- {
- return osg::Vec3f(value.x(), value.y(), value.z());
- }
-
inline osg::Vec3f makeOsgVec3f(const ESM::Pathgrid::Point& value)
{
return osg::Vec3f(value.mX, value.mY, value.mZ);
@@ -72,6 +67,11 @@ namespace Convert
{
return makeBulletQuaternion(position.rot);
}
+
+ inline btTransform makeBulletTransform(const ESM::Position& position)
+ {
+ return btTransform(makeBulletQuaternion(position), toBullet(position.asVec3()));
+ }
}
}
diff --git a/components/misc/errorMarker.cpp b/components/misc/errorMarker.cpp
new file mode 100644
index 0000000000..fd2b0b53ac
--- /dev/null
+++ b/components/misc/errorMarker.cpp
@@ -0,0 +1,1390 @@
+#include "errorMarker.hpp"
+
+namespace Misc
+{
+ const std::string errorMarker = "#Ascii Scene "
+"#Version 162 "
+"#Generator OpenSceneGraph 3.6.5 "
+""
+"osg::Group {"
+" UniqueID 1 "
+" Children 5 {"
+" osg::Geode {"
+" UniqueID 2 "
+" Name \"Error\" "
+" Drawables 1 {"
+" osg::Geometry {"
+" UniqueID 3 "
+" DataVariance STATIC "
+" StateSet TRUE {"
+" osg::StateSet {"
+" UniqueID 4 "
+" DataVariance STATIC "
+" ModeList 1 {"
+" GL_BLEND ON "
+" }"
+" AttributeList 1 {"
+" osg::Material {"
+" UniqueID 5 "
+" Name \"Error\" "
+" Ambient TRUE Front 1 1 1 0.5 Back 1 1 1 0.5 "
+" Diffuse TRUE Front 0.8 0.704 0.32 0.5 Back 0.8 0.704 0.32 0.5 "
+" Specular TRUE Front 0.5 0.5 0.5 0.5 Back 0.5 0.5 0.5 0.5 "
+" Emission TRUE Front 1 0.88 0.4 0.5 Back 1 0.88 0.4 0.5 "
+" Shininess TRUE Front 28.8 Back 28.8 "
+" }"
+" Value OFF "
+" }"
+" RenderingHint 2 "
+" RenderBinMode USE_RENDERBIN_DETAILS "
+" BinNumber 10 "
+" BinName \"DepthSortedBin\" "
+" }"
+" }"
+" PrimitiveSetList 1 {"
+" osg::DrawElementsUShort {"
+" UniqueID 6 "
+" BufferObject TRUE {"
+" osg::ElementBufferObject {"
+" UniqueID 7 "
+" Target 34963 "
+" }"
+" }"
+" Mode TRIANGLES "
+" vector 108 {"
+" 0 1 2 3 "
+" 0 2 2 4 "
+" 3 5 3 4 "
+" 4 6 5 6 "
+" 7 8 8 9 "
+" 10 6 8 10 "
+" 5 6 11 11 "
+" 6 10 12 5 "
+" 11 13 12 11 "
+" 11 14 13 10 "
+" 15 11 11 15 "
+" 16 15 17 16 "
+" 18 16 17 17 "
+" 19 18 20 21 "
+" 22 23 20 22 "
+" 22 24 23 25 "
+" 23 24 24 26 "
+" 25 26 27 28 "
+" 28 29 30 26 "
+" 28 30 25 26 "
+" 31 31 26 30 "
+" 32 25 31 33 "
+" 32 31 31 34 "
+" 33 30 35 31 "
+" 31 35 36 35 "
+" 37 36 38 36 "
+" 37 37 39 38 "
+" "
+" }"
+" }"
+" }"
+" VertexArray TRUE {"
+" osg::Vec3Array {"
+" UniqueID 8 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 9 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 40 {"
+" -4.51996 -5.9634 -60.7026 "
+" 2e-06 -5.96339 -61.6017 "
+" 4.51996 -5.96339 -60.7026 "
+" -8.3518 -5.9634 -58.1422 "
+" 8.3518 -5.96339 -58.1422 "
+" -58.1422 -5.96341 -8.3518 "
+" 58.1422 -5.96339 -8.3518 "
+" 60.7026 -5.96339 -4.51996 "
+" 61.6017 -5.96339 0 "
+" 60.7026 -5.96339 4.51996 "
+" 58.1423 -5.96339 8.3518 "
+" -58.1422 -5.96341 8.3518 "
+" -60.7026 -5.96341 -4.51996 "
+" -61.6017 -5.96341 0 "
+" -60.7026 -5.96341 4.51996 "
+" 8.3518 -5.9634 58.1422 "
+" -8.3518 -5.96341 58.1422 "
+" 4.51997 -5.96341 60.7026 "
+" -4.51996 -5.96341 60.7026 "
+" 2e-06 -5.96341 61.6017 "
+" -60.7026 5.96339 -4.51996 "
+" -61.6017 5.96339 0 "
+" -60.7026 5.96339 4.51996 "
+" -58.1423 5.96339 -8.3518 "
+" -58.1422 5.96339 8.3518 "
+" -8.3518 5.9634 -58.1422 "
+" -8.3518 5.96339 58.1422 "
+" -4.51996 5.96339 60.7026 "
+" -2e-06 5.96339 61.6017 "
+" 4.51996 5.9634 60.7026 "
+" 8.3518 5.9634 58.1422 "
+" 8.3518 5.96341 -58.1422 "
+" -4.51997 5.96341 -60.7026 "
+" -2e-06 5.96341 -61.6017 "
+" 4.51996 5.96341 -60.7026 "
+" 58.1422 5.96341 8.3518 "
+" 58.1422 5.96341 -8.3518 "
+" 60.7026 5.96341 4.51996 "
+" 60.7026 5.96341 -4.51996 "
+" 61.6017 5.96341 0 "
+" }"
+" }"
+" }"
+" NormalArray TRUE {"
+" osg::Vec3Array {"
+" UniqueID 10 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 9 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 40 {"
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" }"
+" }"
+" }"
+" TexCoordArrayList 1 {"
+" osg::Vec2Array {"
+" UniqueID 11 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 9 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 40 {"
+" 0.37739 0.519384 "
+" 0.384197 0.509197 "
+" 0.394384 0.50239 "
+" 0.375 0.531401 "
+" 0.406401 0.5 "
+" 0.375 0.718599 "
+" 0.593599 0.5 "
+" 0.605616 0.50239 "
+" 0.615803 0.509197 "
+" 0.62261 0.519384 "
+" 0.625 0.531401 "
+" 0.406401 0.75 "
+" 0.37739 0.730616 "
+" 0.384197 0.740803 "
+" 0.394384 0.74761 "
+" 0.625 0.718599 "
+" 0.593599 0.75 "
+" 0.62261 0.730616 "
+" 0.605616 0.74761 "
+" 0.615803 0.740803 "
+" 0.37739 0.019384 "
+" 0.384197 0.009197 "
+" 0.394384 0.00239 "
+" 0.375 0.031401 "
+" 0.406401 0 "
+" 0.375 0.218599 "
+" 0.593599 0 "
+" 0.605616 0.00239 "
+" 0.615803 0.009197 "
+" 0.62261 0.019384 "
+" 0.625 0.031401 "
+" 0.406401 0.25 "
+" 0.37739 0.230616 "
+" 0.384197 0.240803 "
+" 0.394384 0.24761 "
+" 0.625 0.218599 "
+" 0.593599 0.25 "
+" 0.62261 0.230616 "
+" 0.605616 0.24761 "
+" 0.615803 0.240803 "
+" }"
+" }"
+" }"
+" }"
+" }"
+" }"
+" osg::Geode {"
+" UniqueID 12 "
+" Name \"Error\" "
+" Drawables 1 {"
+" osg::Geometry {"
+" UniqueID 13 "
+" DataVariance STATIC "
+" StateSet TRUE {"
+" osg::StateSet {"
+" UniqueID 4 "
+" }"
+" }"
+" PrimitiveSetList 1 {"
+" osg::DrawElementsUShort {"
+" UniqueID 14 "
+" BufferObject TRUE {"
+" osg::ElementBufferObject {"
+" UniqueID 15 "
+" Target 34963 "
+" }"
+" }"
+" Mode TRIANGLES "
+" vector 120 {"
+" 0 1 2 0 "
+" 3 1 3 4 "
+" 1 3 5 4 "
+" 4 5 6 4 "
+" 6 7 8 7 "
+" 6 8 6 9 "
+" 10 8 9 10 "
+" 9 11 12 13 "
+" 11 12 11 14 "
+" 15 12 14 15 "
+" 14 16 16 17 "
+" 15 16 18 17 "
+" 18 19 17 18 "
+" 20 19 20 21 "
+" 19 20 22 21 "
+" 22 23 24 22 "
+" 25 23 25 26 "
+" 23 25 27 26 "
+" 28 26 27 28 "
+" 29 26 30 29 "
+" 28 30 28 31 "
+" 32 30 31 32 "
+" 31 33 34 35 "
+" 33 34 33 36 "
+" 37 34 36 37 "
+" 36 38 39 37 "
+" 38 39 38 40 "
+" 40 41 39 40 "
+" 42 41 42 43 "
+" 41 42 0 43 "
+" "
+" }"
+" }"
+" }"
+" VertexArray TRUE {"
+" osg::Vec3Array {"
+" UniqueID 16 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 17 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 44 {"
+" 61.6017 -5.96339 0 "
+" 60.7026 5.96341 -4.51996 "
+" 61.6017 5.96341 0 "
+" 60.7026 -5.96339 -4.51996 "
+" 58.1422 5.96341 -8.3518 "
+" 58.1422 -5.96339 -8.3518 "
+" 8.3518 -5.96339 -58.1422 "
+" 8.3518 5.96341 -58.1422 "
+" 4.51996 5.96341 -60.7026 "
+" 4.51996 -5.96339 -60.7026 "
+" -2e-06 5.96341 -61.6017 "
+" 2e-06 -5.96339 -61.6017 "
+" -4.51997 5.96341 -60.7026 "
+" -2e-06 5.96341 -61.6017 "
+" -4.51996 -5.9634 -60.7026 "
+" -8.3518 5.9634 -58.1422 "
+" -8.3518 -5.9634 -58.1422 "
+" -58.1423 5.96339 -8.3518 "
+" -58.1422 -5.96341 -8.3518 "
+" -60.7026 5.96339 -4.51996 "
+" -60.7026 -5.96341 -4.51996 "
+" -61.6017 5.96339 0 "
+" -61.6017 -5.96341 0 "
+" -60.7026 5.96339 4.51996 "
+" -61.6017 5.96339 0 "
+" -60.7026 -5.96341 4.51996 "
+" -58.1422 5.96339 8.3518 "
+" -58.1422 -5.96341 8.3518 "
+" -8.3518 -5.96341 58.1422 "
+" -8.3518 5.96339 58.1422 "
+" -4.51996 5.96339 60.7026 "
+" -4.51996 -5.96341 60.7026 "
+" -2e-06 5.96339 61.6017 "
+" 2e-06 -5.96341 61.6017 "
+" 4.51996 5.9634 60.7026 "
+" -2e-06 5.96339 61.6017 "
+" 4.51997 -5.96341 60.7026 "
+" 8.3518 5.9634 58.1422 "
+" 8.3518 -5.9634 58.1422 "
+" 58.1422 5.96341 8.3518 "
+" 58.1423 -5.96339 8.3518 "
+" 60.7026 5.96341 4.51996 "
+" 60.7026 -5.96339 4.51996 "
+" 61.6017 5.96341 0 "
+" }"
+" }"
+" }"
+" NormalArray TRUE {"
+" osg::Vec3Array {"
+" UniqueID 18 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 17 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 44 {"
+" 1 0 0 "
+" 0.923877 0 -0.38269 "
+" 0.980784 0 -0.195097 "
+" 0.923877 0 -0.38269 "
+" 0.773003 0 -0.634402 "
+" 0.773003 0 -0.634402 "
+" 0.634402 0 -0.773003 "
+" 0.634402 0 -0.773003 "
+" 0.38269 0 -0.923877 "
+" 0.38269 0 -0.923877 "
+" 0.195097 0 -0.980784 "
+" 0 0 -1 "
+" -0.38269 -0 -0.923877 "
+" -0.195097 -0 -0.980784 "
+" -0.38269 -0 -0.923877 "
+" -0.634402 -0 -0.773003 "
+" -0.634402 -0 -0.773003 "
+" -0.773003 -0 -0.634402 "
+" -0.773003 -0 -0.634402 "
+" -0.923877 -0 -0.38269 "
+" -0.923877 -0 -0.38269 "
+" -0.980784 -0 -0.195097 "
+" -1 0 0 "
+" -0.923877 0 0.38269 "
+" -0.980784 0 0.195097 "
+" -0.923877 0 0.38269 "
+" -0.773003 0 0.634402 "
+" -0.773003 0 0.634402 "
+" -0.634402 0 0.773003 "
+" -0.634402 0 0.773003 "
+" -0.38269 0 0.923877 "
+" -0.38269 0 0.923877 "
+" -0.195097 0 0.980784 "
+" 0 0 1 "
+" 0.38269 0 0.923877 "
+" 0.195097 0 0.980784 "
+" 0.38269 0 0.923877 "
+" 0.634402 0 0.773003 "
+" 0.634402 0 0.773003 "
+" 0.773003 0 0.634402 "
+" 0.773003 0 0.634402 "
+" 0.923877 0 0.38269 "
+" 0.923877 0 0.38269 "
+" 0.980784 0 0.195097 "
+" }"
+" }"
+" }"
+" TexCoordArrayList 1 {"
+" osg::Vec2Array {"
+" UniqueID 19 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 17 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 44 {"
+" 0.625 0.5 "
+" 0.605616 0.25 "
+" 0.625 0.25 "
+" 0.605616 0.5 "
+" 0.593599 0.25 "
+" 0.593599 0.5 "
+" 0.406401 0.5 "
+" 0.406401 0.25 "
+" 0.394384 0.25 "
+" 0.394384 0.5 "
+" 0.375 0.25 "
+" 0.375 0.5 "
+" 0.125 0.519384 "
+" 0.125 0.5 "
+" 0.375 0.519384 "
+" 0.125 0.531401 "
+" 0.375 0.531401 "
+" 0.125 0.718599 "
+" 0.375 0.718599 "
+" 0.125 0.730616 "
+" 0.375 0.730616 "
+" 0.125 0.75 "
+" 0.375 0.75 "
+" 0.394384 1 "
+" 0.375 1 "
+" 0.394384 0.75 "
+" 0.406401 1 "
+" 0.406401 0.75 "
+" 0.593599 0.75 "
+" 0.593599 1 "
+" 0.605616 1 "
+" 0.605616 0.75 "
+" 0.625 1 "
+" 0.625 0.75 "
+" 0.875 0.730616 "
+" 0.875 0.75 "
+" 0.625 0.730616 "
+" 0.875 0.718599 "
+" 0.625 0.718599 "
+" 0.875 0.531401 "
+" 0.625 0.531401 "
+" 0.875 0.519384 "
+" 0.625 0.519384 "
+" 0.875 0.5 "
+" }"
+" }"
+" }"
+" }"
+" }"
+" }"
+" osg::Geode {"
+" UniqueID 20 "
+" Name \"Error\" "
+" Drawables 1 {"
+" osg::Geometry {"
+" UniqueID 21 "
+" DataVariance STATIC "
+" StateSet TRUE {"
+" osg::StateSet {"
+" UniqueID 22 "
+" DataVariance STATIC "
+" AttributeList 1 {"
+" osg::Material {"
+" UniqueID 23 "
+" Name \"ErrorLabel\" "
+" Ambient TRUE Front 1 1 1 1 Back 1 1 1 1 "
+" Diffuse TRUE Front 0.176208 0.176208 0.176208 1 Back 0.176208 0.176208 0.176208 1 "
+" Specular TRUE Front 0.5 0.5 0.5 1 Back 0.5 0.5 0.5 1 "
+" Emission TRUE Front 0.22026 0.22026 0.22026 1 Back 0.22026 0.22026 0.22026 1 "
+" Shininess TRUE Front 28.8 Back 28.8 "
+" }"
+" Value OFF "
+" }"
+" }"
+" }"
+" PrimitiveSetList 1 {"
+" osg::DrawElementsUShort {"
+" UniqueID 24 "
+" BufferObject TRUE {"
+" osg::ElementBufferObject {"
+" UniqueID 25 "
+" Target 34963 "
+" }"
+" }"
+" Mode TRIANGLES "
+" vector 216 {"
+" 0 1 2 3 "
+" 0 2 2 4 "
+" 3 4 5 3 "
+" 5 6 7 5 "
+" 8 3 8 5 "
+" 7 7 9 8 "
+" 10 3 8 8 "
+" 11 10 12 13 "
+" 10 14 12 10 "
+" 15 14 10 10 "
+" 11 15 16 15 "
+" 11 11 17 16 "
+" 18 16 17 17 "
+" 19 18 20 21 "
+" 22 23 20 22 "
+" 22 24 23 25 "
+" 23 24 24 26 "
+" 25 26 27 28 "
+" 28 29 30 26 "
+" 28 30 25 26 "
+" 31 31 26 30 "
+" 32 25 31 33 "
+" 32 31 31 34 "
+" 33 30 35 31 "
+" 31 35 36 35 "
+" 37 36 38 36 "
+" 37 37 39 38 "
+" 40 41 42 43 "
+" 40 42 42 44 "
+" 43 45 43 44 "
+" 44 46 45 47 "
+" 45 46 48 47 "
+" 46 46 49 48 "
+" 44 50 46 51 "
+" 46 50 50 52 "
+" 53 54 50 53 "
+" 53 55 54 50 "
+" 54 51 54 56 "
+" 51 56 57 51 "
+" 58 51 57 57 "
+" 59 58 60 61 "
+" 62 63 60 62 "
+" 62 64 63 65 "
+" 63 64 64 66 "
+" 65 66 67 68 "
+" 69 70 65 71 "
+" 69 65 72 71 "
+" 65 66 68 73 "
+" 65 66 73 68 "
+" 74 73 73 72 "
+" 65 73 75 72 "
+" 72 75 76 75 "
+" 77 76 78 76 "
+" 77 77 79 78 "
+" "
+" }"
+" }"
+" }"
+" VertexArray TRUE {"
+" osg::Vec3Array {"
+" UniqueID 26 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 27 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 80 {"
+" -7.12646 -9.95049 -13.7349 "
+" -6.35115 -9.95049 -14.8952 "
+" -5.1908 -9.95049 -15.6705 "
+" -7.39872 -9.95049 -12.3661 "
+" -3.82208 -9.95049 -15.9428 "
+" 3.82208 -9.95049 -15.9428 "
+" 5.1908 -9.95049 -15.6705 "
+" 6.35115 -9.95049 -14.8952 "
+" 7.39872 -9.95049 -12.3661 "
+" 7.12646 -9.95049 -13.7349 "
+" -7.39872 -9.95049 33.4208 "
+" 7.39872 -9.95049 33.4208 "
+" -6.35115 -9.95049 35.9499 "
+" -7.12647 -9.95049 34.7895 "
+" -5.1908 -9.95049 36.7252 "
+" -3.82208 -9.95049 36.9975 "
+" 3.82208 -9.95049 36.9975 "
+" 7.12646 -9.95049 34.7895 "
+" 5.1908 -9.95049 36.7252 "
+" 6.35115 -9.95049 35.9499 "
+" -7.12646 -9.95042 -36.7346 "
+" -6.35115 -9.95042 -37.8949 "
+" -5.1908 -9.95042 -38.6702 "
+" -7.39872 -9.95042 -35.3659 "
+" -3.82208 -9.95042 -38.9425 "
+" -7.39872 -9.95042 -27.7217 "
+" 3.82208 -9.95042 -38.9425 "
+" 5.1908 -9.95042 -38.6702 "
+" 6.35115 -9.95042 -37.8949 "
+" 7.12646 -9.95042 -36.7346 "
+" 7.39872 -9.95042 -35.3659 "
+" -3.82208 -9.95042 -24.1451 "
+" -7.12647 -9.95042 -26.353 "
+" -6.35115 -9.95042 -25.1926 "
+" -5.1908 -9.95042 -24.4173 "
+" 7.39872 -9.95042 -27.7217 "
+" 3.82208 -9.95042 -24.1451 "
+" 7.12646 -9.95042 -26.353 "
+" 5.1908 -9.95042 -24.4173 "
+" 6.35115 -9.95042 -25.1926 "
+" -5.1908 9.95055 -15.6705 "
+" -6.35115 9.95055 -14.8952 "
+" -7.12646 9.95055 -13.7349 "
+" -3.82208 9.95055 -15.9428 "
+" -7.39872 9.95055 -12.3661 "
+" 3.82208 9.95055 -15.9428 "
+" 7.39872 9.95055 -12.3661 "
+" 5.1908 9.95055 -15.6705 "
+" 6.35115 9.95055 -14.8952 "
+" 7.12646 9.95055 -13.7349 "
+" -7.39872 9.95055 33.4208 "
+" 7.39872 9.95055 33.4208 "
+" -7.12646 9.95055 34.7895 "
+" -6.35115 9.95056 35.9499 "
+" -3.82208 9.95056 36.9975 "
+" -5.1908 9.95056 36.7252 "
+" 3.82208 9.95055 36.9975 "
+" 5.19081 9.95055 36.7252 "
+" 7.12646 9.95056 34.7895 "
+" 6.35115 9.95055 35.9499 "
+" -5.1908 9.95062 -38.6702 "
+" -6.35115 9.95062 -37.8949 "
+" -7.12646 9.95062 -36.7346 "
+" -3.82208 9.95062 -38.9425 "
+" -7.39872 9.95062 -35.3659 "
+" 3.82208 9.95062 -38.9425 "
+" -7.39872 9.95063 -27.7217 "
+" -7.12646 9.95063 -26.353 "
+" -6.35115 9.95063 -25.1926 "
+" 6.35115 9.95062 -37.8949 "
+" 5.1908 9.95062 -38.6702 "
+" 7.12647 9.95062 -36.7346 "
+" 7.39872 9.95062 -35.3659 "
+" -3.82208 9.95063 -24.1451 "
+" -5.1908 9.95063 -24.4173 "
+" 3.82208 9.95063 -24.1451 "
+" 7.39872 9.95062 -27.7217 "
+" 5.1908 9.95063 -24.4173 "
+" 7.12646 9.95063 -26.353 "
+" 6.35115 9.95063 -25.1926 "
+" }"
+" }"
+" }"
+" NormalArray TRUE {"
+" osg::Vec3Array {"
+" UniqueID 28 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 27 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 80 {"
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" }"
+" }"
+" }"
+" TexCoordArrayList 1 {"
+" osg::Vec2Array {"
+" UniqueID 29 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 27 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 80 {"
+" 0.006133 0.041706 "
+" 0.023598 0.006596 "
+" 0.149209 0.001714 "
+" 0 0.06756 "
+" 0.241707 0 "
+" 0.758294 0 "
+" 0.850791 0.001714 "
+" 0.976402 0.006596 "
+" 1 0.06756 "
+" 0.993867 0.041706 "
+" 0 0.93244 "
+" 1 0.93244 "
+" 0.023598 0.993404 "
+" 0.006133 0.958294 "
+" 0.149209 0.998286 "
+" 0.241706 1 "
+" 0.758294 1 "
+" 0.993867 0.958294 "
+" 0.850791 0.998286 "
+" 0.976402 0.993404 "
+" 0.006133 0.149209 "
+" 0.023598 0.023598 "
+" 0.149209 0.006133 "
+" 0 0.241707 "
+" 0.241707 0 "
+" 0 0.758294 "
+" 0.758294 0 "
+" 0.850791 0.006133 "
+" 0.976402 0.023598 "
+" 0.993867 0.149209 "
+" 1 0.241707 "
+" 0.241706 1 "
+" 0.006133 0.850791 "
+" 0.023598 0.976402 "
+" 0.149209 0.993867 "
+" 1 0.758293 "
+" 0.758294 1 "
+" 0.993867 0.850791 "
+" 0.850791 0.993867 "
+" 0.976402 0.976402 "
+" 0.149209 0.001714 "
+" 0.023598 0.006596 "
+" 0.006133 0.041706 "
+" 0.241706 0 "
+" 0 0.06756 "
+" 0.758294 0 "
+" 1 0.06756 "
+" 0.850791 0.001714 "
+" 0.976402 0.006596 "
+" 0.993867 0.041706 "
+" 0 0.93244 "
+" 1 0.93244 "
+" 0.006133 0.958294 "
+" 0.023598 0.993404 "
+" 0.241707 1 "
+" 0.149209 0.998286 "
+" 0.758294 1 "
+" 0.850791 0.998286 "
+" 0.993867 0.958294 "
+" 0.976402 0.993404 "
+" 0.149209 0.006133 "
+" 0.023598 0.023598 "
+" 0.006133 0.149209 "
+" 0.241706 0 "
+" 0 0.241707 "
+" 0.758294 0 "
+" 0 0.758294 "
+" 0.006133 0.850791 "
+" 0.023598 0.976402 "
+" 0.976402 0.023598 "
+" 0.850791 0.006133 "
+" 0.993867 0.149209 "
+" 1 0.241706 "
+" 0.241707 1 "
+" 0.149209 0.993867 "
+" 0.758294 1 "
+" 1 0.758294 "
+" 0.850791 0.993867 "
+" 0.993867 0.850791 "
+" 0.976402 0.976402 "
+" }"
+" }"
+" }"
+" }"
+" }"
+" }"
+" osg::Geode {"
+" UniqueID 30 "
+" Name \"Error\" "
+" Drawables 1 {"
+" osg::Geometry {"
+" UniqueID 31 "
+" DataVariance STATIC "
+" StateSet TRUE {"
+" osg::StateSet {"
+" UniqueID 22 "
+" }"
+" }"
+" PrimitiveSetList 1 {"
+" osg::DrawElementsUShort {"
+" UniqueID 32 "
+" BufferObject TRUE {"
+" osg::ElementBufferObject {"
+" UniqueID 33 "
+" Target 34963 "
+" }"
+" }"
+" Mode TRIANGLES "
+" vector 240 {"
+" 0 1 2 0 "
+" 3 1 0 2 "
+" 4 0 5 3 "
+" 4 2 6 5 "
+" 7 3 4 6 "
+" 8 5 9 7 "
+" 6 10 8 9 "
+" 11 7 6 12 "
+" 10 9 13 11 "
+" 12 14 10 13 "
+" 15 11 12 16 "
+" 14 13 17 15 "
+" 16 18 14 19 "
+" 15 17 16 20 "
+" 18 19 21 15 "
+" 18 20 22 23 "
+" 21 19 18 22 "
+" 24 23 19 25 "
+" 26 24 22 27 "
+" 23 25 26 22 "
+" 28 27 25 29 "
+" 30 26 28 31 "
+" 27 29 30 32 "
+" 26 31 29 33 "
+" 34 32 30 35 "
+" 31 33 34 30 "
+" 36 35 33 37 "
+" 38 34 36 39 "
+" 35 37 38 39 "
+" 34 39 38 35 "
+" 40 41 42 43 "
+" 41 40 40 42 "
+" 44 43 40 45 "
+" 46 44 42 47 "
+" 43 45 46 48 "
+" 44 47 45 49 "
+" 50 48 46 51 "
+" 47 49 50 46 "
+" 52 51 49 53 "
+" 54 50 52 55 "
+" 51 53 54 52 "
+" 56 55 53 57 "
+" 58 54 56 57 "
+" 59 55 58 56 "
+" 60 57 61 59 "
+" 58 60 62 61 "
+" 63 59 58 62 "
+" 64 61 65 63 "
+" 62 66 64 65 "
+" 67 63 62 68 "
+" 66 65 69 67 "
+" 66 68 70 69 "
+" 71 67 66 70 "
+" 72 69 73 71 "
+" 70 74 72 71 "
+" 73 75 70 76 "
+" 74 71 75 77 "
+" 76 78 74 75 "
+" 79 77 76 79 "
+" 78 75 78 79 "
+" "
+" }"
+" }"
+" }"
+" VertexArray TRUE {"
+" osg::Vec3Array {"
+" UniqueID 34 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 35 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 80 {"
+" -7.39872 9.95055 -12.3661 "
+" -7.39872 -9.95049 -12.3661 "
+" -7.39872 -9.95049 33.4208 "
+" -7.12646 -9.95049 -13.7349 "
+" -7.39872 9.95055 33.4208 "
+" -7.12646 9.95055 -13.7349 "
+" -7.12647 -9.95049 34.7895 "
+" -6.35115 -9.95049 -14.8952 "
+" -7.12646 9.95055 34.7895 "
+" -6.35115 9.95055 -14.8952 "
+" -6.35115 9.95056 35.9499 "
+" -5.1908 -9.95049 -15.6705 "
+" -6.35115 -9.95049 35.9499 "
+" -5.1908 9.95055 -15.6705 "
+" -5.1908 9.95056 36.7252 "
+" -3.82208 -9.95049 -15.9428 "
+" -5.1908 -9.95049 36.7252 "
+" -3.82208 9.95055 -15.9428 "
+" -3.82208 9.95056 36.9975 "
+" 3.82208 9.95055 -15.9428 "
+" -3.82208 -9.95049 36.9975 "
+" 3.82208 -9.95049 -15.9428 "
+" 3.82208 -9.95049 36.9975 "
+" 5.1908 -9.95049 -15.6705 "
+" 3.82208 9.95055 36.9975 "
+" 5.1908 9.95055 -15.6705 "
+" 5.19081 9.95055 36.7252 "
+" 6.35115 -9.95049 -14.8952 "
+" 5.1908 -9.95049 36.7252 "
+" 6.35115 9.95055 -14.8952 "
+" 6.35115 -9.95049 35.9499 "
+" 7.12646 -9.95049 -13.7349 "
+" 6.35115 9.95055 35.9499 "
+" 7.12646 9.95055 -13.7349 "
+" 7.12646 9.95056 34.7895 "
+" 7.39872 -9.95049 -12.3661 "
+" 7.12646 -9.95049 34.7895 "
+" 7.39872 9.95055 -12.3661 "
+" 7.39872 -9.95049 33.4208 "
+" 7.39872 9.95055 33.4208 "
+" -3.82208 9.95063 -24.1451 "
+" -3.82208 -9.95042 -24.1451 "
+" 3.82208 -9.95042 -24.1451 "
+" -5.1908 -9.95042 -24.4173 "
+" 3.82208 9.95063 -24.1451 "
+" -5.1908 9.95063 -24.4173 "
+" 5.1908 -9.95042 -24.4173 "
+" -6.35115 -9.95042 -25.1926 "
+" 5.1908 9.95063 -24.4173 "
+" -6.35115 9.95063 -25.1926 "
+" 6.35115 9.95063 -25.1926 "
+" -7.12647 -9.95042 -26.353 "
+" 6.35115 -9.95042 -25.1926 "
+" -7.12646 9.95063 -26.353 "
+" 7.12646 9.95063 -26.353 "
+" -7.39872 -9.95042 -27.7217 "
+" 7.12646 -9.95042 -26.353 "
+" -7.39872 9.95063 -27.7217 "
+" 7.39872 9.95062 -27.7217 "
+" -7.39872 -9.95042 -35.3659 "
+" 7.39872 -9.95042 -27.7217 "
+" -7.39872 9.95062 -35.3659 "
+" 7.39872 -9.95042 -35.3659 "
+" -7.12646 -9.95042 -36.7346 "
+" 7.39872 9.95062 -35.3659 "
+" -7.12646 9.95062 -36.7346 "
+" 7.12647 9.95062 -36.7346 "
+" -6.35115 -9.95042 -37.8949 "
+" 7.12646 -9.95042 -36.7346 "
+" -6.35115 9.95062 -37.8949 "
+" 6.35115 -9.95042 -37.8949 "
+" -5.1908 -9.95042 -38.6702 "
+" 6.35115 9.95062 -37.8949 "
+" -5.1908 9.95062 -38.6702 "
+" 5.1908 9.95062 -38.6702 "
+" -3.82208 9.95062 -38.9425 "
+" 5.1908 -9.95042 -38.6702 "
+" -3.82208 -9.95042 -38.9425 "
+" 3.82208 9.95062 -38.9425 "
+" 3.82208 -9.95042 -38.9425 "
+" }"
+" }"
+" }"
+" NormalArray TRUE {"
+" osg::Vec3Array {"
+" UniqueID 36 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 35 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 80 {"
+" -0.995187 -0 -0.0979987 "
+" -0.995187 -0 -0.0979987 "
+" -0.995187 0 0.0979987 "
+" -0.923877 -0 -0.38269 "
+" -0.995187 0 0.0979987 "
+" -0.923877 -0 -0.38269 "
+" -0.923877 0 0.38269 "
+" -0.707107 -0 -0.707107 "
+" -0.923877 0 0.38269 "
+" -0.707107 -0 -0.707107 "
+" -0.707107 0 0.707107 "
+" -0.38269 -0 -0.923877 "
+" -0.707107 0 0.707107 "
+" -0.38269 -0 -0.923877 "
+" -0.38269 0 0.923877 "
+" -0.0979987 -0 -0.995187 "
+" -0.38269 0 0.923877 "
+" -0.0979987 -0 -0.995187 "
+" -0.0979987 0 0.995187 "
+" 0.0979987 0 -0.995187 "
+" -0.0979987 0 0.995187 "
+" 0.0979987 0 -0.995187 "
+" 0.0979987 0 0.995187 "
+" 0.38269 0 -0.923877 "
+" 0.0979987 0 0.995187 "
+" 0.38269 0 -0.923877 "
+" 0.38269 0 0.923877 "
+" 0.707107 0 -0.707107 "
+" 0.38269 0 0.923877 "
+" 0.707107 0 -0.707107 "
+" 0.707107 0 0.707107 "
+" 0.923877 0 -0.38269 "
+" 0.707107 0 0.707107 "
+" 0.923877 0 -0.38269 "
+" 0.923877 0 0.38269 "
+" 0.995187 0 -0.0979987 "
+" 0.923877 0 0.38269 "
+" 0.995187 0 -0.0979987 "
+" 0.995187 0 0.0979987 "
+" 0.995187 0 0.0979987 "
+" -0.0979987 0 0.995187 "
+" -0.0979987 0 0.995187 "
+" 0.0979987 0 0.995187 "
+" -0.38269 0 0.923877 "
+" 0.0979987 0 0.995187 "
+" -0.38269 0 0.923877 "
+" 0.38269 0 0.923877 "
+" -0.707107 0 0.707107 "
+" 0.38269 0 0.923877 "
+" -0.707107 0 0.707107 "
+" 0.707107 0 0.707107 "
+" -0.923877 0 0.38269 "
+" 0.707107 0 0.707107 "
+" -0.923877 0 0.38269 "
+" 0.923877 0 0.38269 "
+" -0.995187 0 0.0979987 "
+" 0.923877 0 0.38269 "
+" -0.995187 0 0.0979987 "
+" 0.995187 0 0.0979987 "
+" -0.995187 -0 -0.0979987 "
+" 0.995187 0 0.0979987 "
+" -0.995187 -0 -0.0979987 "
+" 0.995187 0 -0.0979987 "
+" -0.923877 -0 -0.38269 "
+" 0.995187 0 -0.0979987 "
+" -0.923877 -0 -0.38269 "
+" 0.923877 0 -0.38269 "
+" -0.707107 -0 -0.707107 "
+" 0.923877 0 -0.38269 "
+" -0.707107 -0 -0.707107 "
+" 0.707107 0 -0.707107 "
+" -0.38269 -0 -0.923877 "
+" 0.707107 0 -0.707107 "
+" -0.38269 -0 -0.923877 "
+" 0.38269 0 -0.923877 "
+" -0.0979987 -0 -0.995187 "
+" 0.38269 0 -0.923877 "
+" -0.0979987 -0 -0.995187 "
+" 0.0979987 0 -0.995187 "
+" 0.0979987 0 -0.995187 "
+" }"
+" }"
+" }"
+" TexCoordArrayList 1 {"
+" osg::Vec2Array {"
+" UniqueID 37 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 35 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 80 {"
+" 0 0.06756 "
+" 0 0.06756 "
+" 0 0.93244 "
+" 0.006133 0.041706 "
+" 0 0.93244 "
+" 0.006133 0.041706 "
+" 0.006133 0.958294 "
+" 0.023598 0.006596 "
+" 0.006133 0.958294 "
+" 0.023598 0.006596 "
+" 0.023598 0.993404 "
+" 0.149209 0.001714 "
+" 0.023598 0.993404 "
+" 0.149209 0.001714 "
+" 0.149209 0.998286 "
+" 0.241706 0 "
+" 0.149209 0.998286 "
+" 0.241707 0 "
+" 0.241706 1 "
+" 0.758294 0 "
+" 0.241707 1 "
+" 0.758294 0 "
+" 0.758294 1 "
+" 0.850791 0.001714 "
+" 0.758294 1 "
+" 0.850791 0.001714 "
+" 0.850791 0.998286 "
+" 0.976402 0.006596 "
+" 0.850791 0.998286 "
+" 0.976402 0.006596 "
+" 0.976402 0.993404 "
+" 0.993867 0.041706 "
+" 0.976402 0.993404 "
+" 0.993867 0.041706 "
+" 0.993867 0.958294 "
+" 1 0.06756 "
+" 0.993867 0.958294 "
+" 1 0.06756 "
+" 1 0.93244 "
+" 1 0.93244 "
+" 0.241706 1 "
+" 0.241707 1 "
+" 0.758294 1 "
+" 0.149209 0.993867 "
+" 0.758294 1 "
+" 0.149209 0.993867 "
+" 0.850791 0.993867 "
+" 0.023598 0.976402 "
+" 0.850791 0.993867 "
+" 0.023598 0.976402 "
+" 0.976402 0.976402 "
+" 0.006133 0.850791 "
+" 0.976402 0.976402 "
+" 0.006133 0.850791 "
+" 0.993867 0.850791 "
+" 0 0.758293 "
+" 0.993867 0.850791 "
+" 0 0.758294 "
+" 1 0.758294 "
+" 0 0.241707 "
+" 1 0.758294 "
+" 0 0.241706 "
+" 1 0.241707 "
+" 0.006133 0.149209 "
+" 1 0.241707 "
+" 0.006133 0.149209 "
+" 0.993867 0.149209 "
+" 0.023598 0.023598 "
+" 0.993867 0.149209 "
+" 0.023598 0.023598 "
+" 0.976402 0.023598 "
+" 0.149209 0.006133 "
+" 0.976402 0.023598 "
+" 0.149209 0.006133 "
+" 0.850791 0.006133 "
+" 0.241707 0 "
+" 0.850791 0.006133 "
+" 0.241706 0 "
+" 0.758294 0 "
+" 0.758294 0 "
+" }"
+" }"
+" }"
+" }"
+" }"
+" }"
+" osg::Geode {"
+" UniqueID 38 "
+" Name \"Cube\" "
+" Drawables 1 {"
+" osg::Geometry {"
+" UniqueID 39 "
+" DataVariance STATIC "
+" StateSet TRUE {"
+" osg::StateSet {"
+" UniqueID 40 "
+" DataVariance STATIC "
+" AttributeList 1 {"
+" osg::Material {"
+" UniqueID 41 "
+" Name \"Material\" "
+" Ambient TRUE Front 1 1 1 1 Back 1 1 1 1 "
+" Diffuse TRUE Front 0.8 0.8 0.8 1 Back 0.8 0.8 0.8 1 "
+" Specular TRUE Front 0.5 0.5 0.5 1 Back 0.5 0.5 0.5 1 "
+" Emission TRUE Front 0 0 0 1 Back 0 0 0 1 "
+" Shininess TRUE Front 41.344 Back 41.344 "
+" }"
+" Value OFF "
+" }"
+" }"
+" }"
+" PrimitiveSetList 1 {"
+" osg::DrawElementsUShort {"
+" UniqueID 42 "
+" BufferObject TRUE {"
+" osg::ElementBufferObject {"
+" UniqueID 43 "
+" Target 34963 "
+" }"
+" }"
+" Mode TRIANGLES "
+" vector 36 {"
+" 0 1 2 0 "
+" 2 3 4 5 "
+" 6 4 6 7 "
+" 8 9 10 8 "
+" 10 11 12 13 "
+" 14 12 14 15 "
+" 16 17 18 16 "
+" 18 19 20 21 "
+" 22 20 22 23 "
+" "
+" }"
+" }"
+" }"
+" VertexArray TRUE {"
+" osg::Vec3Array {"
+" UniqueID 44 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 45 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 24 {"
+" 1 1 1 "
+" -1 1 1 "
+" -1 -1 1 "
+" 1 -1 1 "
+" 1 -1 -1 "
+" 1 -1 1 "
+" -1 -1 1 "
+" -1 -1 -1 "
+" -1 -1 -1 "
+" -1 -1 1 "
+" -1 1 1 "
+" -1 1 -1 "
+" -1 1 -1 "
+" 1 1 -1 "
+" 1 -1 -1 "
+" -1 -1 -1 "
+" 1 1 -1 "
+" 1 1 1 "
+" 1 -1 1 "
+" 1 -1 -1 "
+" -1 1 -1 "
+" -1 1 1 "
+" 1 1 1 "
+" 1 1 -1 "
+" }"
+" }"
+" }"
+" NormalArray TRUE {"
+" osg::Vec3Array {"
+" UniqueID 46 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 45 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 24 {"
+" 0 0 1 "
+" 0 0 1 "
+" 0 0 1 "
+" 0 0 1 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" 0 -1 0 "
+" -1 0 0 "
+" -1 0 0 "
+" -1 0 0 "
+" -1 0 0 "
+" 0 0 -1 "
+" 0 0 -1 "
+" 0 0 -1 "
+" 0 0 -1 "
+" 1 0 0 "
+" 1 0 0 "
+" 1 0 0 "
+" 1 0 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" 0 1 0 "
+" }"
+" }"
+" }"
+" TexCoordArrayList 1 {"
+" osg::Vec2Array {"
+" UniqueID 47 "
+" BufferObject TRUE {"
+" osg::VertexBufferObject {"
+" UniqueID 45 "
+" }"
+" }"
+" Binding BIND_PER_VERTEX "
+" vector 24 {"
+" 0.625 0.5 "
+" 0.875 0.5 "
+" 0.875 0.75 "
+" 0.625 0.75 "
+" 0.375 0.75 "
+" 0.625 0.75 "
+" 0.625 1 "
+" 0.375 1 "
+" 0.375 0 "
+" 0.625 0 "
+" 0.625 0.25 "
+" 0.375 0.25 "
+" 0.125 0.5 "
+" 0.375 0.5 "
+" 0.375 0.75 "
+" 0.125 0.75 "
+" 0.375 0.5 "
+" 0.625 0.5 "
+" 0.625 0.75 "
+" 0.375 0.75 "
+" 0.375 0.25 "
+" 0.625 0.25 "
+" 0.625 0.5 "
+" 0.375 0.5 "
+" }"
+" }"
+" }"
+" }"
+" }"
+" }"
+" }"
+"}";
+
+}
diff --git a/components/misc/errorMarker.hpp b/components/misc/errorMarker.hpp
new file mode 100644
index 0000000000..05e1fa9557
--- /dev/null
+++ b/components/misc/errorMarker.hpp
@@ -0,0 +1,11 @@
+#ifndef OPENMW_COMPONENTS_MISC_ERRORMARKER_H
+#define OPENMW_COMPONENTS_MISC_ERRORMARKER_H
+
+#include <string>
+
+namespace Misc
+{
+ extern const std::string errorMarker;
+}
+
+#endif
diff --git a/components/misc/hash.hpp b/components/misc/hash.hpp
index 30a9c41ee9..861df73772 100644
--- a/components/misc/hash.hpp
+++ b/components/misc/hash.hpp
@@ -1,15 +1,20 @@
#ifndef MISC_HASH_H
#define MISC_HASH_H
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+
namespace Misc
{
/// Implemented similar to the boost::hash_combine
- template <class T>
- inline void hashCombine(std::size_t& seed, const T& v)
+ template <class Seed, class T>
+ inline void hashCombine(Seed& seed, const T& v)
{
+ static_assert(sizeof(Seed) >= sizeof(std::size_t), "Resulting hash will be truncated");
std::hash<T> hasher;
- seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
+ seed ^= static_cast<Seed>(hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2));
}
}
-#endif \ No newline at end of file
+#endif
diff --git a/components/misc/osguservalues.cpp b/components/misc/osguservalues.cpp
new file mode 100644
index 0000000000..3bdd0d1848
--- /dev/null
+++ b/components/misc/osguservalues.cpp
@@ -0,0 +1,6 @@
+#include "osguservalues.hpp"
+
+namespace Misc
+{
+ const std::string OsgUserValues::sFileHash = "fileHash";
+}
diff --git a/components/misc/osguservalues.hpp b/components/misc/osguservalues.hpp
new file mode 100644
index 0000000000..022e81764f
--- /dev/null
+++ b/components/misc/osguservalues.hpp
@@ -0,0 +1,14 @@
+#ifndef OPENMW_COMPONENTS_MISC_OSGUSERVALUES_H
+#define OPENMW_COMPONENTS_MISC_OSGUSERVALUES_H
+
+#include <string>
+
+namespace Misc
+{
+ struct OsgUserValues
+ {
+ static const std::string sFileHash;
+ };
+}
+
+#endif
diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp
index 610c7a790c..0095568653 100644
--- a/components/misc/resourcehelpers.cpp
+++ b/components/misc/resourcehelpers.cpp
@@ -30,17 +30,22 @@ namespace
}
-bool Misc::ResourceHelpers::changeExtensionToDds(std::string &path)
+bool changeExtension(std::string &path, std::string_view ext)
{
std::string::size_type pos = path.rfind('.');
- if(pos != std::string::npos && path.compare(pos, path.length() - pos, ".dds") != 0)
+ if(pos != std::string::npos && path.compare(pos, path.length() - pos, ext) != 0)
{
- path.replace(pos, path.length(), ".dds");
+ path.replace(pos, path.length(), ext);
return true;
}
return false;
}
+bool Misc::ResourceHelpers::changeExtensionToDds(std::string &path)
+{
+ return changeExtension(path, ".dds");
+}
+
std::string Misc::ResourceHelpers::correctResourcePath(const std::string &topLevelDirectory, const std::string &resPath, const VFS::Manager* vfs)
{
/* Bethesda at some point converted all their BSA
@@ -140,6 +145,17 @@ std::string Misc::ResourceHelpers::correctActorModelPath(const std::string &resP
return mdlname;
}
+std::string Misc::ResourceHelpers::correctSoundPath(const std::string& resPath, const VFS::Manager* vfs)
+{
+ std::string sound = resPath;
+ // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav.
+ if (!vfs->exists(sound))
+ changeExtension(sound, ".mp3");
+
+ return vfs->normalizeFilename(sound);
+
+}
+
bool Misc::ResourceHelpers::isHiddenMarker(std::string_view id)
{
return Misc::StringUtils::ciEqual(id, "prisonmarker") || Misc::StringUtils::ciEqual(id, "divinemarker") || Misc::StringUtils::ciEqual(id, "templemarker") || Misc::StringUtils::ciEqual(id, "northmarker");
diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp
index 9e87954e9d..4ea5f5e121 100644
--- a/components/misc/resourcehelpers.hpp
+++ b/components/misc/resourcehelpers.hpp
@@ -25,6 +25,8 @@ namespace Misc
/// Use "xfoo.nif" instead of "foo.nif" if available
std::string correctActorModelPath(const std::string &resPath, const VFS::Manager* vfs);
+ std::string correctSoundPath(const std::string& resPath, const VFS::Manager* vfs);
+
/// marker objects that have a hardcoded function in the game logic, should be hidden from the player
bool isHiddenMarker(std::string_view id);
}
diff --git a/components/misc/stringops.hpp b/components/misc/stringops.hpp
index 12633db826..fe4be00006 100644
--- a/components/misc/stringops.hpp
+++ b/components/misc/stringops.hpp
@@ -184,17 +184,16 @@ public:
static inline void trim(std::string &s)
{
- // left trim
- s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch)
+ const auto notSpace = [](char ch)
{
- return !std::isspace(ch);
- }));
+ // TODO Do we care about multibyte whitespace?
+ return !std::isspace(static_cast<unsigned char>(ch));
+ };
+ // left trim
+ s.erase(s.begin(), std::find_if(s.begin(), s.end(), notSpace));
// right trim
- s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch)
- {
- return !std::isspace(ch);
- }).base(), s.end());
+ s.erase(std::find_if(s.rbegin(), s.rend(), notSpace).base(), s.end());
}
template <class Container>
diff --git a/components/misc/typetraits.hpp b/components/misc/typetraits.hpp
new file mode 100644
index 0000000000..4c6a7e731b
--- /dev/null
+++ b/components/misc/typetraits.hpp
@@ -0,0 +1,19 @@
+#ifndef OPENMW_COMPONENTS_MISC_TYPETRAITS_H
+#define OPENMW_COMPONENTS_MISC_TYPETRAITS_H
+
+#include <optional>
+#include <type_traits>
+
+namespace Misc
+{
+ template <class T>
+ struct IsOptional : std::false_type {};
+
+ template <class T>
+ struct IsOptional<std::optional<T>> : std::true_type {};
+
+ template <class T>
+ inline constexpr bool isOptional = IsOptional<T>::value;
+}
+
+#endif
diff --git a/components/misc/utf8stream.hpp b/components/misc/utf8stream.hpp
index 9dc8aa8208..8435816c67 100644
--- a/components/misc/utf8stream.hpp
+++ b/components/misc/utf8stream.hpp
@@ -3,6 +3,7 @@
#include <cstring>
#include <string>
+#include <string_view>
#include <tuple>
class Utf8Stream
@@ -30,6 +31,11 @@ public:
{
}
+ Utf8Stream (std::string_view str) :
+ Utf8Stream (reinterpret_cast<Point>(str.data()), reinterpret_cast<Point>(str.data() + str.size()))
+ {
+ }
+
bool eof () const
{
return cur == end;
diff --git a/components/myguiplatform/myguicompat.h b/components/myguiplatform/myguicompat.h
deleted file mode 100644
index 04ca11a79f..0000000000
--- a/components/myguiplatform/myguicompat.h
+++ /dev/null
@@ -1,12 +0,0 @@
-#ifndef OPENMW_COMPONENTS_MYGUIPLATFORM_MYGUICOMPAT_H
-#define OPENMW_COMPONENTS_MYGUIPLATFORM_MYGUICOMPAT_H
-
-#include <MyGUI_Prerequest.h>
-
-#if MYGUI_VERSION > MYGUI_DEFINE_VERSION(3, 4, 0)
- #define OPENMW_MYGUI_CONST_GETTER_3_4_1 const
-#else
- #define OPENMW_MYGUI_CONST_GETTER_3_4_1
-#endif
-
-#endif // OPENMW_COMPONENTS_MYGUIPLATFORM_MYGUICOMPAT_H
diff --git a/components/myguiplatform/myguidatamanager.cpp b/components/myguiplatform/myguidatamanager.cpp
index 0310e996b9..fc5d2bf953 100644
--- a/components/myguiplatform/myguidatamanager.cpp
+++ b/components/myguiplatform/myguidatamanager.cpp
@@ -15,7 +15,7 @@ void DataManager::setResourcePath(const std::string &path)
mResourcePath = path;
}
-MyGUI::IDataStream *DataManager::getData(const std::string &name) OPENMW_MYGUI_CONST_GETTER_3_4_1
+MyGUI::IDataStream *DataManager::getData(const std::string &name) const
{
std::string fullpath = getDataPath(name);
std::unique_ptr<boost::filesystem::ifstream> stream;
@@ -34,13 +34,13 @@ void DataManager::freeData(MyGUI::IDataStream *data)
delete data;
}
-bool DataManager::isDataExist(const std::string &name) OPENMW_MYGUI_CONST_GETTER_3_4_1
+bool DataManager::isDataExist(const std::string &name) const
{
std::string fullpath = mResourcePath + "/" + name;
return boost::filesystem::exists(fullpath);
}
-const MyGUI::VectorString &DataManager::getDataListNames(const std::string &pattern) OPENMW_MYGUI_CONST_GETTER_3_4_1
+const MyGUI::VectorString &DataManager::getDataListNames(const std::string &pattern) const
{
// TODO: pattern matching (unused?)
static MyGUI::VectorString strings;
@@ -49,7 +49,7 @@ const MyGUI::VectorString &DataManager::getDataListNames(const std::string &patt
return strings;
}
-const std::string &DataManager::getDataPath(const std::string &name) OPENMW_MYGUI_CONST_GETTER_3_4_1
+const std::string &DataManager::getDataPath(const std::string &name) const
{
static std::string result;
result.clear();
diff --git a/components/myguiplatform/myguidatamanager.hpp b/components/myguiplatform/myguidatamanager.hpp
index ca2f94899c..da24763d7b 100644
--- a/components/myguiplatform/myguidatamanager.hpp
+++ b/components/myguiplatform/myguidatamanager.hpp
@@ -3,8 +3,6 @@
#include <MyGUI_DataManager.h>
-#include "myguicompat.h"
-
namespace osgMyGUI
{
@@ -19,7 +17,7 @@ public:
/** Get data stream from specified resource name.
@param _name Resource name (usually file name).
*/
- MyGUI::IDataStream* getData(const std::string& _name) OPENMW_MYGUI_CONST_GETTER_3_4_1 override;
+ MyGUI::IDataStream* getData(const std::string& _name) const override;
/** Free data stream.
@param _data Data stream.
@@ -29,18 +27,18 @@ public:
/** Is data with specified name exist.
@param _name Resource name.
*/
- bool isDataExist(const std::string& _name) OPENMW_MYGUI_CONST_GETTER_3_4_1 override;
+ bool isDataExist(const std::string& _name) const override;
/** Get all data names with names that matches pattern.
@param _pattern Pattern to match (for example "*.layout").
*/
- const MyGUI::VectorString& getDataListNames(const std::string& _pattern) OPENMW_MYGUI_CONST_GETTER_3_4_1 override;
+ const MyGUI::VectorString& getDataListNames(const std::string& _pattern) const override;
/** Get full path to data.
@param _name Resource name.
@return Return full path to specified data.
*/
- const std::string& getDataPath(const std::string& _name) OPENMW_MYGUI_CONST_GETTER_3_4_1 override;
+ const std::string& getDataPath(const std::string& _name) const override;
private:
std::string mResourcePath;
diff --git a/components/myguiplatform/myguirendermanager.cpp b/components/myguiplatform/myguirendermanager.cpp
index 43b176c795..b8ca06a181 100644
--- a/components/myguiplatform/myguirendermanager.cpp
+++ b/components/myguiplatform/myguirendermanager.cpp
@@ -15,7 +15,6 @@
#include <components/shader/shadermanager.hpp>
#include <components/sceneutil/nodecallback.hpp>
-#include "myguicompat.h"
#include "myguitexture.hpp"
#define MYGUI_PLATFORM_LOG_SECTION "Platform"
@@ -276,7 +275,7 @@ public:
osg::VertexBufferObject* getVertexBuffer();
void setVertexCount(size_t count) override;
- size_t getVertexCount() OPENMW_MYGUI_CONST_GETTER_3_4_1 override;
+ size_t getVertexCount() const override;
MyGUI::Vertex *lock() override;
void unlock() override;
@@ -303,7 +302,7 @@ void OSGVertexBuffer::setVertexCount(size_t count)
mNeedVertexCount = count;
}
-size_t OSGVertexBuffer::getVertexCount() OPENMW_MYGUI_CONST_GETTER_3_4_1
+size_t OSGVertexBuffer::getVertexCount() const
{
return mNeedVertexCount;
}
@@ -586,7 +585,6 @@ bool RenderManager::checkTexture(MyGUI::ITexture* _texture)
return true;
}
-#if MYGUI_VERSION > MYGUI_DEFINE_VERSION(3, 4, 0)
void RenderManager::registerShader(
const std::string& _shaderName,
const std::string& _vertexProgramFile,
@@ -594,6 +592,5 @@ void RenderManager::registerShader(
{
MYGUI_PLATFORM_LOG(Warning, "osgMyGUI::RenderManager::registerShader is not implemented");
}
-#endif
}
diff --git a/components/myguiplatform/myguirendermanager.hpp b/components/myguiplatform/myguirendermanager.hpp
index 8ef9691e4f..0d1ad4fb41 100644
--- a/components/myguiplatform/myguirendermanager.hpp
+++ b/components/myguiplatform/myguirendermanager.hpp
@@ -5,8 +5,6 @@
#include <osg/ref_ptr>
-#include "myguicompat.h"
-
namespace Resource
{
class ImageManager;
@@ -79,7 +77,7 @@ public:
const MyGUI::IntSize& getViewSize() const override { return mViewSize; }
/** @see RenderManager::getVertexFormat */
- MyGUI::VertexColourType getVertexFormat() OPENMW_MYGUI_CONST_GETTER_3_4_1 override
+ MyGUI::VertexColourType getVertexFormat() const override
{ return mVertexFormat; }
/** @see RenderManager::isFormatSupported */
@@ -112,21 +110,13 @@ public:
void setInjectState(osg::StateSet* stateSet);
/** @see IRenderTarget::getInfo */
- const MyGUI::RenderTargetInfo& getInfo() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mInfo; }
+ const MyGUI::RenderTargetInfo& getInfo() const override { return mInfo; }
bool checkTexture(MyGUI::ITexture* _texture);
- // setViewSize() is a part of MyGUI::RenderManager interface since 3.4.0 release
-#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3, 4, 0)
- void setViewSize(int width, int height);
-#else
void setViewSize(int width, int height) override;
-#endif
- // registerShader() is a part of MyGUI::RenderManager interface since 3.4.1 release
-#if MYGUI_VERSION > MYGUI_DEFINE_VERSION(3, 4, 0)
void registerShader(const std::string& _shaderName, const std::string& _vertexProgramFile, const std::string& _fragmentProgramFile) override;
-#endif
/*internal:*/
diff --git a/components/myguiplatform/myguitexture.cpp b/components/myguiplatform/myguitexture.cpp
index d0e4e9a86e..d120cb52f3 100644
--- a/components/myguiplatform/myguitexture.cpp
+++ b/components/myguiplatform/myguitexture.cpp
@@ -165,8 +165,6 @@ namespace osgMyGUI
return nullptr;
}
-#if MYGUI_VERSION > MYGUI_DEFINE_VERSION(3, 4, 0)
void OSGTexture::setShader(const std::string& _shaderName)
{ Log(Debug::Warning) << "OSGTexture::setShader is not implemented"; }
-#endif
}
diff --git a/components/myguiplatform/myguitexture.hpp b/components/myguiplatform/myguitexture.hpp
index e8b49eab04..4f7ff8f116 100644
--- a/components/myguiplatform/myguitexture.hpp
+++ b/components/myguiplatform/myguitexture.hpp
@@ -5,12 +5,6 @@
#include <osg/ref_ptr>
-#if MYGUI_VERSION > MYGUI_DEFINE_VERSION(3, 4, 0)
- #define OPENMW_MYGUI_CONST_GETTER_3_4_1 const
-#else
- #define OPENMW_MYGUI_CONST_GETTER_3_4_1
-#endif
-
namespace osg
{
class Image;
@@ -57,21 +51,18 @@ namespace osgMyGUI
void* lock(MyGUI::TextureUsage access) override;
void unlock() override;
- bool isLocked() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mLockedImage.valid(); }
+ bool isLocked() const override { return mLockedImage.valid(); }
- int getWidth() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mWidth; }
- int getHeight() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mHeight; }
+ int getWidth() const override { return mWidth; }
+ int getHeight() const override { return mHeight; }
- MyGUI::PixelFormat getFormat() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mFormat; }
- MyGUI::TextureUsage getUsage() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mUsage; }
- size_t getNumElemBytes() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mNumElemBytes; }
+ MyGUI::PixelFormat getFormat() const override { return mFormat; }
+ MyGUI::TextureUsage getUsage() const override { return mUsage; }
+ size_t getNumElemBytes() const override { return mNumElemBytes; }
MyGUI::IRenderTarget *getRenderTarget() override;
- // setShader() is a part of MyGUI::RenderManager interface since 3.4.1 release
-#if MYGUI_VERSION > MYGUI_DEFINE_VERSION(3, 4, 0)
- void setShader(const std::string& _shaderName) override;
-#endif
+ void setShader(const std::string& _shaderName) override;
/*internal:*/
osg::Texture2D *getTexture() const { return mTexture.get(); }
diff --git a/components/myguiplatform/scalinglayer.cpp b/components/myguiplatform/scalinglayer.cpp
index 51c148253f..75a149c810 100644
--- a/components/myguiplatform/scalinglayer.cpp
+++ b/components/myguiplatform/scalinglayer.cpp
@@ -3,8 +3,6 @@
#include <MyGUI_RenderManager.h>
#include <algorithm>
-#include "myguicompat.h"
-
namespace osgMyGUI
{
@@ -39,7 +37,7 @@ namespace osgMyGUI
mTarget->doRender(_buffer, _texture, _count);
}
- const MyGUI::RenderTargetInfo& getInfo() OPENMW_MYGUI_CONST_GETTER_3_4_1 override
+ const MyGUI::RenderTargetInfo& getInfo() const override
{
mInfo = mTarget->getInfo();
mInfo.hOffset = mHOffset;
diff --git a/components/nif/controller.cpp b/components/nif/controller.cpp
index 704c4928e6..16e6b5e40f 100644
--- a/components/nif/controller.cpp
+++ b/components/nif/controller.cpp
@@ -270,6 +270,15 @@ namespace Nif
nif->getUInt(); // Zero
}
+ void NiControllerManager::read(NIFStream *nif)
+ {
+ Controller::read(nif);
+ mCumulative = nif->getBoolean();
+ unsigned int numSequences = nif->getUInt();
+ nif->skip(4 * numSequences); // Controller sequences
+ nif->skip(4); // Object palette
+ }
+
void NiPoint3Interpolator::read(NIFStream *nif)
{
defaultVal = nif->getVector3();
diff --git a/components/nif/controller.hpp b/components/nif/controller.hpp
index 503710fe90..210eaab217 100644
--- a/components/nif/controller.hpp
+++ b/components/nif/controller.hpp
@@ -184,6 +184,12 @@ struct bhkBlendController : public Controller
void read(NIFStream *nif) override;
};
+struct NiControllerManager : public Controller
+{
+ bool mCumulative;
+ void read(NIFStream *nif) override;
+};
+
struct Interpolator : public Record { };
struct NiPoint3Interpolator : public Interpolator
diff --git a/components/nif/data.cpp b/components/nif/data.cpp
index b6674611bc..1b6d302d68 100644
--- a/components/nif/data.cpp
+++ b/components/nif/data.cpp
@@ -34,6 +34,13 @@ void NiSkinInstance::post(NIFFile *nif)
}
}
+void BSDismemberSkinInstance::read(NIFStream *nif)
+{
+ NiSkinInstance::read(nif);
+ unsigned int numPartitions = nif->getUInt();
+ nif->skip(4 * numPartitions); // Body part information
+}
+
void NiGeometryData::read(NIFStream *nif)
{
if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,114))
@@ -421,7 +428,7 @@ void NiMorphData::read(NIFStream *nif)
for(int i = 0;i < morphCount;i++)
{
mMorphs[i].mKeyFrames = std::make_shared<FloatKeyMap>();
- mMorphs[i].mKeyFrames->read(nif, true, /*morph*/true);
+ mMorphs[i].mKeyFrames->read(nif, /*morph*/true);
nif->getVector3s(mMorphs[i].mVertices, vertCount);
}
}
@@ -438,9 +445,9 @@ void NiKeyframeData::read(NIFStream *nif)
mXRotations = std::make_shared<FloatKeyMap>();
mYRotations = std::make_shared<FloatKeyMap>();
mZRotations = std::make_shared<FloatKeyMap>();
- mXRotations->read(nif, true);
- mYRotations->read(nif, true);
- mZRotations->read(nif, true);
+ mXRotations->read(nif);
+ mYRotations->read(nif);
+ mZRotations->read(nif);
}
mTranslations = std::make_shared<Vector3KeyMap>();
mTranslations->read(nif);
diff --git a/components/nif/data.hpp b/components/nif/data.hpp
index efbe138c53..70171ac47c 100644
--- a/components/nif/data.hpp
+++ b/components/nif/data.hpp
@@ -170,6 +170,11 @@ struct NiSkinInstance : public Record
void post(NIFFile *nif) override;
};
+struct BSDismemberSkinInstance : public NiSkinInstance
+{
+ void read(NIFStream *nif) override;
+};
+
struct NiSkinData : public Record
{
struct VertWeight
diff --git a/components/nif/extra.cpp b/components/nif/extra.cpp
index a45ea8c50b..04217995ac 100644
--- a/components/nif/extra.cpp
+++ b/components/nif/extra.cpp
@@ -94,4 +94,38 @@ void BSBound::read(NIFStream *nif)
halfExtents = nif->getVector3();
}
+void BSFurnitureMarker::LegacyFurniturePosition::read(NIFStream *nif)
+{
+ mOffset = nif->getVector3();
+ mOrientation = nif->getUShort();
+ mPositionRef = nif->getChar();
+ nif->skip(1); // Position ref 2
+}
+
+void BSFurnitureMarker::FurniturePosition::read(NIFStream *nif)
+{
+ mOffset = nif->getVector3();
+ mHeading = nif->getFloat();
+ mType = nif->getUShort();
+ mEntryPoint = nif->getUShort();
+}
+
+void BSFurnitureMarker::read(NIFStream *nif)
+{
+ Extra::read(nif);
+ unsigned int num = nif->getUInt();
+ if (nif->getBethVersion() <= NIFFile::BethVersion::BETHVER_FO3)
+ {
+ mLegacyMarkers.resize(num);
+ for (auto& marker : mLegacyMarkers)
+ marker.read(nif);
+ }
+ else
+ {
+ mMarkers.resize(num);
+ for (auto& marker : mMarkers)
+ marker.read(nif);
+ }
+}
+
}
diff --git a/components/nif/extra.hpp b/components/nif/extra.hpp
index f4ac1caff9..8eb14f9b01 100644
--- a/components/nif/extra.hpp
+++ b/components/nif/extra.hpp
@@ -120,5 +120,30 @@ struct BSBound : public Extra
void read(NIFStream *nif) override;
};
+struct BSFurnitureMarker : public Extra
+{
+ struct LegacyFurniturePosition
+ {
+ osg::Vec3f mOffset;
+ uint16_t mOrientation;
+ uint8_t mPositionRef;
+ void read(NIFStream *nif);
+ };
+
+ struct FurniturePosition
+ {
+ osg::Vec3f mOffset;
+ float mHeading;
+ uint16_t mType;
+ uint16_t mEntryPoint;
+ void read(NIFStream *nif);
+ };
+
+ std::vector<LegacyFurniturePosition> mLegacyMarkers;
+ std::vector<FurniturePosition> mMarkers;
+
+ void read(NIFStream *nif) override;
+};
+
} // Namespace
#endif
diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp
index 6863209988..1a1bbd7217 100644
--- a/components/nif/niffile.cpp
+++ b/components/nif/niffile.cpp
@@ -1,6 +1,8 @@
#include "niffile.hpp"
#include "effect.hpp"
+#include <components/files/hash.hpp>
+
#include <array>
#include <map>
#include <sstream>
@@ -136,6 +138,24 @@ static std::map<std::string,RecordFactoryEntry> makeFactory()
factory["BSShaderProperty"] = {&construct <BSShaderProperty> , RC_BSShaderProperty };
factory["BSShaderPPLightingProperty"] = {&construct <BSShaderPPLightingProperty> , RC_BSShaderPPLightingProperty };
factory["BSShaderNoLightingProperty"] = {&construct <BSShaderNoLightingProperty> , RC_BSShaderNoLightingProperty };
+ factory["BSFurnitureMarker"] = {&construct <BSFurnitureMarker> , RC_BSFurnitureMarker };
+ factory["NiCollisionObject"] = {&construct <NiCollisionObject> , RC_NiCollisionObject };
+ factory["bhkCollisionObject"] = {&construct <bhkCollisionObject> , RC_bhkCollisionObject };
+ factory["BSDismemberSkinInstance"] = {&construct <BSDismemberSkinInstance> , RC_BSDismemberSkinInstance };
+ factory["NiControllerManager"] = {&construct <NiControllerManager> , RC_NiControllerManager };
+ factory["bhkMoppBvTreeShape"] = {&construct <bhkMoppBvTreeShape> , RC_bhkMoppBvTreeShape };
+ factory["bhkNiTriStripsShape"] = {&construct <bhkNiTriStripsShape> , RC_bhkNiTriStripsShape };
+ factory["bhkPackedNiTriStripsShape"] = {&construct <bhkPackedNiTriStripsShape> , RC_bhkPackedNiTriStripsShape };
+ factory["hkPackedNiTriStripsData"] = {&construct <hkPackedNiTriStripsData> , RC_hkPackedNiTriStripsData };
+ factory["bhkConvexVerticesShape"] = {&construct <bhkConvexVerticesShape> , RC_bhkConvexVerticesShape };
+ factory["bhkBoxShape"] = {&construct <bhkBoxShape> , RC_bhkBoxShape };
+ factory["bhkListShape"] = {&construct <bhkListShape> , RC_bhkListShape };
+ factory["bhkRigidBody"] = {&construct <bhkRigidBody> , RC_bhkRigidBody };
+ factory["bhkRigidBodyT"] = {&construct <bhkRigidBody> , RC_bhkRigidBodyT };
+ factory["BSLightingShaderProperty"] = {&construct <BSLightingShaderProperty> , RC_BSLightingShaderProperty };
+ factory["NiSortAdjustNode"] = {&construct <NiSortAdjustNode> , RC_NiNode };
+ factory["NiClusterAccumulator"] = {&construct <NiClusterAccumulator> , RC_NiClusterAccumulator };
+ factory["NiAlphaAccumulator"] = {&construct <NiAlphaAccumulator> , RC_NiAlphaAccumulator };
return factory;
}
@@ -156,6 +176,9 @@ std::string NIFFile::printVersion(unsigned int version)
void NIFFile::parse(Files::IStreamPtr stream)
{
+ const std::array<std::uint64_t, 2> fileHash = Files::getHash(filename, *stream);
+ hash.append(reinterpret_cast<const char*>(fileHash.data()), fileHash.size() * sizeof(std::uint64_t));
+
NIFStream nif (this, stream);
// Check the header string
diff --git a/components/nif/niffile.hpp b/components/nif/niffile.hpp
index 1ed7cbd5d8..6884f51d58 100644
--- a/components/nif/niffile.hpp
+++ b/components/nif/niffile.hpp
@@ -34,6 +34,8 @@ struct File
virtual std::string getFilename() const = 0;
+ virtual std::string getHash() const = 0;
+
virtual unsigned int getVersion() const = 0;
virtual unsigned int getUserVersion() const = 0;
@@ -50,6 +52,7 @@ class NIFFile final : public File
/// File name, used for error messages and opening the file
std::string filename;
+ std::string hash;
/// Record list
std::vector<Record*> records;
@@ -141,6 +144,8 @@ public:
/// Get the name of the file
std::string getFilename() const override { return filename; }
+ std::string getHash() const override { return hash; }
+
/// Get the version of the NIF format used
unsigned int getVersion() const override { return ver; }
diff --git a/components/nif/nifkey.hpp b/components/nif/nifkey.hpp
index 91869ff849..7736ab7ad9 100644
--- a/components/nif/nifkey.hpp
+++ b/components/nif/nifkey.hpp
@@ -48,93 +48,72 @@ struct KeyMapT {
using ValueType = T;
using KeyType = KeyT<T>;
- unsigned int mInterpolationType = InterpolationType_Linear;
+ unsigned int mInterpolationType = InterpolationType_Unknown;
MapType mKeys;
//Read in a KeyGroup (see http://niftools.sourceforge.net/doc/nif/NiKeyframeData.html)
- void read(NIFStream *nif, bool force = false, bool morph = false)
+ void read(NIFStream *nif, bool morph = false)
{
assert(nif);
- mInterpolationType = InterpolationType_Unknown;
-
if (morph && nif->getVersion() >= NIFStream::generateVersion(10,1,0,106))
nif->getString(); // Frame name
size_t count = nif->getUInt();
- if (count == 0 && !force && !morph)
- return;
-
- if (morph && nif->getVersion() > NIFStream::generateVersion(10,1,0,0))
- {
- if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,104) &&
- nif->getVersion() <= NIFStream::generateVersion(20,1,0,2) && nif->getBethVersion() < 10)
- nif->getFloat(); // Legacy weight
- return;
- }
- mKeys.clear();
-
- mInterpolationType = nif->getUInt();
+ if (count != 0 || morph)
+ mInterpolationType = nif->getUInt();
KeyType key = {};
- NIFStream &nifReference = *nif;
- if (mInterpolationType == InterpolationType_Linear
- || mInterpolationType == InterpolationType_Constant)
+ if (mInterpolationType == InterpolationType_Linear || mInterpolationType == InterpolationType_Constant)
{
- for(size_t i = 0;i < count;i++)
+ for (size_t i = 0;i < count;i++)
{
float time = nif->getFloat();
- readValue(nifReference, key);
+ readValue(*nif, key);
mKeys[time] = key;
}
}
else if (mInterpolationType == InterpolationType_Quadratic)
{
- for(size_t i = 0;i < count;i++)
+ for (size_t i = 0;i < count;i++)
{
float time = nif->getFloat();
- readQuadratic(nifReference, key);
+ readQuadratic(*nif, key);
mKeys[time] = key;
}
}
else if (mInterpolationType == InterpolationType_TBC)
{
- for(size_t i = 0;i < count;i++)
+ for (size_t i = 0;i < count;i++)
{
float time = nif->getFloat();
- readTBC(nifReference, key);
+ readTBC(*nif, key);
mKeys[time] = key;
}
}
- //XYZ keys aren't actually read here.
- //data.hpp sees that the last type read was InterpolationType_XYZ and:
- // Eats a floating point number, then
- // Re-runs the read function 3 more times.
- // When it does that it's reading in a bunch of InterpolationType_Linear keys, not InterpolationType_XYZ.
- else if(mInterpolationType == InterpolationType_XYZ)
- {
- //Don't try to read XYZ keys into the wrong part
- if ( count != 1 )
- {
- std::stringstream error;
- error << "XYZ_ROTATION_KEY count should always be '1' . Retrieved Value: "
- << count;
- nif->file->fail(error.str());
- }
- }
- else if (mInterpolationType == InterpolationType_Unknown)
+ else if (mInterpolationType == InterpolationType_XYZ)
{
- if (count != 0)
- nif->file->fail("Interpolation type 0 doesn't work with keys");
+ //XYZ keys aren't actually read here.
+ //data.cpp sees that the last type read was InterpolationType_XYZ and:
+ // Eats a floating point number, then
+ // Re-runs the read function 3 more times.
+ // When it does that it's reading in a bunch of InterpolationType_Linear keys, not InterpolationType_XYZ.
}
- else
+ else if (count != 0)
{
std::stringstream error;
error << "Unhandled interpolation type: " << mInterpolationType;
nif->file->fail(error.str());
}
+
+ if (morph && nif->getVersion() > NIFStream::generateVersion(10,1,0,0))
+ {
+ if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,104) &&
+ nif->getVersion() <= NIFStream::generateVersion(20,1,0,2) && nif->getBethVersion() < 10)
+ nif->getFloat(); // Legacy weight
+ }
}
private:
diff --git a/components/nif/nifstream.hpp b/components/nif/nifstream.hpp
index b6bf01ce58..04243c2506 100644
--- a/components/nif/nifstream.hpp
+++ b/components/nif/nifstream.hpp
@@ -149,6 +149,9 @@ public:
inp->read(str.data(), length);
if (inp->bad())
throw std::runtime_error("Failed to read sized string of " + std::to_string(length) + " chars");
+ size_t end = str.find('\0');
+ if (end != std::string::npos)
+ str.erase(end);
return str;
}
///Read in a string of the length specified in the file
diff --git a/components/nif/node.hpp b/components/nif/node.hpp
index 406a4d4549..7e5bcdb3be 100644
--- a/components/nif/node.hpp
+++ b/components/nif/node.hpp
@@ -8,6 +8,7 @@
#include "niftypes.hpp"
#include "controller.hpp"
#include "base.hpp"
+#include "physics.hpp"
#include <components/misc/stringops.hpp>
@@ -143,6 +144,9 @@ struct Node : public Named
bool hasBounds{false};
NiBoundingVolume bounds;
+ // Collision object info
+ NiCollisionObjectPtr collision;
+
void read(NIFStream *nif) override
{
Named::read(nif);
@@ -160,7 +164,7 @@ struct Node : public Named
bounds.read(nif);
// Reference to the collision object in Gamebryo files.
if (nif->getVersion() >= NIFStream::generateVersion(10,0,1,0))
- nif->skip(4);
+ collision.read(nif);
parent = nullptr;
@@ -171,6 +175,7 @@ struct Node : public Named
{
Named::post(nif);
props.post(nif);
+ collision.post(nif);
}
// Parent node, or nullptr for the root node. As far as I'm aware, only
@@ -422,5 +427,39 @@ struct NiLODNode : public NiSwitchNode
}
};
+// Abstract
+struct NiAccumulator : Record
+{
+ void read(NIFStream *nif) override {}
+};
+
+// Node children sorters
+struct NiClusterAccumulator : NiAccumulator {};
+struct NiAlphaAccumulator : NiClusterAccumulator {};
+
+struct NiSortAdjustNode : NiNode
+{
+ enum SortingMode
+ {
+ SortingMode_Inherit,
+ SortingMode_Off,
+ SortingMode_Subsort
+ };
+
+ int mMode;
+ NiAccumulatorPtr mSubSorter;
+ void read(NIFStream *nif) override
+ {
+ NiNode::read(nif);
+ mMode = nif->getInt();
+ if (nif->getVersion() <= NIFStream::generateVersion(20,0,0,3))
+ mSubSorter.read(nif);
+ }
+ void post(NIFFile *nif) override
+ {
+ mSubSorter.post(nif);
+ }
+};
+
} // Namespace
#endif
diff --git a/components/nif/physics.cpp b/components/nif/physics.cpp
new file mode 100644
index 0000000000..9bbeb148dd
--- /dev/null
+++ b/components/nif/physics.cpp
@@ -0,0 +1,313 @@
+#include "physics.hpp"
+#include "node.hpp"
+
+namespace Nif
+{
+
+ /// Non-record data types
+
+ void bhkWorldObjCInfoProperty::read(NIFStream *nif)
+ {
+ mData = nif->getUInt();
+ mSize = nif->getUInt();
+ mCapacityAndFlags = nif->getUInt();
+ }
+
+ void bhkWorldObjectCInfo::read(NIFStream *nif)
+ {
+ nif->skip(4); // Unused
+ mPhaseType = static_cast<BroadPhaseType>(nif->getChar());
+ nif->skip(3); // Unused
+ mProperty.read(nif);
+ }
+
+ void HavokMaterial::read(NIFStream *nif)
+ {
+ if (nif->getVersion() <= NIFFile::NIFVersion::VER_OB_OLD)
+ nif->skip(4); // Unknown
+ mMaterial = nif->getUInt();
+ }
+
+ void HavokFilter::read(NIFStream *nif)
+ {
+ mLayer = nif->getChar();
+ mFlags = nif->getChar();
+ mGroup = nif->getUShort();
+ }
+
+ void hkSubPartData::read(NIFStream *nif)
+ {
+ mHavokFilter.read(nif);
+ mNumVertices = nif->getUInt();
+ mHavokMaterial.read(nif);
+ }
+
+ void hkpMoppCode::read(NIFStream *nif)
+ {
+ unsigned int size = nif->getUInt();
+ if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,0))
+ mOffset = nif->getVector4();
+ if (nif->getBethVersion() > NIFFile::BethVersion::BETHVER_FO3)
+ nif->getChar(); // MOPP data build type
+ if (size)
+ nif->getChars(mData, size);
+ }
+
+ void bhkEntityCInfo::read(NIFStream *nif)
+ {
+ mResponseType = static_cast<hkResponseType>(nif->getChar());
+ nif->skip(1); // Unused
+ mProcessContactDelay = nif->getUShort();
+ }
+
+ void TriangleData::read(NIFStream *nif)
+ {
+ for (int i = 0; i < 3; i++)
+ mTriangle[i] = nif->getUShort();
+ mWeldingInfo = nif->getUShort();
+ if (nif->getVersion() <= NIFFile::NIFVersion::VER_OB)
+ mNormal = nif->getVector3();
+ }
+
+ void bhkRigidBodyCInfo::read(NIFStream *nif)
+ {
+ if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,0))
+ {
+ nif->skip(4); // Unused
+ mHavokFilter.read(nif);
+ nif->skip(4); // Unused
+ if (nif->getBethVersion() != NIFFile::BethVersion::BETHVER_FO4)
+ {
+ if (nif->getBethVersion() >= 83)
+ nif->skip(4); // Unused
+ mResponseType = static_cast<hkResponseType>(nif->getChar());
+ nif->skip(1); // Unused
+ mProcessContactDelay = nif->getUShort();
+ }
+ }
+ if (nif->getBethVersion() < 83)
+ nif->skip(4); // Unused
+ mTranslation = nif->getVector4();
+ mRotation = nif->getQuaternion();
+ mLinearVelocity = nif->getVector4();
+ mAngularVelocity = nif->getVector4();
+ for (int i = 0; i < 3; i++)
+ for (int j = 0; j < 4; j++)
+ mInertiaTensor[i][j] = nif->getFloat();
+ mCenter = nif->getVector4();
+ mMass = nif->getFloat();
+ mLinearDamping = nif->getFloat();
+ mAngularDamping = nif->getFloat();
+ if (nif->getBethVersion() >= 83)
+ {
+ if (nif->getBethVersion() != NIFFile::BethVersion::BETHVER_FO4)
+ mTimeFactor = nif->getFloat();
+ mGravityFactor = nif->getFloat();
+ }
+ mFriction = nif->getFloat();
+ if (nif->getBethVersion() >= 83)
+ mRollingFrictionMult = nif->getFloat();
+ mRestitution = nif->getFloat();
+ if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,0))
+ {
+ mMaxLinearVelocity = nif->getFloat();
+ mMaxAngularVelocity = nif->getFloat();
+ if (nif->getBethVersion() != NIFFile::BethVersion::BETHVER_FO4)
+ mPenetrationDepth = nif->getFloat();
+ }
+ mMotionType = static_cast<hkMotionType>(nif->getChar());
+ if (nif->getBethVersion() < 83)
+ mDeactivatorType = static_cast<hkDeactivatorType>(nif->getChar());
+ else
+ mEnableDeactivation = nif->getBoolean();
+ mSolverDeactivation = static_cast<hkSolverDeactivation>(nif->getChar());
+ if (nif->getBethVersion() == NIFFile::BethVersion::BETHVER_FO4)
+ {
+ nif->skip(1);
+ mPenetrationDepth = nif->getFloat();
+ mTimeFactor = nif->getFloat();
+ nif->skip(4);
+ mResponseType = static_cast<hkResponseType>(nif->getChar());
+ nif->skip(1); // Unused
+ mProcessContactDelay = nif->getUShort();
+ }
+ mQualityType = static_cast<hkQualityType>(nif->getChar());
+ if (nif->getBethVersion() >= 83)
+ {
+ mAutoRemoveLevel = nif->getChar();
+ mResponseModifierFlags = nif->getChar();
+ mNumContactPointShapeKeys = nif->getChar();
+ mForceCollidedOntoPPU = nif->getBoolean();
+ }
+ if (nif->getBethVersion() == NIFFile::BethVersion::BETHVER_FO4)
+ nif->skip(3); // Unused
+ else
+ nif->skip(12); // Unused
+ }
+
+ /// Record types
+
+ void bhkCollisionObject::read(NIFStream *nif)
+ {
+ NiCollisionObject::read(nif);
+ mFlags = nif->getUShort();
+ mBody.read(nif);
+ }
+
+ void bhkWorldObject::read(NIFStream *nif)
+ {
+ mShape.read(nif);
+ if (nif->getVersion() <= NIFFile::NIFVersion::VER_OB_OLD)
+ nif->skip(4); // Unknown
+ mHavokFilter.read(nif);
+ mWorldObjectInfo.read(nif);
+ }
+
+ void bhkWorldObject::post(NIFFile *nif)
+ {
+ mShape.post(nif);
+ }
+
+ void bhkEntity::read(NIFStream *nif)
+ {
+ bhkWorldObject::read(nif);
+ mInfo.read(nif);
+ }
+
+ void bhkBvTreeShape::read(NIFStream *nif)
+ {
+ mShape.read(nif);
+ }
+
+ void bhkBvTreeShape::post(NIFFile *nif)
+ {
+ mShape.post(nif);
+ }
+
+ void bhkMoppBvTreeShape::read(NIFStream *nif)
+ {
+ bhkBvTreeShape::read(nif);
+ nif->skip(12); // Unused
+ mScale = nif->getFloat();
+ mMopp.read(nif);
+ }
+
+ void bhkNiTriStripsShape::read(NIFStream *nif)
+ {
+ mHavokMaterial.read(nif);
+ mRadius = nif->getFloat();
+ nif->skip(20); // Unused
+ mGrowBy = nif->getUInt();
+ if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,0))
+ mScale = nif->getVector4();
+ mData.read(nif);
+ unsigned int numFilters = nif->getUInt();
+ nif->getUInts(mFilters, numFilters);
+ }
+
+ void bhkNiTriStripsShape::post(NIFFile *nif)
+ {
+ mData.post(nif);
+ }
+
+ void bhkPackedNiTriStripsShape::read(NIFStream *nif)
+ {
+ if (nif->getVersion() <= NIFFile::NIFVersion::VER_OB)
+ {
+ mSubshapes.resize(nif->getUShort());
+ for (hkSubPartData& subshape : mSubshapes)
+ subshape.read(nif);
+ }
+ mUserData = nif->getUInt();
+ nif->skip(4); // Unused
+ mRadius = nif->getFloat();
+ nif->skip(4); // Unused
+ mScale = nif->getVector4();
+ nif->skip(20); // Duplicates of the two previous fields
+ mData.read(nif);
+ }
+
+ void bhkPackedNiTriStripsShape::post(NIFFile *nif)
+ {
+ mData.post(nif);
+ }
+
+ void hkPackedNiTriStripsData::read(NIFStream *nif)
+ {
+ unsigned int numTriangles = nif->getUInt();
+ mTriangles.resize(numTriangles);
+ for (unsigned int i = 0; i < numTriangles; i++)
+ mTriangles[i].read(nif);
+
+ unsigned int numVertices = nif->getUInt();
+ bool compressed = false;
+ if (nif->getVersion() >= NIFFile::NIFVersion::VER_BGS)
+ compressed = nif->getBoolean();
+ if (!compressed)
+ nif->getVector3s(mVertices, numVertices);
+ else
+ nif->skip(6 * numVertices); // Half-precision vectors are not currently supported
+ if (nif->getVersion() >= NIFFile::NIFVersion::VER_BGS)
+ {
+ mSubshapes.resize(nif->getUShort());
+ for (hkSubPartData& subshape : mSubshapes)
+ subshape.read(nif);
+ }
+ }
+
+ void bhkSphereRepShape::read(NIFStream *nif)
+ {
+ mHavokMaterial.read(nif);
+ }
+
+ void bhkConvexShape::read(NIFStream *nif)
+ {
+ bhkSphereRepShape::read(nif);
+ mRadius = nif->getFloat();
+ }
+
+ void bhkConvexVerticesShape::read(NIFStream *nif)
+ {
+ bhkConvexShape::read(nif);
+ mVerticesProperty.read(nif);
+ mNormalsProperty.read(nif);
+ unsigned int numVertices = nif->getUInt();
+ if (numVertices)
+ nif->getVector4s(mVertices, numVertices);
+ unsigned int numNormals = nif->getUInt();
+ if (numNormals)
+ nif->getVector4s(mNormals, numNormals);
+ }
+
+ void bhkBoxShape::read(NIFStream *nif)
+ {
+ bhkConvexShape::read(nif);
+ nif->skip(8); // Unused
+ mExtents = nif->getVector3();
+ nif->skip(4); // Unused
+ }
+
+ void bhkListShape::read(NIFStream *nif)
+ {
+ mSubshapes.read(nif);
+ mHavokMaterial.read(nif);
+ mChildShapeProperty.read(nif);
+ mChildFilterProperty.read(nif);
+ unsigned int numFilters = nif->getUInt();
+ mHavokFilters.resize(numFilters);
+ for (HavokFilter& filter : mHavokFilters)
+ filter.read(nif);
+ }
+
+ void bhkRigidBody::read(NIFStream *nif)
+ {
+ bhkEntity::read(nif);
+ mInfo.read(nif);
+ mConstraints.read(nif);
+ if (nif->getBethVersion() < 76)
+ mBodyFlags = nif->getUInt();
+ else
+ mBodyFlags = nif->getUShort();
+ }
+
+} // Namespace \ No newline at end of file
diff --git a/components/nif/physics.hpp b/components/nif/physics.hpp
new file mode 100644
index 0000000000..613ec0ba43
--- /dev/null
+++ b/components/nif/physics.hpp
@@ -0,0 +1,332 @@
+#ifndef OPENMW_COMPONENTS_NIF_PHYSICS_HPP
+#define OPENMW_COMPONENTS_NIF_PHYSICS_HPP
+
+#include "base.hpp"
+
+// This header contains certain record definitions
+// specific to Bethesda implementation of Havok physics
+namespace Nif
+{
+
+/// Non-record data types
+
+struct bhkWorldObjCInfoProperty
+{
+ unsigned int mData;
+ unsigned int mSize;
+ unsigned int mCapacityAndFlags;
+ void read(NIFStream *nif);
+};
+
+enum class BroadPhaseType : uint8_t
+{
+ BroadPhase_Invalid = 0,
+ BroadPhase_Entity = 1,
+ BroadPhase_Phantom = 2,
+ BroadPhase_Border = 3
+};
+
+struct bhkWorldObjectCInfo
+{
+ BroadPhaseType mPhaseType;
+ bhkWorldObjCInfoProperty mProperty;
+ void read(NIFStream *nif);
+};
+
+struct HavokMaterial
+{
+ unsigned int mMaterial;
+ void read(NIFStream *nif);
+};
+
+struct HavokFilter
+{
+ unsigned char mLayer;
+ unsigned char mFlags;
+ unsigned short mGroup;
+ void read(NIFStream *nif);
+};
+
+struct hkSubPartData
+{
+ HavokMaterial mHavokMaterial;
+ unsigned int mNumVertices;
+ HavokFilter mHavokFilter;
+ void read(NIFStream *nif);
+};
+
+enum class hkResponseType : uint8_t
+{
+ Response_Invalid = 0,
+ Response_SimpleContact = 1,
+ Response_Reporting = 2,
+ Response_None = 3
+};
+
+struct bhkEntityCInfo
+{
+ hkResponseType mResponseType;
+ unsigned short mProcessContactDelay;
+ void read(NIFStream *nif);
+};
+
+struct hkpMoppCode
+{
+ osg::Vec4f mOffset;
+ std::vector<char> mData;
+ void read(NIFStream *nif);
+};
+
+struct TriangleData
+{
+ unsigned short mTriangle[3];
+ unsigned short mWeldingInfo;
+ osg::Vec3f mNormal;
+ void read(NIFStream *nif);
+};
+
+enum class hkMotionType : uint8_t
+{
+ Motion_Invalid = 0,
+ Motion_Dynamic = 1,
+ Motion_SphereInertia = 2,
+ Motion_SphereStabilized = 3,
+ Motion_BoxInertia = 4,
+ Motion_BoxStabilized = 5,
+ Motion_Keyframed = 6,
+ Motion_Fixed = 7,
+ Motion_ThinBox = 8,
+ Motion_Character = 9
+};
+
+enum class hkDeactivatorType : uint8_t
+{
+ Deactivator_Invalid = 0,
+ Deactivator_Never = 1,
+ Deactivator_Spatial = 2
+};
+
+enum class hkSolverDeactivation : uint8_t
+{
+ SolverDeactivation_Invalid = 0,
+ SolverDeactivation_Off = 1,
+ SolverDeactivation_Low = 2,
+ SolverDeactivation_Medium = 3,
+ SolverDeactivation_High = 4,
+ SolverDeactivation_Max = 5
+};
+
+enum class hkQualityType : uint8_t
+{
+ Quality_Invalid = 0,
+ Quality_Fixed = 1,
+ Quality_Keyframed = 2,
+ Quality_Debris = 3,
+ Quality_Moving = 4,
+ Quality_Critical = 5,
+ Quality_Bullet = 6,
+ Quality_User = 7,
+ Quality_Character = 8,
+ Quality_KeyframedReport = 9
+};
+
+struct bhkRigidBodyCInfo
+{
+ HavokFilter mHavokFilter;
+ hkResponseType mResponseType;
+ unsigned short mProcessContactDelay;
+ osg::Vec4f mTranslation;
+ osg::Quat mRotation;
+ osg::Vec4f mLinearVelocity;
+ osg::Vec4f mAngularVelocity;
+ float mInertiaTensor[3][4];
+ osg::Vec4f mCenter;
+ float mMass;
+ float mLinearDamping;
+ float mAngularDamping;
+ float mTimeFactor{1.f};
+ float mGravityFactor{1.f};
+ float mFriction;
+ float mRollingFrictionMult;
+ float mRestitution;
+ float mMaxLinearVelocity;
+ float mMaxAngularVelocity;
+ float mPenetrationDepth;
+ hkMotionType mMotionType;
+ hkDeactivatorType mDeactivatorType;
+ bool mEnableDeactivation{true};
+ hkSolverDeactivation mSolverDeactivation;
+ hkQualityType mQualityType;
+ unsigned char mAutoRemoveLevel;
+ unsigned char mResponseModifierFlags;
+ unsigned char mNumContactPointShapeKeys;
+ bool mForceCollidedOntoPPU;
+ void read(NIFStream *nif);
+};
+
+/// Record types
+
+// Abstract Bethesda Havok object
+struct bhkRefObject : public Record {};
+
+// Abstract serializable Bethesda Havok object
+struct bhkSerializable : public bhkRefObject {};
+
+// Abstract narrowphase collision detection object
+struct bhkShape : public bhkSerializable {};
+
+// Abstract bhkShape collection
+struct bhkShapeCollection : public bhkShape {};
+
+// Generic collision object
+struct NiCollisionObject : public Record
+{
+ // The node that references this object
+ NodePtr mTarget;
+
+ void read(NIFStream *nif) override
+ {
+ mTarget.read(nif);
+ }
+ void post(NIFFile *nif) override
+ {
+ mTarget.post(nif);
+ }
+};
+
+// Bethesda Havok-specific collision object
+struct bhkCollisionObject : public NiCollisionObject
+{
+ unsigned short mFlags;
+ bhkWorldObjectPtr mBody;
+
+ void read(NIFStream *nif) override;
+ void post(NIFFile *nif) override
+ {
+ NiCollisionObject::post(nif);
+ mBody.post(nif);
+ }
+};
+
+// Abstract Havok shape info record
+struct bhkWorldObject : public bhkSerializable
+{
+ bhkShapePtr mShape;
+ HavokFilter mHavokFilter;
+ bhkWorldObjectCInfo mWorldObjectInfo;
+ void read(NIFStream *nif) override;
+ void post(NIFFile *nif) override;
+};
+
+// Abstract
+struct bhkEntity : public bhkWorldObject
+{
+ bhkEntityCInfo mInfo;
+ void read(NIFStream *nif) override;
+};
+
+// Bethesda extension of hkpBvTreeShape
+// hkpBvTreeShape adds a bounding volume tree to an hkpShapeCollection
+struct bhkBvTreeShape : public bhkShape
+{
+ bhkShapePtr mShape;
+ void read(NIFStream *nif) override;
+ void post(NIFFile *nif) override;
+};
+
+// bhkBvTreeShape with Havok MOPP code
+struct bhkMoppBvTreeShape : public bhkBvTreeShape
+{
+ float mScale;
+ hkpMoppCode mMopp;
+ void read(NIFStream *nif) override;
+};
+
+// Bethesda triangle strip-based Havok shape collection
+struct bhkNiTriStripsShape : public bhkShape
+{
+ HavokMaterial mHavokMaterial;
+ float mRadius;
+ unsigned int mGrowBy;
+ osg::Vec4f mScale{1.f, 1.f, 1.f, 0.f};
+ NiTriStripsDataList mData;
+ std::vector<unsigned int> mFilters;
+ void read(NIFStream *nif) override;
+ void post(NIFFile *nif) override;
+};
+
+// Bethesda packed triangle strip-based Havok shape collection
+struct bhkPackedNiTriStripsShape : public bhkShapeCollection
+{
+ std::vector<hkSubPartData> mSubshapes;
+ unsigned int mUserData;
+ float mRadius;
+ osg::Vec4f mScale;
+ hkPackedNiTriStripsDataPtr mData;
+
+ void read(NIFStream *nif) override;
+ void post(NIFFile *nif) override;
+};
+
+// bhkPackedNiTriStripsShape data block
+struct hkPackedNiTriStripsData : public bhkShapeCollection
+{
+ std::vector<TriangleData> mTriangles;
+ std::vector<osg::Vec3f> mVertices;
+ std::vector<hkSubPartData> mSubshapes;
+ void read(NIFStream *nif) override;
+};
+
+// Abstract
+struct bhkSphereRepShape : public bhkShape
+{
+ HavokMaterial mHavokMaterial;
+ void read(NIFStream *nif) override;
+};
+
+// Abstract
+struct bhkConvexShape : public bhkSphereRepShape
+{
+ float mRadius;
+ void read(NIFStream *nif) override;
+};
+
+// A convex shape built from vertices
+struct bhkConvexVerticesShape : public bhkConvexShape
+{
+ bhkWorldObjCInfoProperty mVerticesProperty;
+ bhkWorldObjCInfoProperty mNormalsProperty;
+ std::vector<osg::Vec4f> mVertices;
+ std::vector<osg::Vec4f> mNormals;
+ void read(NIFStream *nif) override;
+};
+
+// A box
+struct bhkBoxShape : public bhkConvexShape
+{
+ osg::Vec3f mExtents;
+ void read(NIFStream *nif) override;
+};
+
+// A list of shapes
+struct bhkListShape : public bhkShapeCollection
+{
+ bhkShapeList mSubshapes;
+ HavokMaterial mHavokMaterial;
+ bhkWorldObjCInfoProperty mChildShapeProperty;
+ bhkWorldObjCInfoProperty mChildFilterProperty;
+ std::vector<HavokFilter> mHavokFilters;
+ void read(NIFStream *nif) override;
+};
+
+struct bhkRigidBody : public bhkEntity
+{
+ bhkRigidBodyCInfo mInfo;
+ bhkSerializableList mConstraints;
+ unsigned int mBodyFlags;
+
+ void read(NIFStream *nif) override;
+};
+
+} // Namespace
+#endif \ No newline at end of file
diff --git a/components/nif/property.cpp b/components/nif/property.cpp
index 1d2dd885d4..ee47e8ccbe 100644
--- a/components/nif/property.cpp
+++ b/components/nif/property.cpp
@@ -146,6 +146,62 @@ void BSShaderNoLightingProperty::read(NIFStream *nif)
falloffParams = nif->getVector4();
}
+void BSLightingShaderProperty::read(NIFStream *nif)
+{
+ type = nif->getUInt();
+ BSShaderProperty::read(nif);
+ flags1 = nif->getUInt();
+ flags2 = nif->getUInt();
+ nif->skip(8); // UV offset
+ nif->skip(8); // UV scale
+ mTextureSet.read(nif);
+ mEmissive = nif->getVector3();
+ mEmissiveMult = nif->getFloat();
+ mClamp = nif->getUInt();
+ mAlpha = nif->getFloat();
+ nif->getFloat(); // Refraction strength
+ mGlossiness = nif->getFloat();
+ mSpecular = nif->getVector3();
+ mSpecStrength = nif->getFloat();
+ nif->skip(8); // Lighting effects
+ switch (static_cast<BSLightingShaderType>(type))
+ {
+ case BSLightingShaderType::ShaderType_EnvMap:
+ nif->skip(4); // Environment map scale
+ break;
+ case BSLightingShaderType::ShaderType_SkinTint:
+ case BSLightingShaderType::ShaderType_HairTint:
+ nif->skip(12); // Tint color
+ break;
+ case BSLightingShaderType::ShaderType_ParallaxOcc:
+ nif->skip(4); // Max passes
+ nif->skip(4); // Scale
+ break;
+ case BSLightingShaderType::ShaderType_MultiLayerParallax:
+ nif->skip(4); // Inner layer thickness
+ nif->skip(4); // Refraction scale
+ nif->skip(8); // Inner layer texture scale
+ nif->skip(4); // Environment map strength
+ break;
+ case BSLightingShaderType::ShaderType_SparkleSnow:
+ nif->skip(16); // Sparkle parameters
+ break;
+ case BSLightingShaderType::ShaderType_EyeEnvmap:
+ nif->skip(4); // Cube map scale
+ nif->skip(12); // Left eye cube map offset
+ nif->skip(12); // Right eye cube map offset
+ break;
+ default:
+ break;
+ }
+}
+
+void BSLightingShaderProperty::post(NIFFile *nif)
+{
+ BSShaderProperty::post(nif);
+ mTextureSet.post(nif);
+}
+
void NiFogProperty::read(NIFStream *nif)
{
Property::read(nif);
diff --git a/components/nif/property.hpp b/components/nif/property.hpp
index 9c76f6d6ec..a428d6a7e1 100644
--- a/components/nif/property.hpp
+++ b/components/nif/property.hpp
@@ -116,20 +116,21 @@ struct NiShadeProperty : public Property
}
};
-struct BSShaderProperty : public NiShadeProperty
+
+enum class BSShaderType : unsigned int
{
- enum BSShaderType
- {
- SHADER_TALL_GRASS = 0,
- SHADER_DEFAULT = 1,
- SHADER_SKY = 10,
- SHADER_SKIN = 14,
- SHADER_WATER = 17,
- SHADER_LIGHTING30 = 29,
- SHADER_TILE = 32,
- SHADER_NOLIGHTING = 33
- };
+ ShaderType_TallGrass = 0,
+ ShaderType_Default = 1,
+ ShaderType_Sky = 10,
+ ShaderType_Skin = 14,
+ ShaderType_Water = 17,
+ ShaderType_Lighting30 = 29,
+ ShaderType_Tile = 32,
+ ShaderType_NoLighting = 33
+};
+struct BSShaderProperty : public NiShadeProperty
+{
unsigned int type{0u}, flags1{0u}, flags2{0u};
float envMapIntensity{0.f};
void read(NIFStream *nif) override;
@@ -169,6 +170,44 @@ struct BSShaderNoLightingProperty : public BSShaderLightingProperty
void read(NIFStream *nif) override;
};
+enum class BSLightingShaderType : unsigned int
+{
+ ShaderType_Default = 0,
+ ShaderType_EnvMap = 1,
+ ShaderType_Glow = 2,
+ ShaderType_Parallax = 3,
+ ShaderType_FaceTint = 4,
+ ShaderType_SkinTint = 5,
+ ShaderType_HairTint = 6,
+ ShaderType_ParallaxOcc = 7,
+ ShaderType_MultitexLand = 8,
+ ShaderType_LODLand = 9,
+ ShaderType_Snow = 10,
+ ShaderType_MultiLayerParallax = 11,
+ ShaderType_TreeAnim = 12,
+ ShaderType_LODObjects = 13,
+ ShaderType_SparkleSnow = 14,
+ ShaderType_LODObjectsHD = 15,
+ ShaderType_EyeEnvmap = 16,
+ ShaderType_Cloud = 17,
+ ShaderType_LODNoise = 18,
+ ShaderType_MultitexLandLODBlend = 19,
+ ShaderType_Dismemberment = 20
+};
+
+struct BSLightingShaderProperty : public BSShaderProperty
+{
+ BSShaderTextureSetPtr mTextureSet;
+ unsigned int mClamp{0u};
+ float mAlpha;
+ float mGlossiness;
+ osg::Vec3f mEmissive, mSpecular;
+ float mEmissiveMult, mSpecStrength;
+
+ void read(NIFStream *nif) override;
+ void post(NIFFile *nif) override;
+};
+
struct NiDitherProperty : public Property
{
unsigned short flags;
diff --git a/components/nif/record.hpp b/components/nif/record.hpp
index dc81eb69c7..937ce1d1e9 100644
--- a/components/nif/record.hpp
+++ b/components/nif/record.hpp
@@ -125,7 +125,24 @@ enum RecordType
RC_BSLODTriShape,
RC_BSShaderProperty,
RC_BSShaderPPLightingProperty,
- RC_BSShaderNoLightingProperty
+ RC_BSShaderNoLightingProperty,
+ RC_BSFurnitureMarker,
+ RC_NiCollisionObject,
+ RC_bhkCollisionObject,
+ RC_BSDismemberSkinInstance,
+ RC_NiControllerManager,
+ RC_bhkMoppBvTreeShape,
+ RC_bhkNiTriStripsShape,
+ RC_bhkPackedNiTriStripsShape,
+ RC_hkPackedNiTriStripsData,
+ RC_bhkConvexVerticesShape,
+ RC_bhkBoxShape,
+ RC_bhkListShape,
+ RC_bhkRigidBody,
+ RC_bhkRigidBodyT,
+ RC_BSLightingShaderProperty,
+ RC_NiClusterAccumulator,
+ RC_NiAlphaAccumulator
};
/// Base class for all records
diff --git a/components/nif/recordptr.hpp b/components/nif/recordptr.hpp
index b30d99fbe4..5ec00b0c92 100644
--- a/components/nif/recordptr.hpp
+++ b/components/nif/recordptr.hpp
@@ -148,6 +148,12 @@ struct BSShaderTextureSet;
struct NiGeometryData;
struct BSShaderProperty;
struct NiAlphaProperty;
+struct NiCollisionObject;
+struct bhkWorldObject;
+struct bhkShape;
+struct bhkSerializable;
+struct hkPackedNiTriStripsData;
+struct NiAccumulator;
using NodePtr = RecordPtrT<Node>;
using ExtraPtr = RecordPtrT<Extra>;
@@ -175,6 +181,11 @@ using BSShaderTextureSetPtr = RecordPtrT<BSShaderTextureSet>;
using NiGeometryDataPtr = RecordPtrT<NiGeometryData>;
using BSShaderPropertyPtr = RecordPtrT<BSShaderProperty>;
using NiAlphaPropertyPtr = RecordPtrT<NiAlphaProperty>;
+using NiCollisionObjectPtr = RecordPtrT<NiCollisionObject>;
+using bhkWorldObjectPtr = RecordPtrT<bhkWorldObject>;
+using bhkShapePtr = RecordPtrT<bhkShape>;
+using hkPackedNiTriStripsDataPtr = RecordPtrT<hkPackedNiTriStripsData>;
+using NiAccumulatorPtr = RecordPtrT<NiAccumulator>;
using NodeList = RecordListT<Node>;
using PropertyList = RecordListT<Property>;
@@ -182,6 +193,8 @@ using ExtraList = RecordListT<Extra>;
using NiSourceTextureList = RecordListT<NiSourceTexture>;
using NiFloatInterpolatorList = RecordListT<NiFloatInterpolator>;
using NiTriStripsDataList = RecordListT<NiTriStripsData>;
+using bhkShapeList = RecordListT<bhkShape>;
+using bhkSerializableList = RecordListT<bhkSerializable>;
} // Namespace
#endif
diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp
index 6678d8ff74..4be07525a6 100644
--- a/components/nifbullet/bulletnifloader.cpp
+++ b/components/nifbullet/bulletnifloader.cpp
@@ -166,6 +166,8 @@ osg::ref_ptr<Resource::BulletShape> BulletNifLoader::load(const Nif::File& nif)
mStaticMesh.reset();
mAvoidStaticMesh.reset();
+ mShape->mFileHash = nif.getHash();
+
const size_t numRoots = nif.numRoots();
std::vector<const Nif::Node*> roots;
for (size_t i = 0; i < numRoots; ++i)
@@ -178,6 +180,7 @@ osg::ref_ptr<Resource::BulletShape> BulletNifLoader::load(const Nif::File& nif)
roots.emplace_back(node);
}
const std::string filename = nif.getFilename();
+ mShape->mFileName = filename;
if (roots.empty())
{
warn("Found no root nodes in NIF file " + filename);
diff --git a/components/nifosg/controller.cpp b/components/nifosg/controller.cpp
index ddded82156..07134532e9 100644
--- a/components/nifosg/controller.cpp
+++ b/components/nifosg/controller.cpp
@@ -57,7 +57,7 @@ float ControllerFunction::calculate(float value) const
}
case Constant:
default:
- return std::min(mStopTime, std::max(mStartTime, time));
+ return std::clamp(time, mStartTime, mStopTime);
}
}
@@ -82,34 +82,37 @@ KeyframeController::KeyframeController(const KeyframeController &copy, const osg
{
}
-KeyframeController::KeyframeController(const Nif::NiKeyframeData *data)
- : mRotations(data->mRotations)
- , mXRotations(data->mXRotations, 0.f)
- , mYRotations(data->mYRotations, 0.f)
- , mZRotations(data->mZRotations, 0.f)
- , mTranslations(data->mTranslations, osg::Vec3f())
- , mScales(data->mScales, 1.f)
-{
-}
-
-KeyframeController::KeyframeController(const Nif::NiTransformInterpolator* interpolator)
- : mRotations(interpolator->data->mRotations, interpolator->defaultRot)
- , mXRotations(interpolator->data->mXRotations, 0.f)
- , mYRotations(interpolator->data->mYRotations, 0.f)
- , mZRotations(interpolator->data->mZRotations, 0.f)
- , mTranslations(interpolator->data->mTranslations, interpolator->defaultPos)
- , mScales(interpolator->data->mScales, interpolator->defaultScale)
-{
-}
-
-KeyframeController::KeyframeController(const float scale, const osg::Vec3f& pos, const osg::Quat& rot)
- : mRotations(Nif::QuaternionKeyMapPtr(), rot)
- , mXRotations(Nif::FloatKeyMapPtr(), 0.f)
- , mYRotations(Nif::FloatKeyMapPtr(), 0.f)
- , mZRotations(Nif::FloatKeyMapPtr(), 0.f)
- , mTranslations(Nif::Vector3KeyMapPtr(), pos)
- , mScales(Nif::FloatKeyMapPtr(), scale)
+KeyframeController::KeyframeController(const Nif::NiKeyframeController *keyctrl)
{
+ if (!keyctrl->interpolator.empty())
+ {
+ const Nif::NiTransformInterpolator* interp = keyctrl->interpolator.getPtr();
+ if (!interp->data.empty())
+ {
+ mRotations = QuaternionInterpolator(interp->data->mRotations, interp->defaultRot);
+ mXRotations = FloatInterpolator(interp->data->mXRotations);
+ mYRotations = FloatInterpolator(interp->data->mYRotations);
+ mZRotations = FloatInterpolator(interp->data->mZRotations);
+ mTranslations = Vec3Interpolator(interp->data->mTranslations, interp->defaultPos);
+ mScales = FloatInterpolator(interp->data->mScales, interp->defaultScale);
+ }
+ else
+ {
+ mRotations = QuaternionInterpolator(Nif::QuaternionKeyMapPtr(), interp->defaultRot);
+ mTranslations = Vec3Interpolator(Nif::Vector3KeyMapPtr(), interp->defaultPos);
+ mScales = FloatInterpolator(Nif::FloatKeyMapPtr(), interp->defaultScale);
+ }
+ }
+ else if (!keyctrl->data.empty())
+ {
+ const Nif::NiKeyframeData* keydata = keyctrl->data.getPtr();
+ mRotations = QuaternionInterpolator(keydata->mRotations);
+ mXRotations = FloatInterpolator(keydata->mXRotations);
+ mYRotations = FloatInterpolator(keydata->mYRotations);
+ mZRotations = FloatInterpolator(keydata->mZRotations);
+ mTranslations = Vec3Interpolator(keydata->mTranslations);
+ mScales = FloatInterpolator(keydata->mScales, 1.f);
+ }
}
osg::Quat KeyframeController::getXYZRotation(float time) const
@@ -224,7 +227,7 @@ void GeomMorpherController::operator()(SceneUtil::MorphGeometry* node, osg::Node
if (mKeyFrames.size() <= 1)
return;
float input = getInputValue(nv);
- int i = 0;
+ int i = 1;
for (std::vector<FloatInterpolator>::iterator it = mKeyFrames.begin()+1; it != mKeyFrames.end(); ++it,++i)
{
float val = 0;
diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp
index 5d88dda1f1..a5f887ebe6 100644
--- a/components/nifosg/controller.hpp
+++ b/components/nifosg/controller.hpp
@@ -234,16 +234,9 @@ namespace NifOsg
class KeyframeController : public SceneUtil::KeyframeController, public SceneUtil::NodeCallback<KeyframeController, NifOsg::MatrixTransform*>
{
public:
- // This is used if there's no interpolator but there is data (Morrowind meshes).
- KeyframeController(const Nif::NiKeyframeData *data);
- // This is used if the interpolator has data.
- KeyframeController(const Nif::NiTransformInterpolator* interpolator);
- // This is used if there are default values available (e.g. from a data-less interpolator).
- // If there's neither keyframe data nor an interpolator a KeyframeController must not be created.
- KeyframeController(const float scale, const osg::Vec3f& pos, const osg::Quat& rot);
-
KeyframeController();
KeyframeController(const KeyframeController& copy, const osg::CopyOp& copyop);
+ KeyframeController(const Nif::NiKeyframeController *keyctrl);
META_Object(NifOsg, KeyframeController)
diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp
index 838895eb47..43af68fc90 100644
--- a/components/nifosg/nifloader.cpp
+++ b/components/nifosg/nifloader.cpp
@@ -16,7 +16,7 @@
#include <components/misc/stringops.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/imagemanager.hpp>
-#include <components/sceneutil/util.hpp>
+#include <components/misc/osguservalues.hpp>
// particle
#include <osgParticle/ParticleSystem>
@@ -270,8 +270,8 @@ namespace NifOsg
if (key->data.empty() && key->interpolator.empty())
continue;
- osg::ref_ptr<SceneUtil::KeyframeController> callback(handleKeyframeController(key));
- callback->setFunction(std::shared_ptr<NifOsg::ControllerFunction>(new NifOsg::ControllerFunction(key)));
+ osg::ref_ptr<SceneUtil::KeyframeController> callback = new NifOsg::KeyframeController(key);
+ setupController(key, callback, /*animflags*/0);
if (!target.mKeyframeControllers.emplace(strdata->string, callback).second)
Log(Debug::Verbose) << "Controller " << strdata->string << " present more than once in " << nif->getFilename() << ", ignoring later version";
@@ -325,6 +325,8 @@ namespace NifOsg
if (!textkeys->mTextKeys.empty())
created->getOrCreateUserDataContainer()->addUserObject(textkeys);
+ created->setUserValue(Misc::OsgUserValues::sFileHash, nif->getHash());
+
return created;
}
@@ -357,7 +359,7 @@ namespace NifOsg
handleProperty(geometry->shaderprop.getPtr(), applyTo, composite, imageManager, boundTextures, animflags);
}
- void setupController(const Nif::Controller* ctrl, SceneUtil::Controller* toSetup, int animflags)
+ static void setupController(const Nif::Controller* ctrl, SceneUtil::Controller* toSetup, int animflags)
{
bool autoPlay = animflags & Nif::NiNode::AnimFlag_AutoPlay;
if (autoPlay)
@@ -366,7 +368,7 @@ namespace NifOsg
toSetup->setFunction(std::shared_ptr<ControllerFunction>(new ControllerFunction(ctrl)));
}
- osg::ref_ptr<osg::LOD> handleLodNode(const Nif::NiLODNode* niLodNode)
+ static osg::ref_ptr<osg::LOD> handleLodNode(const Nif::NiLODNode* niLodNode)
{
osg::ref_ptr<osg::LOD> lod (new osg::LOD);
lod->setName(niLodNode->name);
@@ -381,7 +383,7 @@ namespace NifOsg
return lod;
}
- osg::ref_ptr<osg::Switch> handleSwitchNode(const Nif::NiSwitchNode* niSwitchNode)
+ static osg::ref_ptr<osg::Switch> handleSwitchNode(const Nif::NiSwitchNode* niSwitchNode)
{
osg::ref_ptr<osg::Switch> switchNode (new osg::Switch);
switchNode->setName(niSwitchNode->name);
@@ -716,24 +718,6 @@ namespace NifOsg
}
}
- static osg::ref_ptr<KeyframeController> handleKeyframeController(const Nif::NiKeyframeController* keyctrl)
- {
- osg::ref_ptr<NifOsg::KeyframeController> ctrl;
- if (!keyctrl->interpolator.empty())
- {
- const Nif::NiTransformInterpolator* interp = keyctrl->interpolator.getPtr();
- if (!interp->data.empty())
- ctrl = new NifOsg::KeyframeController(interp);
- else
- ctrl = new NifOsg::KeyframeController(interp->defaultScale, interp->defaultPos, interp->defaultRot);
- }
- else if (!keyctrl->data.empty())
- {
- ctrl = new NifOsg::KeyframeController(keyctrl->data.getPtr());
- }
- return ctrl;
- }
-
void handleNodeControllers(const Nif::Node* nifNode, osg::Node* node, int animflags, bool& isAnimated)
{
for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next)
@@ -745,7 +729,7 @@ namespace NifOsg
const Nif::NiKeyframeController *key = static_cast<const Nif::NiKeyframeController*>(ctrl.getPtr());
if (key->data.empty() && key->interpolator.empty())
continue;
- osg::ref_ptr<KeyframeController> callback(handleKeyframeController(key));
+ osg::ref_ptr<KeyframeController> callback = new KeyframeController(key);
setupController(key, callback, animflags);
node->addUpdateCallback(callback);
isAnimated = true;
@@ -1258,8 +1242,9 @@ namespace NifOsg
const std::vector<Nif::NiMorphData::MorphData>& morphs = morpher->data.getPtr()->mMorphs;
if (morphs.empty())
return morphGeom;
- // Note we are not interested in morph 0, which just contains the original vertices
- for (unsigned int i = 1; i < morphs.size(); ++i)
+ if (morphs[0].mVertices.size() != static_cast<const osg::Vec3Array*>(sourceGeometry->getVertexArray())->size())
+ return morphGeom;
+ for (unsigned int i = 0; i < morphs.size(); ++i)
morphGeom->addMorphTarget(new osg::Vec3Array(morphs[i].mVertices.size(), morphs[i].mVertices.data()), 0.f);
return morphGeom;
@@ -1611,6 +1596,9 @@ namespace NifOsg
}
else if (i == Nif::NiTexturingProperty::DecalTexture)
{
+ // This is only an inaccurate imitation of the original implementation,
+ // see https://github.com/niftools/nifskope/issues/184
+
osg::TexEnvCombine* texEnv = new osg::TexEnvCombine;
// Interpolate to the decal texture's colour...
texEnv->setCombine_RGB(osg::TexEnvCombine::INTERPOLATE);
@@ -1714,28 +1702,64 @@ namespace NifOsg
}
}
- const std::string& getNVShaderPrefix(unsigned int type) const
+ const std::string& getBSShaderPrefix(unsigned int type) const
+ {
+ static const std::unordered_map<Nif::BSShaderType, std::string> mapping =
+ {
+ {Nif::BSShaderType::ShaderType_TallGrass, std::string()},
+ {Nif::BSShaderType::ShaderType_Default, "nv_default"},
+ {Nif::BSShaderType::ShaderType_Sky, std::string()},
+ {Nif::BSShaderType::ShaderType_Skin, std::string()},
+ {Nif::BSShaderType::ShaderType_Water, std::string()},
+ {Nif::BSShaderType::ShaderType_Lighting30, std::string()},
+ {Nif::BSShaderType::ShaderType_Tile, std::string()},
+ {Nif::BSShaderType::ShaderType_NoLighting, "nv_nolighting"},
+ };
+ auto prefix = mapping.find(static_cast<Nif::BSShaderType>(type));
+ if (prefix == mapping.end())
+ Log(Debug::Warning) << "Unknown BSShaderType " << type << " in " << mFilename;
+ else if (prefix->second.empty())
+ Log(Debug::Warning) << "Unhandled BSShaderType " << type << " in " << mFilename;
+ else
+ return prefix->second;
+
+ return mapping.at(Nif::BSShaderType::ShaderType_Default);
+ }
+
+ const std::string& getBSLightingShaderPrefix(unsigned int type) const
{
- static const std::map<unsigned int, std::string> mapping =
- {
- {Nif::BSShaderProperty::SHADER_TALL_GRASS, std::string()},
- {Nif::BSShaderProperty::SHADER_DEFAULT, "nv_default"},
- {Nif::BSShaderProperty::SHADER_SKY, std::string()},
- {Nif::BSShaderProperty::SHADER_SKIN, std::string()},
- {Nif::BSShaderProperty::SHADER_WATER, std::string()},
- {Nif::BSShaderProperty::SHADER_LIGHTING30, std::string()},
- {Nif::BSShaderProperty::SHADER_TILE, std::string()},
- {Nif::BSShaderProperty::SHADER_NOLIGHTING, "nv_nolighting"},
+ static const std::unordered_map<Nif::BSLightingShaderType, std::string> mapping =
+ {
+ {Nif::BSLightingShaderType::ShaderType_Default, "nv_default"},
+ {Nif::BSLightingShaderType::ShaderType_EnvMap, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_Glow, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_Parallax, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_FaceTint, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_HairTint, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_ParallaxOcc, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_MultitexLand, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_LODLand, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_Snow, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_MultiLayerParallax, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_TreeAnim, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_LODObjects, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_SparkleSnow, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_LODObjectsHD, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_EyeEnvmap, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_Cloud, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_LODNoise, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_MultitexLandLODBlend, std::string()},
+ {Nif::BSLightingShaderType::ShaderType_Dismemberment, std::string()}
};
- auto prefix = mapping.find(type);
+ auto prefix = mapping.find(static_cast<Nif::BSLightingShaderType>(type));
if (prefix == mapping.end())
- Log(Debug::Warning) << "Unknown shader type " << type << " in " << mFilename;
+ Log(Debug::Warning) << "Unknown BSLightingShaderType " << type << " in " << mFilename;
else if (prefix->second.empty())
- Log(Debug::Warning) << "Unhandled shader type " << type << " in " << mFilename;
+ Log(Debug::Warning) << "Unhandled BSLightingShaderType " << type << " in " << mFilename;
else
return prefix->second;
- return mapping.at(Nif::BSShaderProperty::SHADER_DEFAULT);
+ return mapping.at(Nif::BSLightingShaderType::ShaderType_Default);
}
void handleProperty(const Nif::Property *property,
@@ -1795,7 +1819,7 @@ namespace NifOsg
// Depth test flag
stateset->setMode(GL_DEPTH_TEST, zprop->flags&1 ? osg::StateAttribute::ON
: osg::StateAttribute::OFF);
- auto depth = SceneUtil::createDepth();
+ osg::ref_ptr<osg::Depth> depth = new osg::Depth;
// Depth write flag
depth->setWriteMask((zprop->flags>>1)&1);
// Morrowind ignores depth test function
@@ -1827,7 +1851,7 @@ namespace NifOsg
{
auto texprop = static_cast<const Nif::BSShaderPPLightingProperty*>(property);
bool shaderRequired = true;
- node->setUserValue("shaderPrefix", getNVShaderPrefix(texprop->type));
+ node->setUserValue("shaderPrefix", getBSShaderPrefix(texprop->type));
node->setUserValue("shaderRequired", shaderRequired);
osg::StateSet* stateset = node->getOrCreateStateSet();
if (!texprop->textureSet.empty())
@@ -1842,7 +1866,7 @@ namespace NifOsg
{
auto texprop = static_cast<const Nif::BSShaderNoLightingProperty*>(property);
bool shaderRequired = true;
- node->setUserValue("shaderPrefix", getNVShaderPrefix(texprop->type));
+ node->setUserValue("shaderPrefix", getBSShaderPrefix(texprop->type));
node->setUserValue("shaderRequired", shaderRequired);
osg::StateSet* stateset = node->getOrCreateStateSet();
if (!texprop->filename.empty())
@@ -1880,6 +1904,18 @@ namespace NifOsg
handleTextureControllers(texprop, composite, imageManager, stateset, animflags);
break;
}
+ case Nif::RC_BSLightingShaderProperty:
+ {
+ auto texprop = static_cast<const Nif::BSLightingShaderProperty*>(property);
+ bool shaderRequired = true;
+ node->setUserValue("shaderPrefix", getBSLightingShaderPrefix(texprop->type));
+ node->setUserValue("shaderRequired", shaderRequired);
+ osg::StateSet* stateset = node->getOrCreateStateSet();
+ if (!texprop->mTextureSet.empty())
+ handleTextureSet(texprop->mTextureSet.getPtr(), texprop->mClamp, node->getName(), stateset, imageManager, boundTextures);
+ handleTextureControllers(texprop, composite, imageManager, stateset, animflags);
+ break;
+ }
// unused by mw
case Nif::RC_NiShadeProperty:
case Nif::RC_NiDitherProperty:
@@ -1931,6 +1967,7 @@ namespace NifOsg
int lightmode = 1;
float emissiveMult = 1.f;
+ float specStrength = 1.f;
for (const Nif::Property* property : properties)
{
@@ -2027,6 +2064,17 @@ namespace NifOsg
}
break;
}
+ case Nif::RC_BSLightingShaderProperty:
+ {
+ auto shaderprop = static_cast<const Nif::BSLightingShaderProperty*>(property);
+ mat->setAlpha(osg::Material::FRONT_AND_BACK, shaderprop->mAlpha);
+ mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(shaderprop->mEmissive, 1.f));
+ mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(shaderprop->mSpecular, 1.f));
+ mat->setShininess(osg::Material::FRONT_AND_BACK, shaderprop->mGlossiness);
+ emissiveMult = shaderprop->mEmissiveMult;
+ specStrength = shaderprop->mSpecStrength;
+ break;
+ }
}
}
@@ -2080,6 +2128,8 @@ namespace NifOsg
stateset->setAttributeAndModes(mat, osg::StateAttribute::ON);
if (emissiveMult != 1.f)
stateset->addUniform(new osg::Uniform("emissiveMult", emissiveMult));
+ if (specStrength != 1.f)
+ stateset->addUniform(new osg::Uniform("specStrength", specStrength));
}
};
diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp
index 78cf70038b..7c45c865a1 100644
--- a/components/process/processinvoker.cpp
+++ b/components/process/processinvoker.cpp
@@ -7,7 +7,8 @@
#include <QDebug>
#include <QCoreApplication>
-Process::ProcessInvoker::ProcessInvoker()
+Process::ProcessInvoker::ProcessInvoker(QObject* parent)
+ : QObject(parent)
{
mProcess = new QProcess(this);
@@ -56,6 +57,7 @@ bool Process::ProcessInvoker::startProcess(const QString &name, const QStringLis
// mProcess = new QProcess(this);
mName = name;
mArguments = arguments;
+ mIgnoreErrors = false;
QString path(name);
#ifdef Q_OS_WIN
@@ -151,6 +153,8 @@ bool Process::ProcessInvoker::startProcess(const QString &name, const QStringLis
void Process::ProcessInvoker::processError(QProcess::ProcessError error)
{
+ if (mIgnoreErrors)
+ return;
QMessageBox msgBox;
msgBox.setWindowTitle(tr("Error running executable"));
msgBox.setIcon(QMessageBox::Critical);
@@ -166,6 +170,8 @@ void Process::ProcessInvoker::processError(QProcess::ProcessError error)
void Process::ProcessInvoker::processFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
if (exitCode != 0 || exitStatus == QProcess::CrashExit) {
+ if (mIgnoreErrors)
+ return;
QString error(mProcess->readAllStandardError());
error.append(tr("\nArguments:\n"));
error.append(mArguments.join(" "));
@@ -181,3 +187,9 @@ void Process::ProcessInvoker::processFinished(int exitCode, QProcess::ExitStatus
msgBox.exec();
}
}
+
+void Process::ProcessInvoker::killProcess()
+{
+ mIgnoreErrors = true;
+ mProcess->kill();
+}
diff --git a/components/process/processinvoker.hpp b/components/process/processinvoker.hpp
index 8fff6658ca..f4b402cb12 100644
--- a/components/process/processinvoker.hpp
+++ b/components/process/processinvoker.hpp
@@ -13,7 +13,7 @@ namespace Process
public:
- ProcessInvoker();
+ ProcessInvoker(QObject* parent = nullptr);
~ProcessInvoker();
// void setProcessName(const QString &name);
@@ -27,12 +27,16 @@ namespace Process
inline bool startProcess(const QString &name, bool detached = false) { return startProcess(name, QStringList(), detached); }
bool startProcess(const QString &name, const QStringList &arguments, bool detached = false);
+ void killProcess();
+
private:
QProcess *mProcess;
QString mName;
QStringList mArguments;
+ bool mIgnoreErrors = false;
+
private slots:
void processError(QProcess::ProcessError error);
void processFinished(int exitCode, QProcess::ExitStatus exitStatus);
diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp
index 52d639d272..74515691c6 100644
--- a/components/resource/bulletshape.cpp
+++ b/components/resource/bulletshape.cpp
@@ -74,6 +74,8 @@ BulletShape::BulletShape(const BulletShape &copy, const osg::CopyOp &copyop)
, mAvoidCollisionShape(duplicateCollisionShape(copy.mAvoidCollisionShape.get()))
, mCollisionBox(copy.mCollisionBox)
, mAnimatedShapes(copy.mAnimatedShapes)
+ , mFileName(copy.mFileName)
+ , mFileHash(copy.mFileHash)
{
}
diff --git a/components/resource/bulletshape.hpp b/components/resource/bulletshape.hpp
index 7188165045..cd8922ec8e 100644
--- a/components/resource/bulletshape.hpp
+++ b/components/resource/bulletshape.hpp
@@ -1,6 +1,7 @@
#ifndef OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H
#define OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H
+#include <array>
#include <map>
#include <memory>
@@ -12,6 +13,11 @@
class btCollisionShape;
+namespace NifBullet
+{
+ class BulletNifLoader;
+}
+
namespace Resource
{
struct DeleteCollisionShape
@@ -47,6 +53,9 @@ namespace Resource
// we store the node's record index mapped to the child index of the shape in the btCompoundShape.
std::map<int, int> mAnimatedShapes;
+ std::string mFileName;
+ std::string mFileHash;
+
void setLocalScaling(const btVector3& scale);
bool isAnimated() const { return !mAnimatedShapes.empty(); }
@@ -60,6 +69,8 @@ namespace Resource
public:
BulletShapeInstance(osg::ref_ptr<const BulletShape> source);
+ const osg::ref_ptr<const BulletShape>& getSource() const { return mSource; }
+
private:
osg::ref_ptr<const BulletShape> mSource;
};
diff --git a/components/resource/bulletshapemanager.cpp b/components/resource/bulletshapemanager.cpp
index 39ceb4fe7e..da4672757a 100644
--- a/components/resource/bulletshapemanager.cpp
+++ b/components/resource/bulletshapemanager.cpp
@@ -1,5 +1,7 @@
#include "bulletshapemanager.hpp"
+#include <cstring>
+
#include <osg/NodeVisitor>
#include <osg/TriangleFunctor>
#include <osg/Transform>
@@ -10,6 +12,7 @@
#include <components/misc/pathhelpers.hpp>
#include <components/sceneutil/visitor.hpp>
#include <components/vfs/manager.hpp>
+#include <components/misc/osguservalues.hpp>
#include <components/nifbullet/bulletnifloader.hpp>
@@ -162,6 +165,12 @@ osg::ref_ptr<const BulletShape> BulletShapeManager::getShape(const std::string &
if (!shape)
return osg::ref_ptr<BulletShape>();
}
+
+ if (shape != nullptr)
+ {
+ shape->mFileName = normalized;
+ constNode->getUserValue(Misc::OsgUserValues::sFileHash, shape->mFileHash);
+ }
}
mCache->addEntryToObjectCache(normalized, shape);
diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp
index c1d567c0e1..41f65ba0ab 100644
--- a/components/resource/scenemanager.cpp
+++ b/components/resource/scenemanager.cpp
@@ -1,6 +1,7 @@
#include "scenemanager.hpp"
#include <cstdlib>
+#include <filesystem>
#include <osg/AlphaFunc>
#include <osg/Node>
@@ -10,6 +11,7 @@
#include <osgUtil/IncrementalCompileOperation>
+#include <osgDB/FileUtils>
#include <osgDB/SharedStateManager>
#include <osgDB/Registry>
@@ -21,6 +23,8 @@
#include <components/misc/pathhelpers.hpp>
#include <components/misc/stringops.hpp>
#include <components/misc/algorithm.hpp>
+#include <components/misc/errorMarker.hpp>
+#include <components/misc/osguservalues.hpp>
#include <components/vfs/manager.hpp>
@@ -30,10 +34,14 @@
#include <components/sceneutil/optimizer.hpp>
#include <components/sceneutil/visitor.hpp>
#include <components/sceneutil/lightmanager.hpp>
+#include <components/sceneutil/depth.hpp>
#include <components/shader/shadervisitor.hpp>
#include <components/shader/shadermanager.hpp>
+#include <components/files/hash.hpp>
+#include <components/files/memorystream.hpp>
+
#include "imagemanager.hpp"
#include "niffilemanager.hpp"
#include "objectcache.hpp"
@@ -247,14 +255,14 @@ namespace Resource
{
if (stateset->getRenderingHint() == osg::StateSet::TRANSPARENT_BIN)
{
- osg::ref_ptr<osg::Depth> depth = SceneUtil::createDepth();
+ osg::ref_ptr<osg::Depth> depth = new osg::Depth;
depth->setWriteMask(false);
stateset->setAttributeAndModes(depth, osg::StateAttribute::ON);
}
else if (stateset->getRenderingHint() == osg::StateSet::OPAQUE_BIN)
{
- osg::ref_ptr<osg::Depth> depth = SceneUtil::createDepth();
+ osg::ref_ptr<osg::Depth> depth = new osg::Depth;
depth->setWriteMask(true);
stateset->setAttributeAndModes(depth, osg::StateAttribute::ON);
@@ -265,7 +273,7 @@ namespace Resource
correct format for OpenMW: <Description>alphatest mode value MaterialName</Description>
e.g <Description>alphatest GEQUAL 0.8 MyAlphaTestedMaterial</Description> */
std::vector<std::string> descriptions = node.getDescriptions();
- for (auto description : descriptions)
+ for (const auto & description : descriptions)
{
mDescriptions.emplace_back(description);
}
@@ -273,7 +281,7 @@ namespace Resource
// Iterate each description, and see if the current node uses the specified material for alpha testing
if (node.getStateSet())
{
- for (auto description : mDescriptions)
+ for (const auto & description : mDescriptions)
{
std::vector<std::string> descriptionParts;
std::istringstream descriptionStringStream(description);
@@ -430,6 +438,11 @@ namespace Resource
mConvertAlphaTestToAlphaToCoverage = convert;
}
+ void SceneManager::setOpaqueDepthTex(osg::ref_ptr<osg::Texture2D> texture)
+ {
+ mOpaqueDepthTex = texture;
+ }
+
SceneManager::~SceneManager()
{
// this has to be defined in the .cpp file as we can't delete incomplete types
@@ -461,9 +474,15 @@ namespace Resource
osgDB::ReaderWriter::ReadResult readImage(const std::string& filename, const osgDB::Options* options) override
{
+ std::filesystem::path filePath(filename);
+ if (filePath.is_absolute())
+ // It is a hack. Needed because either OSG or libcollada-dom tries to make an absolute path from
+ // our relative VFS path by adding current working directory.
+ filePath = std::filesystem::relative(filename, osgDB::getCurrentWorkingDirectory());
try
{
- return osgDB::ReaderWriter::ReadResult(mImageManager->getImage(filename), osgDB::ReaderWriter::ReadResult::FILE_LOADED);
+ return osgDB::ReaderWriter::ReadResult(mImageManager->getImage(filePath.string()),
+ osgDB::ReaderWriter::ReadResult::FILE_LOADED);
}
catch (std::exception& e)
{
@@ -475,13 +494,11 @@ namespace Resource
Resource::ImageManager* mImageManager;
};
- osg::ref_ptr<osg::Node> load (const std::string& normalizedFilename, const VFS::Manager* vfs, Resource::ImageManager* imageManager, Resource::NifFileManager* nifFileManager)
+ namespace
{
- auto ext = Misc::getFileExtension(normalizedFilename);
- if (ext == "nif")
- return NifOsg::Loader::load(nifFileManager->get(normalizedFilename), imageManager);
- else
+ osg::ref_ptr<osg::Node> loadNonNif(const std::string& normalizedFilename, std::istream& model, Resource::ImageManager* imageManager)
{
+ auto ext = Misc::getFileExtension(normalizedFilename);
osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension(std::string(ext));
if (!reader)
{
@@ -497,7 +514,9 @@ namespace Resource
options->setReadFileCallback(new ImageReadCallback(imageManager));
if (ext == "dae") options->setOptionString("daeUseSequencedTextureUnits");
- osgDB::ReaderWriter::ReadResult result = reader->readNode(*vfs->get(normalizedFilename), options);
+ const std::array<std::uint64_t, 2> fileHash = Files::getHash(normalizedFilename, model);
+
+ osgDB::ReaderWriter::ReadResult result = reader->readNode(model, options);
if (!result.success())
{
std::stringstream errormsg;
@@ -508,7 +527,9 @@ namespace Resource
// Recognize and hide collision node
unsigned int hiddenNodeMask = 0;
SceneUtil::FindByNameVisitor nameFinder("Collision");
- result.getNode()->accept(nameFinder);
+
+ auto node = result.getNode();
+ node->accept(nameFinder);
if (nameFinder.mFoundNode)
nameFinder.mFoundNode->setNodeMask(hiddenNodeMask);
@@ -516,18 +537,30 @@ namespace Resource
{
// Collada alpha testing
Resource::ColladaAlphaTrickVisitor colladaAlphaTrickVisitor;
- result.getNode()->accept(colladaAlphaTrickVisitor);
+ node->accept(colladaAlphaTrickVisitor);
- result.getNode()->getOrCreateStateSet()->addUniform(new osg::Uniform("emissiveMult", 1.f));
- result.getNode()->getOrCreateStateSet()->addUniform(new osg::Uniform("envMapColor", osg::Vec4f(1,1,1,1)));
- result.getNode()->getOrCreateStateSet()->addUniform(new osg::Uniform("useFalloff", false));
+ node->getOrCreateStateSet()->addUniform(new osg::Uniform("emissiveMult", 1.f));
+ node->getOrCreateStateSet()->addUniform(new osg::Uniform("specStrength", 1.f));
+ node->getOrCreateStateSet()->addUniform(new osg::Uniform("envMapColor", osg::Vec4f(1,1,1,1)));
+ node->getOrCreateStateSet()->addUniform(new osg::Uniform("useFalloff", false));
}
+ node->setUserValue(Misc::OsgUserValues::sFileHash,
+ std::string(reinterpret_cast<const char*>(fileHash.data()), fileHash.size() * sizeof(std::uint64_t)));
- return result.getNode();
+ return node;
}
}
+ osg::ref_ptr<osg::Node> load (const std::string& normalizedFilename, const VFS::Manager* vfs, Resource::ImageManager* imageManager, Resource::NifFileManager* nifFileManager)
+ {
+ auto ext = Misc::getFileExtension(normalizedFilename);
+ if (ext == "nif")
+ return NifOsg::Loader::load(nifFileManager->get(normalizedFilename), imageManager);
+ else
+ return loadNonNif(normalizedFilename, *vfs->get(normalizedFilename), imageManager);
+ }
+
class CanOptimizeCallback : public SceneUtil::Optimizer::IsOperationPermissibleForObjectCallback
{
public:
@@ -643,23 +676,23 @@ namespace Resource
{
loaded = load(normalized, mVFS, mImageManager, mNifFileManager);
}
- catch (std::exception& e)
+ catch (const std::exception& e)
{
- static const char * const sMeshTypes[] = { "nif", "osg", "osgt", "osgb", "osgx", "osg2", "dae" };
+ static osg::ref_ptr<osg::Node> errorMarkerNode = [&] {
+ static const char* const sMeshTypes[] = { "nif", "osg", "osgt", "osgb", "osgx", "osg2", "dae" };
- for (unsigned int i=0; i<sizeof(sMeshTypes)/sizeof(sMeshTypes[0]); ++i)
- {
- normalized = "meshes/marker_error." + std::string(sMeshTypes[i]);
- if (mVFS->exists(normalized))
+ for (unsigned int i=0; i<sizeof(sMeshTypes)/sizeof(sMeshTypes[0]); ++i)
{
- Log(Debug::Error) << "Failed to load '" << name << "': " << e.what() << ", using marker_error." << sMeshTypes[i] << " instead";
- loaded = load(normalized, mVFS, mImageManager, mNifFileManager);
- break;
+ normalized = "meshes/marker_error." + std::string(sMeshTypes[i]);
+ if (mVFS->exists(normalized))
+ return load(normalized, mVFS, mImageManager, mNifFileManager);
}
- }
+ Files::IMemStream file(Misc::errorMarker.data(), Misc::errorMarker.size());
+ return loadNonNif("error_marker.osgt", file, mImageManager);
+ }();
- if (!loaded)
- throw;
+ Log(Debug::Error) << "Failed to load '" << name << "': " << e.what() << ", using marker_error instead";
+ loaded = static_cast<osg::Node*>(errorMarkerNode->clone(osg::CopyOp::DEEP_COPY_ALL));
}
// set filtering settings
@@ -668,6 +701,9 @@ namespace Resource
SetFilterSettingsControllerVisitor setFilterSettingsControllerVisitor(mMinFilter, mMagFilter, mMaxAnisotropy);
loaded->accept(setFilterSettingsControllerVisitor);
+ SceneUtil::ReplaceDepthVisitor replaceDepthVisitor;
+ loaded->accept(replaceDepthVisitor);
+
osg::ref_ptr<Shader::ShaderVisitor> shaderVisitor (createShaderVisitor());
loaded->accept(*shaderVisitor);
@@ -892,6 +928,7 @@ namespace Resource
shaderVisitor->setSpecularMapPattern(mSpecularMapPattern);
shaderVisitor->setApplyLightingToEnvMaps(mApplyLightingToEnvMaps);
shaderVisitor->setConvertAlphaTestToAlphaToCoverage(mConvertAlphaTestToAlphaToCoverage);
+ shaderVisitor->setOpaqueDepthTex(mOpaqueDepthTex);
return shaderVisitor;
}
}
diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp
index 85e012071d..58ae8fdb8b 100644
--- a/components/resource/scenemanager.hpp
+++ b/components/resource/scenemanager.hpp
@@ -9,6 +9,7 @@
#include <osg/ref_ptr>
#include <osg/Node>
#include <osg/Texture>
+#include <osg/Texture2D>
#include "resourcemanager.hpp"
@@ -111,6 +112,8 @@ namespace Resource
void setSupportedLightingMethods(const SceneUtil::LightManager::SupportedMethods& supported);
bool isSupportedLightingMethod(SceneUtil::LightingMethod method) const;
+ void setOpaqueDepthTex(osg::ref_ptr<osg::Texture2D> texture);
+
enum class UBOBinding
{
// If we add more UBO's, we should probably assign their bindings dynamically according to the current count of UBO's in the programTemplate
@@ -209,6 +212,7 @@ namespace Resource
SceneUtil::LightManager::SupportedMethods mSupportedLightingMethods;
bool mConvertAlphaTestToAlphaToCoverage;
GLenum mDepthFormat;
+ osg::ref_ptr<osg::Texture2D> mOpaqueDepthTex;
osg::ref_ptr<Resource::SharedStateManager> mSharedStateManager;
mutable std::mutex mSharedStateMutex;
diff --git a/components/resource/stats.cpp b/components/resource/stats.cpp
index b3705f69cc..d97ddd1d6f 100644
--- a/components/resource/stats.cpp
+++ b/components/resource/stats.cpp
@@ -393,6 +393,8 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase *viewer)
"NavMesh Waiting",
"NavMesh Pushed",
"NavMesh Processing",
+ "NavMesh DbJobs",
+ "NavMesh DbCacheHitRate",
"NavMesh CacheSize",
"NavMesh UsedTiles",
"NavMesh CachedTiles",
diff --git a/components/sceneutil/agentpath.cpp b/components/sceneutil/agentpath.cpp
index 5f9b574e7d..db026a3332 100644
--- a/components/sceneutil/agentpath.cpp
+++ b/components/sceneutil/agentpath.cpp
@@ -37,13 +37,13 @@ namespace SceneUtil
{
osg::ref_ptr<osg::Group> createAgentPathGroup(const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end,
- const DetourNavigator::Settings& settings)
+ const DetourNavigator::RecastSettings& settings)
{
using namespace DetourNavigator;
const osg::ref_ptr<osg::Group> group(new osg::Group);
- DebugDraw debugDraw(*group, osg::Vec3f(0, 0, 0), 1);
+ DebugDraw debugDraw(*group, DebugDraw::makeStateSet(), osg::Vec3f(0, 0, 0), 1);
const auto agentRadius = halfExtents.x();
const auto agentHeight = 2.0f * halfExtents.z();
diff --git a/components/sceneutil/agentpath.hpp b/components/sceneutil/agentpath.hpp
index a8965d852e..1194fa512a 100644
--- a/components/sceneutil/agentpath.hpp
+++ b/components/sceneutil/agentpath.hpp
@@ -13,14 +13,14 @@ namespace osg
namespace DetourNavigator
{
- struct Settings;
+ struct RecastSettings;
}
namespace SceneUtil
{
osg::ref_ptr<osg::Group> createAgentPathGroup(const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end,
- const DetourNavigator::Settings& settings);
+ const DetourNavigator::RecastSettings& settings);
}
#endif
diff --git a/components/sceneutil/controller.cpp b/components/sceneutil/controller.cpp
index dfc72918aa..2c7507d0a3 100644
--- a/components/sceneutil/controller.cpp
+++ b/components/sceneutil/controller.cpp
@@ -121,6 +121,21 @@ namespace SceneUtil
ctrl.setSource(mToAssign);
}
+ ForceControllerSourcesVisitor::ForceControllerSourcesVisitor()
+ : AssignControllerSourcesVisitor()
+ {
+ }
+
+ ForceControllerSourcesVisitor::ForceControllerSourcesVisitor(std::shared_ptr<ControllerSource> toAssign)
+ : AssignControllerSourcesVisitor(toAssign)
+ {
+ }
+
+ void ForceControllerSourcesVisitor::visit(osg::Node&, Controller &ctrl)
+ {
+ ctrl.setSource(mToAssign);
+ }
+
FindMaxControllerLengthVisitor::FindMaxControllerLengthVisitor()
: SceneUtil::ControllerVisitor()
, mMaxLength(0)
diff --git a/components/sceneutil/controller.hpp b/components/sceneutil/controller.hpp
index 2656d654e1..6ef800f05b 100644
--- a/components/sceneutil/controller.hpp
+++ b/components/sceneutil/controller.hpp
@@ -85,10 +85,20 @@ namespace SceneUtil
/// By default assigns the ControllerSource passed to the constructor of this class if no ControllerSource is assigned to that controller yet.
void visit(osg::Node& node, Controller& ctrl) override;
- private:
+ protected:
std::shared_ptr<ControllerSource> mToAssign;
};
+ class ForceControllerSourcesVisitor : public AssignControllerSourcesVisitor
+ {
+ public:
+ ForceControllerSourcesVisitor();
+ ForceControllerSourcesVisitor(std::shared_ptr<ControllerSource> toAssign);
+
+ /// Assign the wanted ControllerSource even if one is already assigned to the controller.
+ void visit(osg::Node& node, Controller& ctrl) override;
+ };
+
/// Finds the maximum of all controller functions in the given scene graph
class FindMaxControllerLengthVisitor : public ControllerVisitor
{
diff --git a/components/sceneutil/depth.cpp b/components/sceneutil/depth.cpp
new file mode 100644
index 0000000000..5d53b97726
--- /dev/null
+++ b/components/sceneutil/depth.cpp
@@ -0,0 +1,63 @@
+#include "depth.hpp"
+
+#include <algorithm>
+
+#include <SDL_opengl_glext.h>
+
+#include <components/settings/settings.hpp>
+
+#ifndef GL_DEPTH32F_STENCIL8_NV
+#define GL_DEPTH32F_STENCIL8_NV 0x8DAC
+#endif
+
+namespace SceneUtil
+{
+ void setCameraClearDepth(osg::Camera* camera)
+ {
+ camera->setClearDepth(AutoDepth::isReversed() ? 0.0 : 1.0);
+ }
+
+ osg::Matrix getReversedZProjectionMatrixAsPerspectiveInf(double fov, double aspect, double near)
+ {
+ double A = 1.0/std::tan(osg::DegreesToRadians(fov)/2.0);
+ return osg::Matrix(
+ A/aspect, 0, 0, 0,
+ 0, A, 0, 0,
+ 0, 0, 0, -1,
+ 0, 0, near, 0
+ );
+ }
+
+ osg::Matrix getReversedZProjectionMatrixAsPerspective(double fov, double aspect, double near, double far)
+ {
+ double A = 1.0/std::tan(osg::DegreesToRadians(fov)/2.0);
+ return osg::Matrix(
+ A/aspect, 0, 0, 0,
+ 0, A, 0, 0,
+ 0, 0, near/(far-near), -1,
+ 0, 0, (far*near)/(far - near), 0
+ );
+ }
+
+ osg::Matrix getReversedZProjectionMatrixAsOrtho(double left, double right, double bottom, double top, double near, double far)
+ {
+ return osg::Matrix(
+ 2/(right-left), 0, 0, 0,
+ 0, 2/(top-bottom), 0, 0,
+ 0, 0, 1/(far-near), 0,
+ (right+left)/(left-right), (top+bottom)/(bottom-top), far/(far-near), 1
+ );
+ }
+
+ bool isFloatingPointDepthFormat(GLenum format)
+ {
+ constexpr std::array<GLenum, 4> formats = {
+ GL_DEPTH_COMPONENT32F,
+ GL_DEPTH_COMPONENT32F_NV,
+ GL_DEPTH32F_STENCIL8,
+ GL_DEPTH32F_STENCIL8_NV,
+ };
+
+ return std::find(formats.cbegin(), formats.cend(), format) != formats.cend();
+ }
+} \ No newline at end of file
diff --git a/components/sceneutil/depth.hpp b/components/sceneutil/depth.hpp
new file mode 100644
index 0000000000..11a863ef7a
--- /dev/null
+++ b/components/sceneutil/depth.hpp
@@ -0,0 +1,117 @@
+#ifndef OPENMW_COMPONENTS_SCENEUTIL_DEPTH_H
+#define OPENMW_COMPONENTS_SCENEUTIL_DEPTH_H
+
+#include <osg/Depth>
+
+#include "util.hpp"
+
+namespace SceneUtil
+{
+ // Sets camera clear depth to 0 if reversed depth buffer is in use, 1 otherwise.
+ void setCameraClearDepth(osg::Camera* camera);
+
+ // Returns a perspective projection matrix for use with a reversed z-buffer
+ // and an infinite far plane. This is derived by mapping the default z-range
+ // of [0,1] to [1,0], then taking the limit as far plane approaches infinity.
+ osg::Matrix getReversedZProjectionMatrixAsPerspectiveInf(double fov, double aspect, double near);
+
+ // Returns a perspective projection matrix for use with a reversed z-buffer.
+ osg::Matrix getReversedZProjectionMatrixAsPerspective(double fov, double aspect, double near, double far);
+
+ // Returns an orthographic projection matrix for use with a reversed z-buffer.
+ osg::Matrix getReversedZProjectionMatrixAsOrtho(double left, double right, double bottom, double top, double near, double far);
+
+ // Returns true if the GL format is a floating point depth format.
+ bool isFloatingPointDepthFormat(GLenum format);
+
+ // Brief wrapper around an osg::Depth that applies the reversed depth function when a reversed depth buffer is in use
+ class AutoDepth : public osg::Depth
+ {
+ public:
+ AutoDepth(osg::Depth::Function func=osg::Depth::LESS, double zNear=0.0, double zFar=1.0, bool writeMask=true)
+ {
+ setFunction(func);
+ setZNear(zNear);
+ setZFar(zFar);
+ setWriteMask(writeMask);
+ }
+
+ AutoDepth(const osg::Depth& copy, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY) : osg::Depth(copy, copyop) {}
+
+ osg::Object* cloneType() const override { return new AutoDepth; }
+ osg::Object* clone(const osg::CopyOp& copyop) const override { return new AutoDepth(*this,copyop); }
+
+ void apply(osg::State& state) const override
+ {
+ glDepthFunc(static_cast<GLenum>(AutoDepth::isReversed() ? getReversedDepthFunction() : getFunction()));
+ glDepthMask(static_cast<GLboolean>(getWriteMask()));
+ #if defined(OSG_GLES1_AVAILABLE) || defined(OSG_GLES2_AVAILABLE) || defined(OSG_GLES3_AVAILABLE)
+ glDepthRangef(getZNear(),getZFar());
+ #else
+ glDepthRange(getZNear(),getZFar());
+ #endif
+ }
+
+ static void setReversed(bool reverseZ)
+ {
+ static bool init = false;
+
+ if (!init)
+ {
+ AutoDepth::sReversed = reverseZ;
+ init = true;
+ }
+ }
+
+ static bool isReversed()
+ {
+ return AutoDepth::sReversed;
+ }
+
+ private:
+
+ static inline bool sReversed = false;
+
+ osg::Depth::Function getReversedDepthFunction() const
+ {
+ const osg::Depth::Function func = getFunction();
+
+ switch (func)
+ {
+ case osg::Depth::LESS:
+ return osg::Depth::GREATER;
+ case osg::Depth::LEQUAL:
+ return osg::Depth::GEQUAL;
+ case osg::Depth::GREATER:
+ return osg::Depth::LESS;
+ case osg::Depth::GEQUAL:
+ return osg::Depth::LEQUAL;
+ default:
+ return func;
+ }
+ }
+
+ };
+
+ // Replaces all nodes osg::Depth state attributes with SceneUtil::AutoDepth.
+ class ReplaceDepthVisitor : public osg::NodeVisitor
+ {
+ public:
+ ReplaceDepthVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) {}
+
+ void apply(osg::Node& node) override
+ {
+ osg::StateSet* stateSet = node.getStateSet();
+
+ if (stateSet)
+ {
+ if (osg::Depth* depth = static_cast<osg::Depth*>(stateSet->getAttribute(osg::StateAttribute::DEPTH)))
+ stateSet->setAttribute(new SceneUtil::AutoDepth(*depth));
+ };
+
+ traverse(node);
+ }
+ };
+}
+
+#endif
diff --git a/components/sceneutil/detourdebugdraw.cpp b/components/sceneutil/detourdebugdraw.cpp
index 7ef329fc16..f1325340ea 100644
--- a/components/sceneutil/detourdebugdraw.cpp
+++ b/components/sceneutil/detourdebugdraw.cpp
@@ -32,19 +32,19 @@ namespace
namespace SceneUtil
{
- DebugDraw::DebugDraw(osg::Group& group, const osg::Vec3f& shift, float recastInvertedScaleFactor)
+ DebugDraw::DebugDraw(osg::Group& group, const osg::ref_ptr<osg::StateSet>& stateSet,
+ const osg::Vec3f& shift, float recastInvertedScaleFactor)
: mGroup(group)
+ , mStateSet(stateSet)
, mShift(shift)
, mRecastInvertedScaleFactor(recastInvertedScaleFactor)
- , mDepthMask(false)
, mMode(osg::PrimitiveSet::POINTS)
, mSize(1.0f)
{
}
- void DebugDraw::depthMask(bool state)
+ void DebugDraw::depthMask(bool)
{
- mDepthMask = state;
}
void DebugDraw::texture(bool)
@@ -56,7 +56,7 @@ namespace SceneUtil
mMode = mode;
mVertices = new osg::Vec3Array;
mColors = new osg::Vec4Array;
- mSize = size * mRecastInvertedScaleFactor;
+ mSize = size;
}
void DebugDraw::begin(duDebugDrawPrimitives prim, float size)
@@ -88,16 +88,8 @@ namespace SceneUtil
void DebugDraw::end()
{
- osg::ref_ptr<osg::StateSet> stateSet(new osg::StateSet);
- stateSet->setMode(GL_BLEND, osg::StateAttribute::ON);
- stateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
- stateSet->setMode(GL_DEPTH, (mDepthMask ? osg::StateAttribute::ON : osg::StateAttribute::OFF));
- stateSet->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
- stateSet->setAttributeAndModes(new osg::LineWidth(mSize));
- stateSet->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
-
osg::ref_ptr<osg::Geometry> geometry(new osg::Geometry);
- geometry->setStateSet(stateSet);
+ geometry->setStateSet(mStateSet);
geometry->setVertexArray(mVertices);
geometry->setColorArray(mColors, osg::Array::BIND_PER_VERTEX);
geometry->addPrimitiveSet(new osg::DrawArrays(mMode, 0, static_cast<int>(mVertices->size())));
@@ -117,4 +109,16 @@ namespace SceneUtil
{
mColors->push_back(value);
}
+
+ osg::ref_ptr<osg::StateSet> DebugDraw::makeStateSet()
+ {
+ osg::ref_ptr<osg::StateSet> stateSet = new osg::StateSet;
+ stateSet->setMode(GL_BLEND, osg::StateAttribute::ON);
+ stateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
+ stateSet->setMode(GL_DEPTH, osg::StateAttribute::OFF);
+ stateSet->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
+ stateSet->setAttributeAndModes(new osg::LineWidth());
+ stateSet->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
+ return stateSet;
+ }
}
diff --git a/components/sceneutil/detourdebugdraw.hpp b/components/sceneutil/detourdebugdraw.hpp
index 9b6a28acea..1d00735ea0 100644
--- a/components/sceneutil/detourdebugdraw.hpp
+++ b/components/sceneutil/detourdebugdraw.hpp
@@ -15,7 +15,10 @@ namespace SceneUtil
class DebugDraw : public duDebugDraw
{
public:
- DebugDraw(osg::Group& group, const osg::Vec3f& shift, float recastInvertedScaleFactor);
+ explicit DebugDraw(osg::Group& group, const osg::ref_ptr<osg::StateSet>& stateSet,
+ const osg::Vec3f& shift, float recastInvertedScaleFactor);
+
+ static osg::ref_ptr<osg::StateSet> makeStateSet();
void depthMask(bool state) override;
@@ -38,9 +41,9 @@ namespace SceneUtil
private:
osg::Group& mGroup;
+ osg::ref_ptr<osg::StateSet> mStateSet;
osg::Vec3f mShift;
float mRecastInvertedScaleFactor;
- bool mDepthMask;
osg::PrimitiveSet::Mode mMode;
float mSize;
osg::ref_ptr<osg::Vec3Array> mVertices;
diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp
index 448f6ca916..29907c542d 100644
--- a/components/sceneutil/lightmanager.cpp
+++ b/components/sceneutil/lightmanager.cpp
@@ -29,18 +29,6 @@ namespace
return left->mViewBound.center().length2() - left->mViewBound.radius2()*illuminationBias < right->mViewBound.center().length2() - right->mViewBound.radius2()*illuminationBias;
}
- float getLightRadius(const osg::Light* light)
- {
- float value = 0.0;
- light->getUserValue("radius", value);
- return value;
- }
-
- void setLightRadius(osg::Light* light, float value)
- {
- light->setUserValue("radius", value);
- }
-
void configurePosition(osg::Matrixf& mat, const osg::Vec4& pos)
{
mat(0, 0) = pos.x();
@@ -77,11 +65,6 @@ namespace
mat(2, 3) = q;
mat(3, 3) = r;
}
-
- bool isReflectionCamera(osg::Camera* camera)
- {
- return (camera->getName() == "ReflectionCamera");
- }
}
namespace SceneUtil
@@ -188,19 +171,15 @@ namespace SceneUtil
void configureLayout(int offsetColors, int offsetPosition, int offsetAttenuationRadius, int size, int stride)
{
- const Offsets offsets(offsetColors, offsetPosition, offsetAttenuationRadius, stride);
+ configureLayout(Offsets(offsetColors, offsetPosition, offsetAttenuationRadius, stride), size);
+ }
- // Copy cloned data using current layout into current data using new layout.
- // This allows to preserve osg::FloatArray buffer object in mData.
- const auto data = mData->asVector();
- mData->resizeArray(static_cast<unsigned>(size));
- for (int i = 0; i < mCount; ++i)
- {
- std::memcpy(&(*mData)[offsets.get(i, Diffuse)], data.data() + getOffset(i, Diffuse), sizeof(osg::Vec4f));
- std::memcpy(&(*mData)[offsets.get(i, Position)], data.data() + getOffset(i, Position), sizeof(osg::Vec4f));
- std::memcpy(&(*mData)[offsets.get(i, AttenuationRadius)], data.data() + getOffset(i, AttenuationRadius), sizeof(osg::Vec4f));
- }
- mOffsets = offsets;
+ void configureLayout(const LightBuffer* other)
+ {
+ mOffsets = other->mOffsets;
+ int size = other->mData->size();
+
+ configureLayout(mOffsets, size);
}
private:
@@ -242,6 +221,21 @@ namespace SceneUtil
std::array<int, 6> mValues;
};
+ void configureLayout(const Offsets& offsets, int size)
+ {
+ // Copy cloned data using current layout into current data using new layout.
+ // This allows to preserve osg::FloatArray buffer object in mData.
+ const auto data = mData->asVector();
+ mData->resizeArray(static_cast<unsigned>(size));
+ for (int i = 0; i < mCount; ++i)
+ {
+ std::memcpy(&(*mData)[offsets.get(i, Diffuse)], data.data() + getOffset(i, Diffuse), sizeof(osg::Vec4f));
+ std::memcpy(&(*mData)[offsets.get(i, Position)], data.data() + getOffset(i, Position), sizeof(osg::Vec4f));
+ std::memcpy(&(*mData)[offsets.get(i, AttenuationRadius)], data.data() + getOffset(i, AttenuationRadius), sizeof(osg::Vec4f));
+ }
+ mOffsets = offsets;
+ }
+
osg::ref_ptr<osg::FloatArray> mData;
osg::Endian mEndian;
int mCount;
@@ -249,9 +243,8 @@ namespace SceneUtil
osg::Vec4 mCachedSunPos;
};
- class LightStateCache
+ struct LightStateCache
{
- public:
std::vector<osg::Light*> lastAppliedLight;
};
@@ -281,9 +274,7 @@ namespace SceneUtil
configureDiffuse(lightMat, light->getDiffuse());
configureSpecular(lightMat, light->getSpecular());
- osg::ref_ptr<osg::Uniform> uni = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", lightManager->getMaxLights());
- uni->setElement(0, lightMat);
- stateset->addUniform(uni, mode);
+ stateset->addUniform(lightManager->generateLightBufferUniform(lightMat), mode);
break;
}
case LightingMethod::SingleUBO:
@@ -318,25 +309,20 @@ namespace SceneUtil
DisableLight(const DisableLight& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY)
: osg::StateAttribute(copy,copyop), mIndex(copy.mIndex) {}
- osg::Object* cloneType() const override { return new DisableLight(mIndex); }
- osg::Object* clone(const osg::CopyOp& copyop) const override { return new DisableLight(*this,copyop); }
- bool isSameKindAs(const osg::Object* obj) const override { return dynamic_cast<const DisableLight *>(obj)!=nullptr; }
- const char* libraryName() const override { return "SceneUtil"; }
- const char* className() const override { return "DisableLight"; }
- Type getType() const override { return LIGHT; }
+ META_StateAttribute(SceneUtil, DisableLight, osg::StateAttribute::LIGHT)
unsigned int getMember() const override
{
return mIndex;
}
- bool getModeUsage(ModeUsage & usage) const override
+ bool getModeUsage(ModeUsage& usage) const override
{
usage.usesMode(GL_LIGHT0 + mIndex);
return true;
}
- int compare(const StateAttribute &sa) const override
+ int compare(const StateAttribute& sa) const override
{
throw std::runtime_error("DisableLight::compare: unimplemented");
}
@@ -361,7 +347,7 @@ namespace SceneUtil
{
public:
FFPLightStateAttribute() : mIndex(0) {}
- FFPLightStateAttribute(size_t index, const std::vector<osg::ref_ptr<osg::Light> >& lights) : mIndex(index), mLights(lights) {}
+ FFPLightStateAttribute(size_t index, const std::vector<osg::ref_ptr<osg::Light>>& lights) : mIndex(index), mLights(lights) {}
FFPLightStateAttribute(const FFPLightStateAttribute& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY)
: osg::StateAttribute(copy,copyop), mIndex(copy.mIndex), mLights(copy.mLights) {}
@@ -371,19 +357,19 @@ namespace SceneUtil
return mIndex;
}
- bool getModeUsage(ModeUsage & usage) const override
+ bool getModeUsage(ModeUsage& usage) const override
{
for (size_t i = 0; i < mLights.size(); ++i)
usage.usesMode(GL_LIGHT0 + mIndex + i);
return true;
}
- int compare(const StateAttribute &sa) const override
+ int compare(const StateAttribute& sa) const override
{
throw std::runtime_error("FFPLightStateAttribute::compare: unimplemented");
}
- META_StateAttribute(NifOsg, FFPLightStateAttribute, osg::StateAttribute::LIGHT)
+ META_StateAttribute(SceneUtil, FFPLightStateAttribute, osg::StateAttribute::LIGHT)
void apply(osg::State& state) const override
{
@@ -429,69 +415,6 @@ namespace SceneUtil
std::vector<osg::ref_ptr<osg::Light>> mLights;
};
- LightManager* findLightManager(const osg::NodePath& path)
- {
- for (size_t i = 0; i < path.size(); ++i)
- {
- if (LightManager* lightManager = dynamic_cast<LightManager*>(path[i]))
- return lightManager;
- }
- return nullptr;
- }
-
- class LightStateAttributePerObjectUniform : public osg::StateAttribute
- {
- public:
- LightStateAttributePerObjectUniform() : mLightManager(nullptr) {}
- LightStateAttributePerObjectUniform(const std::vector<osg::ref_ptr<osg::Light>>& lights, LightManager* lightManager) : mLights(lights), mLightManager(lightManager) {}
-
- LightStateAttributePerObjectUniform(const LightStateAttributePerObjectUniform& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY)
- : osg::StateAttribute(copy,copyop), mLights(copy.mLights), mLightManager(copy.mLightManager) {}
-
- int compare(const StateAttribute &sa) const override
- {
- throw std::runtime_error("LightStateAttributePerObjectUniform::compare: unimplemented");
- }
-
- META_StateAttribute(NifOsg, LightStateAttributePerObjectUniform, osg::StateAttribute::LIGHT)
-
- void resize(int numLights)
- {
- mLights.resize(std::min(static_cast<size_t>(numLights), mLights.size()));
- }
-
- void apply(osg::State &state) const override
- {
- osg::StateSet* stateSet = mLightManager->getStateSet();
- if (!stateSet)
- return;
-
- auto* lightUniform = stateSet->getUniform("LightBuffer");
- for (size_t i = 0; i < mLights.size(); ++i)
- {
- auto light = mLights[i];
- osg::Matrixf lightMat;
-
- configurePosition(lightMat, light->getPosition() * state.getInitialViewMatrix());
- configureAmbient(lightMat, light->getAmbient());
- configureDiffuse(lightMat, light->getDiffuse());
- configureAttenuation(lightMat, light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), getLightRadius(light));
-
- lightUniform->setElement(i+1, lightMat);
- }
-
- auto sun = mLightManager->getSunlightBuffer(state.getFrameStamp()->getFrameNumber());
- configurePosition(sun, osg::Vec4(sun(0,0), sun(0,1), sun(0,2), 0.0) * state.getInitialViewMatrix());
- lightUniform->setElement(0, sun);
-
- lightUniform->dirty();
- }
-
- private:
- std::vector<osg::ref_ptr<osg::Light>> mLights;
- LightManager* mLightManager;
- };
-
struct StateSetGenerator
{
LightManager* mLightManager;
@@ -501,6 +424,8 @@ namespace SceneUtil
virtual osg::ref_ptr<osg::StateSet> generate(const LightManager::LightList& lightList, size_t frameNum) = 0;
virtual void update(osg::StateSet* stateset, const LightManager::LightList& lightList, size_t frameNum) {}
+
+ osg::Matrix mViewMatrix;
};
struct StateSetGeneratorFFP : StateSetGenerator
@@ -588,37 +513,50 @@ namespace SceneUtil
osg::ref_ptr<osg::StateSet> generate(const LightManager::LightList& lightList, size_t frameNum) override
{
osg::ref_ptr<osg::StateSet> stateset = new osg::StateSet;
-
- std::vector<osg::ref_ptr<osg::Light>> lights(lightList.size());
+ osg::ref_ptr<osg::Uniform> data = mLightManager->generateLightBufferUniform(mLightManager->getSunlightBuffer(frameNum));
for (size_t i = 0; i < lightList.size(); ++i)
{
auto* light = lightList[i]->mLightSource->getLight(frameNum);
- lights[i] = light;
- setLightRadius(light, lightList[i]->mLightSource->getRadius());
- }
+ osg::Matrixf lightMat;
+ configurePosition(lightMat, light->getPosition() * mViewMatrix);
+ configureAmbient(lightMat, light->getAmbient());
+ configureDiffuse(lightMat, light->getDiffuse());
+ configureAttenuation(lightMat, light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), lightList[i]->mLightSource->getRadius());
- stateset->setAttributeAndModes(new LightStateAttributePerObjectUniform(std::move(lights), mLightManager), osg::StateAttribute::ON);
+ data->setElement(i+1, lightMat);
+ }
+ stateset->addUniform(data);
stateset->addUniform(new osg::Uniform("PointLightCount", static_cast<int>(lightList.size() + 1)));
return stateset;
}
};
+ LightManager* findLightManager(const osg::NodePath& path)
+ {
+ for (size_t i = 0; i < path.size(); ++i)
+ {
+ if (LightManager* lightManager = dynamic_cast<LightManager*>(path[i]))
+ return lightManager;
+ }
+ return nullptr;
+ }
+
// Set on a LightSource. Adds the light source to its light manager for the current frame.
// This allows us to keep track of the current lights in the scene graph without tying creation & destruction to the manager.
- class CollectLightCallback : public SceneUtil::NodeCallback<CollectLightCallback>
+ class CollectLightCallback : public NodeCallback<CollectLightCallback>
{
public:
CollectLightCallback()
: mLightManager(nullptr) { }
CollectLightCallback(const CollectLightCallback& copy, const osg::CopyOp& copyop)
- : SceneUtil::NodeCallback<CollectLightCallback>(copy, copyop)
+ : NodeCallback<CollectLightCallback>(copy, copyop)
, mLightManager(nullptr) { }
- META_Object(SceneUtil, SceneUtil::CollectLightCallback)
+ META_Object(SceneUtil, CollectLightCallback)
void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
@@ -656,169 +594,163 @@ namespace SceneUtil
class LightManagerCullCallback : public SceneUtil::NodeCallback<LightManagerCullCallback, LightManager*, osgUtil::CullVisitor*>
{
public:
- LightManagerCullCallback() : mLastFrameNumber(0) {}
-
void operator()(LightManager* node, osgUtil::CullVisitor* cv)
{
- if (mLastFrameNumber != cv->getTraversalNumber())
- {
- mLastFrameNumber = cv->getTraversalNumber();
+ osg::ref_ptr<osg::StateSet> stateset = new osg::StateSet;
- if (node->getLightingMethod() == LightingMethod::SingleUBO)
- {
- auto stateset = node->getStateSet();
- auto bo = node->getLightBuffer(mLastFrameNumber);
+ if (node->getLightingMethod() == LightingMethod::SingleUBO)
+ {
+ auto buffer = node->getUBOManager()->getLightBuffer(cv->getTraversalNumber());
#if OSG_VERSION_GREATER_OR_EQUAL(3,5,7)
- osg::ref_ptr<osg::UniformBufferBinding> ubb = new osg::UniformBufferBinding(static_cast<int>(Resource::SceneManager::UBOBinding::LightBuffer), bo->getData(), 0, bo->getData()->getTotalDataSize());
+ osg::ref_ptr<osg::UniformBufferBinding> ubb = new osg::UniformBufferBinding(static_cast<int>(Resource::SceneManager::UBOBinding::LightBuffer), buffer->getData(), 0, buffer->getData()->getTotalDataSize());
#else
- osg::ref_ptr<osg::UniformBufferBinding> ubb = new osg::UniformBufferBinding(static_cast<int>(Resource::SceneManager::UBOBinding::LightBuffer), bo->getData()->getBufferObject(), 0, bo->getData()->getTotalDataSize());
+ osg::ref_ptr<osg::UniformBufferBinding> ubb = new osg::UniformBufferBinding(static_cast<int>(Resource::SceneManager::UBOBinding::LightBuffer), buffer->getData()->getBufferObject(), 0, buffer->getData()->getTotalDataSize());
#endif
- stateset->setAttributeAndModes(ubb, osg::StateAttribute::ON);
- }
+ stateset->setAttributeAndModes(ubb, osg::StateAttribute::ON);
- auto sun = node->getSunlight();
-
- if (sun)
+ if (auto sun = node->getSunlight())
{
- // we must defer uploading the transformation to view-space position to deal with different cameras (e.g. reflection RTT).
- if (node->getLightingMethod() == LightingMethod::PerObjectUniform)
- {
- osg::Matrixf lightMat;
- configurePosition(lightMat, sun->getPosition());
- configureAmbient(lightMat, sun->getAmbient());
- configureDiffuse(lightMat, sun->getDiffuse());
- configureSpecular(lightMat, sun->getSpecular());
- node->setSunlightBuffer(lightMat, mLastFrameNumber);
- }
- else
- {
- auto buf = node->getLightBuffer(mLastFrameNumber);
-
- buf->setCachedSunPos(sun->getPosition());
- buf->setAmbient(0, sun->getAmbient());
- buf->setDiffuse(0, sun->getDiffuse());
- buf->setSpecular(0, sun->getSpecular());
- }
+ buffer->setCachedSunPos(sun->getPosition());
+ buffer->setAmbient(0, sun->getAmbient());
+ buffer->setDiffuse(0, sun->getDiffuse());
+ buffer->setSpecular(0, sun->getSpecular());
+ }
+ }
+ else if (node->getLightingMethod() == LightingMethod::PerObjectUniform)
+ {
+ if (auto sun = node->getSunlight())
+ {
+ osg::Matrixf lightMat;
+ configurePosition(lightMat, sun->getPosition() * (*cv->getCurrentRenderStage()->getInitialViewMatrix()));
+ configureAmbient(lightMat, sun->getAmbient());
+ configureDiffuse(lightMat, sun->getDiffuse());
+ configureSpecular(lightMat, sun->getSpecular());
+ node->setSunlightBuffer(lightMat, cv->getTraversalNumber());
+ stateset->addUniform(node->generateLightBufferUniform(lightMat));
}
}
+ cv->pushStateSet(stateset);
traverse(node, cv);
+ cv->popStateSet();
}
-
- private:
- size_t mLastFrameNumber;
};
- class LightManagerStateAttribute : public osg::StateAttribute
+ UBOManager::UBOManager(int lightCount)
+ : mDummyProgram(new osg::Program)
+ , mInitLayout(false)
+ , mDirty({ true, true })
+ , mTemplate(new LightBuffer(lightCount))
{
- public:
- LightManagerStateAttribute()
- : mLightManager(nullptr)
- , mInitLayout(false)
- {
- }
+ static const std::string dummyVertSource = generateDummyShader(lightCount);
- LightManagerStateAttribute(LightManager* lightManager)
- : mLightManager(lightManager)
- , mDummyProgram(new osg::Program)
- , mInitLayout(false)
- {
- static const std::string dummyVertSource = generateDummyShader(mLightManager->getMaxLightsInScene());
+ // Needed to query the layout of the buffer object. The layout specifier needed to use the std140 layout is not reliably
+ // available, regardless of extensions, until GLSL 140.
+ mDummyProgram->addShader(new osg::Shader(osg::Shader::VERTEX, dummyVertSource));
+ mDummyProgram->addBindUniformBlock("LightBufferBinding", static_cast<int>(Resource::SceneManager::UBOBinding::LightBuffer));
- // Needed to query the layout of the buffer object. The layout specifier needed to use the std140 layout is not reliably
- // available, regardless of extensions, until GLSL 140.
- mDummyProgram->addShader(new osg::Shader(osg::Shader::VERTEX, dummyVertSource));
- mDummyProgram->addBindUniformBlock("LightBufferBinding", static_cast<int>(Resource::SceneManager::UBOBinding::LightBuffer));
- }
+ for (size_t i = 0; i < mLightBuffers.size(); ++i)
+ {
+ mLightBuffers[i] = new LightBuffer(lightCount);
- LightManagerStateAttribute(const LightManagerStateAttribute& copy, const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY)
- : osg::StateAttribute(copy,copyop), mLightManager(copy.mLightManager), mInitLayout(copy.mInitLayout) {}
+ osg::ref_ptr<osg::UniformBufferObject> ubo = new osg::UniformBufferObject;
+ ubo->setUsage(GL_STREAM_DRAW);
- int compare(const StateAttribute &sa) const override
- {
- throw std::runtime_error("LightManagerStateAttribute::compare: unimplemented");
+ mLightBuffers[i]->getData()->setBufferObject(ubo);
}
+ }
- META_StateAttribute(NifOsg, LightManagerStateAttribute, osg::StateAttribute::LIGHT)
+ UBOManager::UBOManager(const UBOManager& copy, const osg::CopyOp& copyop) : osg::StateAttribute(copy,copyop), mDummyProgram(copy.mDummyProgram), mInitLayout(copy.mInitLayout) {}
- void initSharedLayout(osg::GLExtensions* ext, int handle) const
- {
- constexpr std::array<unsigned int, 1> index = { static_cast<unsigned int>(Resource::SceneManager::UBOBinding::LightBuffer) };
- int totalBlockSize = -1;
- int stride = -1;
+ void UBOManager::releaseGLObjects(osg::State* state) const
+ {
+ mDummyProgram->releaseGLObjects(state);
+ }
- ext->glGetActiveUniformBlockiv(handle, 0, GL_UNIFORM_BLOCK_DATA_SIZE, &totalBlockSize);
- ext->glGetActiveUniformsiv(handle, index.size(), index.data(), GL_UNIFORM_ARRAY_STRIDE, &stride);
+ int UBOManager::compare(const StateAttribute &sa) const
+ {
+ throw std::runtime_error("LightManagerStateAttribute::compare: unimplemented");
+ }
- std::array<const char*, 3> names = {
- "LightBuffer[0].packedColors",
- "LightBuffer[0].position",
- "LightBuffer[0].attenuation",
- };
- std::vector<unsigned int> indices(names.size());
- std::vector<int> offsets(names.size());
+ void UBOManager::apply(osg::State& state) const
+ {
+ unsigned int frame = state.getFrameStamp()->getFrameNumber();
+ unsigned int index = frame % 2;
- ext->glGetUniformIndices(handle, names.size(), names.data(), indices.data());
- ext->glGetActiveUniformsiv(handle, indices.size(), indices.data(), GL_UNIFORM_OFFSET, offsets.data());
+ if (!mInitLayout)
+ {
+ mDummyProgram->apply(state);
+ auto handle = mDummyProgram->getPCP(state)->getHandle();
+ auto* ext = state.get<osg::GLExtensions>();
- for (int i = 0; i < 2; ++i)
- mLightManager->getLightBuffer(i)->configureLayout(offsets[0], offsets[1], offsets[2], totalBlockSize, stride);
- }
+ int activeUniformBlocks = 0;
+ ext->glGetProgramiv(handle, GL_ACTIVE_UNIFORM_BLOCKS, &activeUniformBlocks);
- void apply(osg::State& state) const override
- {
- if (!mInitLayout)
+ // wait until the UBO binding is created
+ if (activeUniformBlocks > 0)
{
- mDummyProgram->apply(state);
- auto handle = mDummyProgram->getPCP(state)->getHandle();
- auto* ext = state.get<osg::GLExtensions>();
+ initSharedLayout(ext, handle, frame);
+ mInitLayout = true;
+ }
+ }
+ else if (mDirty[index])
+ {
+ mDirty[index] = false;
+ mLightBuffers[index]->configureLayout(mTemplate);
+ }
- int activeUniformBlocks = 0;
- ext->glGetProgramiv(handle, GL_ACTIVE_UNIFORM_BLOCKS, &activeUniformBlocks);
+ mLightBuffers[index]->uploadCachedSunPos(state.getInitialViewMatrix());
+ mLightBuffers[index]->dirty();
+ }
- // wait until the UBO binding is created
- if (activeUniformBlocks > 0)
- {
- initSharedLayout(ext, handle);
- mInitLayout = true;
- }
+ std::string UBOManager::generateDummyShader(int maxLightsInScene)
+ {
+ const std::string define = "@maxLightsInScene";
+
+ std::string shader = R"GLSL(
+ #version 120
+ #extension GL_ARB_uniform_buffer_object : require
+ struct LightData {
+ ivec4 packedColors;
+ vec4 position;
+ vec4 attenuation;
+ };
+ uniform LightBufferBinding {
+ LightData LightBuffer[@maxLightsInScene];
+ };
+ void main()
+ {
+ gl_Position = vec4(0.0);
}
- mLightManager->getLightBuffer(state.getFrameStamp()->getFrameNumber())->uploadCachedSunPos(state.getInitialViewMatrix());
- mLightManager->getLightBuffer(state.getFrameStamp()->getFrameNumber())->dirty();
- }
+ )GLSL";
- private:
+ shader.replace(shader.find(define), define.length(), std::to_string(maxLightsInScene));
+ return shader;
+ }
- std::string generateDummyShader(int maxLightsInScene)
- {
- const std::string define = "@maxLightsInScene";
-
- std::string shader = R"GLSL(
- #version 120
- #extension GL_ARB_uniform_buffer_object : require
- struct LightData {
- ivec4 packedColors;
- vec4 position;
- vec4 attenuation;
- };
- uniform LightBufferBinding {
- LightData LightBuffer[@maxLightsInScene];
- };
- void main()
- {
- gl_Position = vec4(0.0);
- }
- )GLSL";
+ void UBOManager::initSharedLayout(osg::GLExtensions* ext, int handle, unsigned int frame) const
+ {
+ constexpr std::array<unsigned int, 1> index = { static_cast<unsigned int>(Resource::SceneManager::UBOBinding::LightBuffer) };
+ int totalBlockSize = -1;
+ int stride = -1;
- shader.replace(shader.find(define), define.length(), std::to_string(maxLightsInScene));
- return shader;
- }
+ ext->glGetActiveUniformBlockiv(handle, 0, GL_UNIFORM_BLOCK_DATA_SIZE, &totalBlockSize);
+ ext->glGetActiveUniformsiv(handle, index.size(), index.data(), GL_UNIFORM_ARRAY_STRIDE, &stride);
- LightManager* mLightManager;
- osg::ref_ptr<osg::Program> mDummyProgram;
- mutable bool mInitLayout;
- };
+ std::array<const char*, 3> names = {
+ "LightBuffer[0].packedColors",
+ "LightBuffer[0].position",
+ "LightBuffer[0].attenuation",
+ };
+ std::vector<unsigned int> indices(names.size());
+ std::vector<int> offsets(names.size());
+
+ ext->glGetUniformIndices(handle, names.size(), names.data(), indices.data());
+ ext->glGetActiveUniformsiv(handle, indices.size(), indices.data(), GL_UNIFORM_OFFSET, offsets.data());
+
+ mTemplate->configureLayout(offsets[0], offsets[1], offsets[2], totalBlockSize, stride);
+ }
const std::unordered_map<std::string, LightingMethod> LightManager::mLightingMethodSettingMap = {
{"legacy", LightingMethod::FFP}
@@ -845,11 +777,6 @@ namespace SceneUtil
return "";
}
- LightManager::~LightManager()
- {
- getOrCreateStateSet()->removeAttribute(osg::StateAttribute::LIGHT);
- }
-
LightManager::LightManager(bool ffp)
: mStartLight(0)
, mLightingMask(~0u)
@@ -899,7 +826,7 @@ namespace SceneUtil
getOrCreateStateSet()->addUniform(new osg::Uniform("PointLightCount", 0));
- addCullCallback(new LightManagerCullCallback());
+ addCullCallback(new LightManagerCullCallback);
}
LightManager::LightManager(const LightManager &copy, const osg::CopyOp &copyop)
@@ -970,55 +897,16 @@ namespace SceneUtil
if (usingFFP())
return;
- int targetLights = std::clamp(Settings::Manager::getInt("max lights", "Shaders"), mMaxLightsLowerLimit, mMaxLightsUpperLimit);
- setMaxLights(targetLights);
+ setMaxLights(std::clamp(Settings::Manager::getInt("max lights", "Shaders"), mMaxLightsLowerLimit, mMaxLightsUpperLimit));
if (getLightingMethod() == LightingMethod::PerObjectUniform)
{
- auto* prevUniform = getStateSet()->getUniform("LightBuffer");
- osg::ref_ptr<osg::Uniform> newUniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", getMaxLights());
-
- for (int i = 0; i < getMaxLights(); ++i)
- {
- osg::Matrixf prevLightData;
- prevUniform->getElement(i, prevLightData);
- newUniform->setElement(i, prevLightData);
- }
-
- getStateSet()->removeUniform(prevUniform);
- getStateSet()->addUniform(newUniform);
-
- for (int i = 0; i < 2; ++i)
- {
- for (auto& pair : mStateSetCache[i])
- static_cast<LightStateAttributePerObjectUniform*>(pair.second->getAttribute(osg::StateAttribute::LIGHT))->resize(getMaxLights());
- mStateSetCache[i].clear();
- }
+ getStateSet()->removeUniform("LightBuffer");
+ getStateSet()->addUniform(generateLightBufferUniform(osg::Matrixf()));
}
- else
- {
- for (int i = 0; i < 2; ++i)
- {
- for (auto& pair : mStateSetCache[i])
- {
- auto& stateset = pair.second;
- osg::Uniform* uOldArray = stateset->getUniform("PointLightIndex");
- osg::Uniform* uOldCount = stateset->getUniform("PointLightCount");
- int prevCount;
- uOldCount->get(prevCount);
- int newCount = std::min(getMaxLights(), prevCount);
- uOldCount->set(newCount);
-
- osg::ref_ptr<osg::IntArray> newArray = uOldArray->getIntArray();
- newArray->resize(newCount);
-
- stateset->removeUniform(uOldArray);
- stateset->addUniform(new osg::Uniform("PointLightIndex", newArray));
- }
- mStateSetCache[i].clear();
- }
- }
+ for (auto& cache : mStateSetCache)
+ cache.clear();
}
void LightManager::updateSettings()
@@ -1047,14 +935,10 @@ namespace SceneUtil
void LightManager::initPerObjectUniform(int targetLights)
{
- auto* stateset = getOrCreateStateSet();
-
setLightingMethod(LightingMethod::PerObjectUniform);
setMaxLights(targetLights);
- // ensures sunlight element in our uniform array is updated when there are no point lights in scene
- stateset->setAttributeAndModes(new LightStateAttributePerObjectUniform({}, this), osg::StateAttribute::ON);
- stateset->addUniform(new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", getMaxLights()));
+ getOrCreateStateSet()->addUniform(generateLightBufferUniform(osg::Matrixf()));
}
void LightManager::initSingleUBO(int targetLights)
@@ -1062,17 +946,8 @@ namespace SceneUtil
setLightingMethod(LightingMethod::SingleUBO);
setMaxLights(targetLights);
- for (int i = 0; i < 2; ++i)
- {
- mLightBuffers[i] = new LightBuffer(getMaxLightsInScene());
-
- osg::ref_ptr<osg::UniformBufferObject> ubo = new osg::UniformBufferObject;
- ubo->setUsage(GL_STREAM_DRAW);
-
- mLightBuffers[i]->getData()->setBufferObject(ubo);
- }
-
- getOrCreateStateSet()->setAttribute(new LightManagerStateAttribute(this), osg::StateAttribute::ON);
+ mUBOManager = new UBOManager(getMaxLightsInScene());
+ getOrCreateStateSet()->setAttributeAndModes(mUBOManager);
}
void LightManager::setLightingMethod(LightingMethod method)
@@ -1171,6 +1046,12 @@ namespace SceneUtil
osg::ref_ptr<osg::StateSet> LightManager::getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix)
{
+ if (getLightingMethod() == LightingMethod::PerObjectUniform)
+ {
+ mStateSetGenerator->mViewMatrix = *viewMatrix;
+ return mStateSetGenerator->generate(lightList, frameNum);
+ }
+
// possible optimization: return a StateSet containing all requested lights plus some extra lights (if a suitable one exists)
if (getLightingMethod() == LightingMethod::SingleUBO)
@@ -1205,7 +1086,7 @@ namespace SceneUtil
return stateset;
}
- const std::vector<LightManager::LightSourceViewBound>& LightManager::getLightsInViewSpace(osgUtil::CullVisitor *cv, const osg::RefMatrix* viewMatrix, size_t frameNum)
+ const std::vector<LightManager::LightSourceViewBound>& LightManager::getLightsInViewSpace(osgUtil::CullVisitor* cv, const osg::RefMatrix* viewMatrix, size_t frameNum)
{
osg::Camera* camera = cv->getCurrentCamera();
@@ -1216,8 +1097,6 @@ namespace SceneUtil
{
it = mLightsInViewSpace.insert(std::make_pair(camPtr, LightSourceViewBoundCollection())).first;
- bool isReflection = isReflectionCamera(camera);
-
for (const auto& transform : mLights)
{
osg::Matrixf worldViewMat = transform.mWorldMatrix * (*viewMatrix);
@@ -1227,7 +1106,7 @@ namespace SceneUtil
osg::BoundingSphere viewBound = osg::BoundingSphere(osg::Vec3f(0,0,0), radius);
transformBoundingSphere(worldViewMat, viewBound);
- if (!isReflection && mPointLightFadeEnd != 0.f)
+ if (transform.mLightSource->getLastAppliedFrame() != frameNum && mPointLightFadeEnd != 0.f)
{
const float fadeDelta = mPointLightFadeEnd - mPointLightFadeStart;
float fade = 1 - std::clamp((viewBound.center().length() - mPointLightFadeStart) / fadeDelta, 0.f, 1.f);
@@ -1236,6 +1115,7 @@ namespace SceneUtil
auto* light = transform.mLightSource->getLight(frameNum);
light->setDiffuse(light->getDiffuse() * fade);
+ transform.mLightSource->setLastAppliedFrame(frameNum);
}
// remove lights culled by this camera
@@ -1272,16 +1152,25 @@ namespace SceneUtil
void LightManager::updateGPUPointLight(int index, LightSource* lightSource, size_t frameNum,const osg::RefMatrix* viewMatrix)
{
auto* light = lightSource->getLight(frameNum);
- auto& buf = getLightBuffer(frameNum);
+ auto& buf = getUBOManager()->getLightBuffer(frameNum);
buf->setDiffuse(index, light->getDiffuse());
buf->setAmbient(index, light->getAmbient());
buf->setAttenuationRadius(index, osg::Vec4(light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), lightSource->getRadius()));
buf->setPosition(index, light->getPosition() * (*viewMatrix));
}
+ osg::ref_ptr<osg::Uniform> LightManager::generateLightBufferUniform(const osg::Matrixf& sun)
+ {
+ osg::ref_ptr<osg::Uniform> uniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", getMaxLights());
+ uniform->setElement(0, sun);
+
+ return uniform;
+ }
+
LightSource::LightSource()
: mRadius(0.f)
, mActorFade(1.f)
+ , mLastAppliedFrame(0)
{
setUpdateCallback(new CollectLightCallback);
mId = sLightId++;
@@ -1291,10 +1180,11 @@ namespace SceneUtil
: osg::Node(copy, copyop)
, mRadius(copy.mRadius)
, mActorFade(copy.mActorFade)
+ , mLastAppliedFrame(copy.mLastAppliedFrame)
{
mId = sLightId++;
- for (int i = 0; i < 2; ++i)
+ for (size_t i = 0; i < mLight.size(); ++i)
mLight[i] = new osg::Light(*copy.mLight[i].get(), copyop);
}
@@ -1358,7 +1248,7 @@ namespace SceneUtil
{
size_t maxLights = mLightManager->getMaxLights() - mLightManager->getStartLight();
- osg::StateSet* stateset = nullptr;
+ osg::ref_ptr<osg::StateSet> stateset = nullptr;
if (mLightList.size() > maxLights)
{
diff --git a/components/sceneutil/lightmanager.hpp b/components/sceneutil/lightmanager.hpp
index 4a7dc7dbe7..64b7e325ad 100644
--- a/components/sceneutil/lightmanager.hpp
+++ b/components/sceneutil/lightmanager.hpp
@@ -2,13 +2,11 @@
#define OPENMW_COMPONENTS_SCENEUTIL_LIGHTMANAGER_H
#include <set>
-#include <unordered_set>
#include <unordered_map>
#include <memory>
#include <array>
#include <osg/Light>
-
#include <osg/Group>
#include <osg/NodeVisitor>
#include <osg/observer_ptr>
@@ -46,7 +44,7 @@ namespace SceneUtil
class LightSource : public osg::Node
{
// double buffered osg::Light's, since one of them may be in use by the draw thread at any given time
- osg::ref_ptr<osg::Light> mLight[2];
+ std::array<osg::ref_ptr<osg::Light>, 2> mLight;
// LightSource will affect objects within this radius
float mRadius;
@@ -55,6 +53,8 @@ namespace SceneUtil
float mActorFade;
+ unsigned int mLastAppliedFrame;
+
public:
META_Node(SceneUtil, LightSource)
@@ -107,6 +107,43 @@ namespace SceneUtil
{
return mId;
}
+
+ void setLastAppliedFrame(unsigned int lastAppliedFrame)
+ {
+ mLastAppliedFrame = lastAppliedFrame;
+ }
+
+ unsigned int getLastAppliedFrame() const
+ {
+ return mLastAppliedFrame;
+ }
+ };
+
+ class UBOManager : public osg::StateAttribute
+ {
+ public:
+ UBOManager(int lightCount=1);
+ UBOManager(const UBOManager& copy, const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY);
+
+ void releaseGLObjects(osg::State* state) const override;
+
+ int compare(const StateAttribute& sa) const override;
+
+ META_StateAttribute(SceneUtil, UBOManager, osg::StateAttribute::LIGHT)
+
+ void apply(osg::State& state) const override;
+
+ auto& getLightBuffer(size_t frameNum) { return mLightBuffers[frameNum%2]; }
+
+ private:
+ std::string generateDummyShader(int maxLightsInScene);
+ void initSharedLayout(osg::GLExtensions* ext, int handle, unsigned int frame) const;
+
+ osg::ref_ptr<osg::Program> mDummyProgram;
+ mutable bool mInitLayout;
+ mutable std::array<osg::ref_ptr<LightBuffer>, 2> mLightBuffers;
+ mutable std::array<bool, 2> mDirty;
+ osg::ref_ptr<LightBuffer> mTemplate;
};
/// @brief Decorator node implementing the rendering of any number of LightSources that can be anywhere in the subgraph.
@@ -138,8 +175,6 @@ namespace SceneUtil
LightManager(const LightManager& copy, const osg::CopyOp& copyop);
- ~LightManager();
-
/// @param mask This mask is compared with the current Camera's cull mask to determine if lighting is desired.
/// By default, it's ~0u i.e. always on.
/// If you have some views that do not require lighting, then set the Camera's cull mask to not include
@@ -176,7 +211,7 @@ namespace SceneUtil
auto& getLightIndexMap(size_t frameNum) { return mLightIndexMaps[frameNum%2]; }
- auto& getLightBuffer(size_t frameNum) { return mLightBuffers[frameNum%2]; }
+ auto& getUBOManager() { return mUBOManager; }
osg::Matrixf getSunlightBuffer(size_t frameNum) const { return mSunlightBuffers[frameNum%2]; }
void setSunlightBuffer(const osg::Matrixf& buffer, size_t frameNum) { mSunlightBuffers[frameNum%2] = buffer; }
@@ -190,6 +225,8 @@ namespace SceneUtil
/// Not thread safe, it is the responsibility of the caller to stop/start threading on the viewer
void updateMaxLights();
+ osg::ref_ptr<osg::Uniform> generateLightBufferUniform(const osg::Matrixf& sun);
+
private:
void initFFP(int targetLights);
void initPerObjectUniform(int targetLights);
@@ -223,8 +260,6 @@ namespace SceneUtil
osg::ref_ptr<osg::Light> mSun;
- osg::ref_ptr<LightBuffer> mLightBuffers[2];
-
osg::Matrixf mSunlightBuffers[2];
// < Light ID , Buffer Index >
@@ -233,6 +268,8 @@ namespace SceneUtil
std::unique_ptr<StateSetGenerator> mStateSetGenerator;
+ osg::ref_ptr<UBOManager> mUBOManager;
+
LightingMethod mLightingMethod;
float mPointLightRadiusMultiplier;
diff --git a/components/sceneutil/morphgeometry.cpp b/components/sceneutil/morphgeometry.cpp
index 59adbffffe..3e34e3dedc 100644
--- a/components/sceneutil/morphgeometry.cpp
+++ b/components/sceneutil/morphgeometry.cpp
@@ -104,8 +104,8 @@ void MorphGeometry::accept(osg::PrimitiveFunctor& func) const
osg::BoundingBox MorphGeometry::computeBoundingBox() const
{
bool anyMorphTarget = false;
- for (unsigned int i=0; i<mMorphTargets.size(); ++i)
- if (mMorphTargets[i].getWeight() > 0)
+ for (unsigned int i=1; i<mMorphTargets.size(); ++i)
+ if (mMorphTargets[i].getWeight() != 0)
{
anyMorphTarget = true;
break;
@@ -122,8 +122,10 @@ osg::BoundingBox MorphGeometry::computeBoundingBox() const
{
mMorphedBoundingBox = true;
- osg::Vec3Array& sourceVerts = *static_cast<osg::Vec3Array*>(mSourceGeometry->getVertexArray());
- std::vector<osg::BoundingBox> vertBounds(sourceVerts.size());
+ const osg::Vec3Array* sourceVerts = static_cast<const osg::Vec3Array*>(mSourceGeometry->getVertexArray());
+ if (mMorphTargets.size() != 0)
+ sourceVerts = mMorphTargets[0].getOffsets();
+ std::vector<osg::BoundingBox> vertBounds(sourceVerts->size());
// Since we don't know what combinations of morphs are being applied we need to keep track of a bounding box for each vertex.
// The minimum/maximum of the box is the minimum/maximum offset the vertex can have from its starting position.
@@ -132,7 +134,7 @@ osg::BoundingBox MorphGeometry::computeBoundingBox() const
for (unsigned int i=0; i<vertBounds.size(); ++i)
vertBounds[i].set(osg::Vec3f(0,0,0), osg::Vec3f(0,0,0));
- for (unsigned int i = 0; i < mMorphTargets.size(); ++i)
+ for (unsigned int i = 1; i < mMorphTargets.size(); ++i)
{
const osg::Vec3Array& offsets = *mMorphTargets[i].getOffsets();
for (unsigned int j=0; j<offsets.size() && j<vertBounds.size(); ++j)
@@ -146,8 +148,8 @@ osg::BoundingBox MorphGeometry::computeBoundingBox() const
osg::BoundingBox box;
for (unsigned int i=0; i<vertBounds.size(); ++i)
{
- vertBounds[i]._max += sourceVerts[i];
- vertBounds[i]._min += sourceVerts[i];
+ vertBounds[i]._max += (*sourceVerts)[i];
+ vertBounds[i]._min += (*sourceVerts)[i];
box.expandBy(vertBounds[i]);
}
return box;
@@ -156,7 +158,7 @@ osg::BoundingBox MorphGeometry::computeBoundingBox() const
void MorphGeometry::cull(osg::NodeVisitor *nv)
{
- if (mLastFrameNumber == nv->getTraversalNumber() || !mDirty)
+ if (mLastFrameNumber == nv->getTraversalNumber() || !mDirty || mMorphTargets.size() == 0)
{
osg::Geometry& geom = *getGeometry(mLastFrameNumber);
nv->pushOntoNodePath(&geom);
@@ -169,13 +171,13 @@ void MorphGeometry::cull(osg::NodeVisitor *nv)
mLastFrameNumber = nv->getTraversalNumber();
osg::Geometry& geom = *getGeometry(mLastFrameNumber);
- const osg::Vec3Array* positionSrc = static_cast<osg::Vec3Array*>(mSourceGeometry->getVertexArray());
+ const osg::Vec3Array* positionSrc = mMorphTargets[0].getOffsets();
osg::Vec3Array* positionDst = static_cast<osg::Vec3Array*>(geom.getVertexArray());
assert(positionSrc->size() == positionDst->size());
for (unsigned int vertex=0; vertex<positionSrc->size(); ++vertex)
(*positionDst)[vertex] = (*positionSrc)[vertex];
- for (unsigned int i=0; i<mMorphTargets.size(); ++i)
+ for (unsigned int i=1; i<mMorphTargets.size(); ++i)
{
float weight = mMorphTargets[i].getWeight();
if (weight == 0.f)
diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp
index 024123b3e1..d56708e46c 100644
--- a/components/sceneutil/mwshadowtechnique.cpp
+++ b/components/sceneutil/mwshadowtechnique.cpp
@@ -27,8 +27,6 @@
#include <sstream>
-#include <components/sceneutil/util.hpp>
-
#include "shadowsbin.hpp"
namespace {
@@ -279,14 +277,7 @@ void VDSMCameraCullCallback::operator()(osg::Node* node, osg::NodeVisitor* nv)
}
#endif
// bin has to go inside camera cull or the rendertexture stage will override it
- static osg::ref_ptr<osg::StateSet> ss;
- if (!ss)
- {
- ShadowsBinAdder adder("ShadowsBin", _vdsm->getCastingPrograms());
- ss = new osg::StateSet;
- ss->setRenderBinDetails(osg::StateSet::OPAQUE_BIN, "ShadowsBin", osg::StateSet::OVERRIDE_PROTECTED_RENDERBIN_DETAILS);
- }
- cv->pushStateSet(ss);
+ cv->pushStateSet(_vdsm->getOrCreateShadowsBinStateSet());
if (_vdsm->getShadowedScene())
{
_vdsm->getShadowedScene()->osg::Group::traverse(*nv);
@@ -811,6 +802,8 @@ MWShadowTechnique::MWShadowTechnique(const MWShadowTechnique& vdsm, const osg::C
MWShadowTechnique::~MWShadowTechnique()
{
+ if (_shadowsBin != nullptr)
+ osgUtil::RenderBin::removeRenderBinPrototype(_shadowsBin);
}
@@ -1411,7 +1404,7 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv)
std::string validRegionUniformName = "validRegionMatrix" + std::to_string(sm_i);
osg::ref_ptr<osg::Uniform> validRegionUniform;
- for (auto uniform : _uniforms[cv.getTraversalNumber() % 2])
+ for (const auto & uniform : _uniforms[cv.getTraversalNumber() % 2])
{
if (uniform->getName() == validRegionUniformName)
validRegionUniform = uniform;
@@ -1657,11 +1650,8 @@ void MWShadowTechnique::createShaders()
_shadowCastingStateSet->addUniform(new osg::Uniform("alphaTestShadows", false));
osg::ref_ptr<osg::Depth> depth = new osg::Depth;
depth->setWriteMask(true);
- if (SceneUtil::getReverseZ())
- {
- osg::ref_ptr<osg::ClipControl> clipcontrol = new osg::ClipControl(osg::ClipControl::LOWER_LEFT, osg::ClipControl::NEGATIVE_ONE_TO_ONE);
- _shadowCastingStateSet->setAttribute(clipcontrol, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
- }
+ osg::ref_ptr<osg::ClipControl> clipcontrol = new osg::ClipControl(osg::ClipControl::LOWER_LEFT, osg::ClipControl::NEGATIVE_ONE_TO_ONE);
+ _shadowCastingStateSet->setAttribute(clipcontrol, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
_shadowCastingStateSet->setAttribute(depth, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
_shadowCastingStateSet->setMode(GL_DEPTH_CLAMP, osg::StateAttribute::ON);
@@ -3282,3 +3272,18 @@ void SceneUtil::MWShadowTechnique::DebugHUD::addAnotherShadowMap()
for(auto& uniformVector : mFrustumUniforms)
uniformVector.push_back(new osg::Uniform(osg::Uniform::FLOAT_MAT4, "transform"));
}
+
+osg::ref_ptr<osg::StateSet> SceneUtil::MWShadowTechnique::getOrCreateShadowsBinStateSet()
+{
+ if (_shadowsBinStateSet == nullptr)
+ {
+ if (_shadowsBin == nullptr)
+ {
+ _shadowsBin = new ShadowsBin(_castingPrograms);
+ osgUtil::RenderBin::addRenderBinPrototype(_shadowsBinName, _shadowsBin);
+ }
+ _shadowsBinStateSet = new osg::StateSet;
+ _shadowsBinStateSet->setRenderBinDetails(osg::StateSet::OPAQUE_BIN, _shadowsBinName, osg::StateSet::OVERRIDE_PROTECTED_RENDERBIN_DETAILS);
+ }
+ return _shadowsBinStateSet;
+}
diff --git a/components/sceneutil/mwshadowtechnique.hpp b/components/sceneutil/mwshadowtechnique.hpp
index 3f6c0fb765..574d054204 100644
--- a/components/sceneutil/mwshadowtechnique.hpp
+++ b/components/sceneutil/mwshadowtechnique.hpp
@@ -21,6 +21,7 @@
#include <array>
#include <mutex>
+#include <string>
#include <osg/Camera>
#include <osg/Material>
@@ -215,8 +216,6 @@ namespace SceneUtil {
virtual void createShaders();
- virtual std::array<osg::ref_ptr<osg::Program>, GL_ALWAYS - GL_NEVER + 1> getCastingPrograms() const { return _castingPrograms; }
-
virtual bool selectActiveLights(osgUtil::CullVisitor* cv, ViewDependentData* vdd) const;
virtual osg::Polytope computeLightViewFrustumPolytope(Frustum& frustum, LightData& positionedLight);
@@ -237,6 +236,8 @@ namespace SceneUtil {
void setWorldMask(unsigned int worldMask) { _worldMask = worldMask; }
+ osg::ref_ptr<osg::StateSet> getOrCreateShadowsBinStateSet();
+
protected:
virtual ~MWShadowTechnique();
@@ -297,6 +298,9 @@ namespace SceneUtil {
osg::ref_ptr<DebugHUD> _debugHud;
std::array<osg::ref_ptr<osg::Program>, GL_ALWAYS - GL_NEVER + 1> _castingPrograms;
+ const std::string _shadowsBinName = "ShadowsBin_" + std::to_string(reinterpret_cast<std::uint64_t>(this));
+ osg::ref_ptr<osgUtil::RenderBin> _shadowsBin;
+ osg::ref_ptr<osg::StateSet> _shadowsBinStateSet;
};
}
diff --git a/components/sceneutil/navmesh.cpp b/components/sceneutil/navmesh.cpp
index b0f356f089..eac3d17156 100644
--- a/components/sceneutil/navmesh.cpp
+++ b/components/sceneutil/navmesh.cpp
@@ -1,5 +1,6 @@
#include "navmesh.hpp"
#include "detourdebugdraw.hpp"
+#include "depth.hpp"
#include <components/detournavigator/settings.hpp>
@@ -7,21 +8,256 @@
#include <osg/Group>
#include <osg/Material>
+#include <osg/PolygonOffset>
-namespace SceneUtil
+namespace
{
- osg::ref_ptr<osg::Group> createNavMeshGroup(const dtNavMesh& navMesh, const DetourNavigator::Settings& settings)
+ // Copied from https://github.com/recastnavigation/recastnavigation/blob/c5cbd53024c8a9d8d097a4371215e3342d2fdc87/DebugUtils/Source/DetourDebugDraw.cpp#L26-L38
+ float distancePtLine2d(const float* pt, const float* p, const float* q)
{
- const osg::ref_ptr<osg::Group> group(new osg::Group);
- DebugDraw debugDraw(*group, osg::Vec3f(0, 0, 10), 1.0f / settings.mRecastScaleFactor);
- dtNavMeshQuery navMeshQuery;
- navMeshQuery.init(&navMesh, settings.mMaxNavMeshQueryNodes);
- duDebugDrawNavMeshWithClosedList(&debugDraw, navMesh, navMeshQuery,
- DU_DRAWNAVMESH_OFFMESHCONS | DU_DRAWNAVMESH_CLOSEDLIST);
+ float pqx = q[0] - p[0];
+ float pqz = q[2] - p[2];
+ float dx = pt[0] - p[0];
+ float dz = pt[2] - p[2];
+ float d = pqx*pqx + pqz*pqz;
+ float t = pqx*dx + pqz*dz;
+ if (d != 0) t /= d;
+ dx = p[0] + t*pqx - pt[0];
+ dz = p[2] + t*pqz - pt[2];
+ return dx*dx + dz*dz;
+ }
+
+ // Copied from https://github.com/recastnavigation/recastnavigation/blob/c5cbd53024c8a9d8d097a4371215e3342d2fdc87/DebugUtils/Source/DetourDebugDraw.cpp#L40-L118
+ void drawPolyBoundaries(duDebugDraw* dd, const dtMeshTile* tile, const unsigned int col,
+ const float linew, bool inner)
+ {
+ static const float thr = 0.01f*0.01f;
+
+ dd->begin(DU_DRAW_LINES, linew);
+
+ for (int i = 0; i < tile->header->polyCount; ++i)
+ {
+ const dtPoly* p = &tile->polys[i];
+
+ if (p->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) continue;
+
+ const dtPolyDetail* pd = &tile->detailMeshes[i];
+
+ for (int j = 0, nj = (int)p->vertCount; j < nj; ++j)
+ {
+ unsigned int c = col;
+ if (inner)
+ {
+ if (p->neis[j] == 0) continue;
+ if (p->neis[j] & DT_EXT_LINK)
+ {
+ bool con = false;
+ for (unsigned int k = p->firstLink; k != DT_NULL_LINK; k = tile->links[k].next)
+ {
+ if (tile->links[k].edge == j)
+ {
+ con = true;
+ break;
+ }
+ }
+ if (con)
+ c = duRGBA(255,255,255,48);
+ else
+ c = duRGBA(0,0,0,48);
+ }
+ else
+ c = duRGBA(0,48,64,32);
+ }
+ else
+ {
+ if (p->neis[j] != 0) continue;
+ }
+
+ const float* v0 = &tile->verts[p->verts[j]*3];
+ const float* v1 = &tile->verts[p->verts[(j+1) % nj]*3];
+
+ // Draw detail mesh edges which align with the actual poly edge.
+ // This is really slow.
+ for (int k = 0; k < pd->triCount; ++k)
+ {
+ const unsigned char* t = &tile->detailTris[(pd->triBase+k)*4];
+ const float* tv[3];
+ for (int m = 0; m < 3; ++m)
+ {
+ if (t[m] < p->vertCount)
+ tv[m] = &tile->verts[p->verts[t[m]]*3];
+ else
+ tv[m] = &tile->detailVerts[(pd->vertBase+(t[m]-p->vertCount))*3];
+ }
+ for (int m = 0, n = 2; m < 3; n=m++)
+ {
+ if ((dtGetDetailTriEdgeFlags(t[3], n) & DT_DETAIL_EDGE_BOUNDARY) == 0)
+ continue;
+
+ if (distancePtLine2d(tv[n],v0,v1) < thr &&
+ distancePtLine2d(tv[m],v0,v1) < thr)
+ {
+ dd->vertex(tv[n], c);
+ dd->vertex(tv[m], c);
+ }
+ }
+ }
+ }
+ }
+ dd->end();
+ }
+
+ // Based on https://github.com/recastnavigation/recastnavigation/blob/c5cbd53024c8a9d8d097a4371215e3342d2fdc87/DebugUtils/Source/DetourDebugDraw.cpp#L120-L235
+ void drawMeshTile(duDebugDraw* dd, const dtNavMesh& mesh, const dtNavMeshQuery* query,
+ const dtMeshTile* tile, unsigned char flags)
+ {
+ dtPolyRef base = mesh.getPolyRefBase(tile);
+
+ int tileNum = mesh.decodePolyIdTile(base);
+ const unsigned int tileNumColor = duIntToCol(tileNum, 128);
+ const unsigned alpha = tile->header->userId == 0 ? 64 : 128;
+
+ dd->depthMask(false);
+
+ dd->begin(DU_DRAW_TRIS);
+ for (int i = 0; i < tile->header->polyCount; ++i)
+ {
+ const dtPoly* p = &tile->polys[i];
+ if (p->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) // Skip off-mesh links.
+ continue;
+
+ const dtPolyDetail* pd = &tile->detailMeshes[i];
+
+ unsigned int col;
+ if (query && query->isInClosedList(base | (dtPolyRef)i))
+ col = duRGBA(255, 196, 0, alpha);
+ else
+ {
+ if (flags & DU_DRAWNAVMESH_COLOR_TILES)
+ col = duTransCol(tileNumColor, alpha);
+ else
+ col = duTransCol(dd->areaToCol(p->getArea()), alpha);
+ }
+
+ for (int j = 0; j < pd->triCount; ++j)
+ {
+ const unsigned char* t = &tile->detailTris[(pd->triBase+j)*4];
+ for (int k = 0; k < 3; ++k)
+ {
+ if (t[k] < p->vertCount)
+ dd->vertex(&tile->verts[p->verts[t[k]]*3], col);
+ else
+ dd->vertex(&tile->detailVerts[(pd->vertBase+t[k]-p->vertCount)*3], col);
+ }
+ }
+ }
+ dd->end();
+
+ // Draw inter poly boundaries
+ drawPolyBoundaries(dd, tile, duRGBA(0,48,64,32), 1.5f, true);
+
+ // Draw outer poly boundaries
+ drawPolyBoundaries(dd, tile, duRGBA(0,48,64,220), 2.5f, false);
+ if (flags & DU_DRAWNAVMESH_OFFMESHCONS)
+ {
+ dd->begin(DU_DRAW_LINES, 2.0f);
+ for (int i = 0; i < tile->header->polyCount; ++i)
+ {
+ const dtPoly* p = &tile->polys[i];
+ if (p->getType() != DT_POLYTYPE_OFFMESH_CONNECTION) // Skip regular polys.
+ continue;
+
+ unsigned int col, col2;
+ if (query && query->isInClosedList(base | (dtPolyRef)i))
+ col = duRGBA(255,196,0,220);
+ else
+ col = duDarkenCol(duTransCol(dd->areaToCol(p->getArea()), 220));
+
+ const dtOffMeshConnection* con = &tile->offMeshCons[i - tile->header->offMeshBase];
+ const float* va = &tile->verts[p->verts[0]*3];
+ const float* vb = &tile->verts[p->verts[1]*3];
+
+ // Check to see if start and end end-points have links.
+ bool startSet = false;
+ bool endSet = false;
+ for (unsigned int k = p->firstLink; k != DT_NULL_LINK; k = tile->links[k].next)
+ {
+ if (tile->links[k].edge == 0)
+ startSet = true;
+ if (tile->links[k].edge == 1)
+ endSet = true;
+ }
+
+ // End points and their on-mesh locations.
+ dd->vertex(va[0],va[1],va[2], col);
+ dd->vertex(con->pos[0],con->pos[1],con->pos[2], col);
+ col2 = startSet ? col : duRGBA(220,32,16,196);
+ duAppendCircle(dd, con->pos[0],con->pos[1]+0.1f,con->pos[2], con->rad, col2);
+
+ dd->vertex(vb[0],vb[1],vb[2], col);
+ dd->vertex(con->pos[3],con->pos[4],con->pos[5], col);
+ col2 = endSet ? col : duRGBA(220,32,16,196);
+ duAppendCircle(dd, con->pos[3],con->pos[4]+0.1f,con->pos[5], con->rad, col2);
+
+ // End point vertices.
+ dd->vertex(con->pos[0],con->pos[1],con->pos[2], duRGBA(0,48,64,196));
+ dd->vertex(con->pos[0],con->pos[1]+0.2f,con->pos[2], duRGBA(0,48,64,196));
+
+ dd->vertex(con->pos[3],con->pos[4],con->pos[5], duRGBA(0,48,64,196));
+ dd->vertex(con->pos[3],con->pos[4]+0.2f,con->pos[5], duRGBA(0,48,64,196));
+
+ // Connection arc.
+ duAppendArc(dd, con->pos[0],con->pos[1],con->pos[2], con->pos[3],con->pos[4],con->pos[5], 0.25f,
+ (con->flags & 1) ? 0.6f : 0, 0.6f, col);
+ }
+ dd->end();
+ }
+
+ const unsigned int vcol = duRGBA(0,0,0,196);
+ dd->begin(DU_DRAW_POINTS, 3.0f);
+ for (int i = 0; i < tile->header->vertCount; ++i)
+ {
+ const float* v = &tile->verts[i*3];
+ dd->vertex(v[0], v[1], v[2], vcol);
+ }
+ dd->end();
+
+ dd->depthMask(true);
+ }
+}
+
+namespace SceneUtil
+{
+ osg::ref_ptr<osg::StateSet> makeNavMeshTileStateSet()
+ {
osg::ref_ptr<osg::Material> material = new osg::Material;
material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE);
- group->getOrCreateStateSet()->setAttribute(material);
+
+ const float polygonOffsetFactor = SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0;
+ const float polygonOffsetUnits = SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0;
+ osg::ref_ptr<osg::PolygonOffset> polygonOffset = new osg::PolygonOffset(polygonOffsetFactor, polygonOffsetUnits);
+
+ osg::ref_ptr<osg::StateSet> stateSet = new osg::StateSet;
+ stateSet->setAttribute(material);
+ stateSet->setAttributeAndModes(polygonOffset);
+ return stateSet;
+ }
+
+ osg::ref_ptr<osg::Group> createNavMeshTileGroup(const dtNavMesh& navMesh, const dtMeshTile& meshTile,
+ const DetourNavigator::Settings& settings, const osg::ref_ptr<osg::StateSet>& groupStateSet,
+ const osg::ref_ptr<osg::StateSet>& debugDrawStateSet)
+ {
+ if (meshTile.header == nullptr)
+ return nullptr;
+
+ osg::ref_ptr<osg::Group> group(new osg::Group);
+ group->setStateSet(groupStateSet);
+ constexpr float shift = 10.0f;
+ DebugDraw debugDraw(*group, debugDrawStateSet, osg::Vec3f(0, 0, shift), 1.0f / settings.mRecast.mRecastScaleFactor);
+ dtNavMeshQuery navMeshQuery;
+ navMeshQuery.init(&navMesh, settings.mDetour.mMaxNavMeshQueryNodes);
+ drawMeshTile(&debugDraw, navMesh, &navMeshQuery, &meshTile, DU_DRAWNAVMESH_OFFMESHCONS | DU_DRAWNAVMESH_CLOSEDLIST);
return group;
}
diff --git a/components/sceneutil/navmesh.hpp b/components/sceneutil/navmesh.hpp
index b255b05756..ca9b8bd570 100644
--- a/components/sceneutil/navmesh.hpp
+++ b/components/sceneutil/navmesh.hpp
@@ -4,10 +4,12 @@
#include <osg/ref_ptr>
class dtNavMesh;
+struct dtMeshTile;
namespace osg
{
class Group;
+ class StateSet;
}
namespace DetourNavigator
@@ -17,7 +19,11 @@ namespace DetourNavigator
namespace SceneUtil
{
- osg::ref_ptr<osg::Group> createNavMeshGroup(const dtNavMesh& navMesh, const DetourNavigator::Settings& settings);
+ osg::ref_ptr<osg::StateSet> makeNavMeshTileStateSet();
+
+ osg::ref_ptr<osg::Group> createNavMeshTileGroup(const dtNavMesh& navMesh, const dtMeshTile& meshTile,
+ const DetourNavigator::Settings& settings, const osg::ref_ptr<osg::StateSet>& groupStateSet,
+ const osg::ref_ptr<osg::StateSet>& debugDrawStateSet);
}
#endif
diff --git a/components/sceneutil/optimizer.cpp b/components/sceneutil/optimizer.cpp
index b9d2f5bac7..748ceee952 100644
--- a/components/sceneutil/optimizer.cpp
+++ b/components/sceneutil/optimizer.cpp
@@ -42,7 +42,7 @@
#include <iterator>
-#include <components/sceneutil/util.hpp>
+#include <components/sceneutil/depth.hpp>
using namespace osgUtil;
@@ -1137,7 +1137,7 @@ bool isAbleToMerge(const osg::Geometry& g1, const osg::Geometry& g2)
bool Optimizer::MergeGeometryVisitor::pushStateSet(osg::StateSet *stateSet)
{
- if (_mergeAlphaBlending || !stateSet || stateSet->getRenderBinMode() & osg::StateSet::INHERIT_RENDERBIN_DETAILS)
+ if (!stateSet || stateSet->getRenderBinMode() & osg::StateSet::INHERIT_RENDERBIN_DETAILS)
return false;
_stateSetStack.push_back(stateSet);
checkAlphaBlendingActive();
@@ -1597,8 +1597,8 @@ bool Optimizer::MergeGeometryVisitor::mergeGroup(osg::Group& group)
}
if (_alphaBlendingActive && _mergeAlphaBlending && !geom->getStateSet())
{
- auto d = createDepth();
- d->setWriteMask(0);
+ osg::ref_ptr<osg::Depth> d = new SceneUtil::AutoDepth;
+ d->setWriteMask(false);
geom->getOrCreateStateSet()->setAttribute(d);
}
}
diff --git a/components/sceneutil/osgacontroller.cpp b/components/sceneutil/osgacontroller.cpp
index 520b2d177b..4d0f7a460a 100644
--- a/components/sceneutil/osgacontroller.cpp
+++ b/components/sceneutil/osgacontroller.cpp
@@ -33,7 +33,7 @@ namespace SceneUtil
if (channelTargetName != umt->getName()) continue;
// check if we can link a StackedTransformElement to the current Channel
- for (auto stackedTransform : umt->getStackedTransforms())
+ for (const auto & stackedTransform : umt->getStackedTransforms())
{
osgAnimation::StackedTransformElement* element = stackedTransform.get();
if (element && !element->getName().empty() && channelName == element->getName())
diff --git a/components/sceneutil/recastmesh.cpp b/components/sceneutil/recastmesh.cpp
index 18cec022ff..d320624682 100644
--- a/components/sceneutil/recastmesh.cpp
+++ b/components/sceneutil/recastmesh.cpp
@@ -1,5 +1,6 @@
-#include "navmesh.hpp"
+#include "recastmesh.hpp"
#include "detourdebugdraw.hpp"
+#include "depth.hpp"
#include <components/detournavigator/settings.hpp>
#include <components/detournavigator/recastmesh.hpp>
@@ -9,6 +10,7 @@
#include <osg/Group>
#include <osg/Material>
+#include <osg/PolygonOffset>
#include <algorithm>
#include <vector>
@@ -40,12 +42,12 @@ namespace
namespace SceneUtil
{
osg::ref_ptr<osg::Group> createRecastMeshGroup(const DetourNavigator::RecastMesh& recastMesh,
- const DetourNavigator::Settings& settings)
+ const DetourNavigator::RecastSettings& settings)
{
using namespace DetourNavigator;
const osg::ref_ptr<osg::Group> group(new osg::Group);
- DebugDraw debugDraw(*group, osg::Vec3f(0, 0, 0), 1.0f);
+ DebugDraw debugDraw(*group, DebugDraw::makeStateSet(), osg::Vec3f(0, 0, 0), 1.0f);
const DetourNavigator::Mesh& mesh = recastMesh.getMesh();
std::vector<int> indices = mesh.getIndices();
std::vector<float> vertices = mesh.getVertices();
@@ -69,7 +71,14 @@ namespace SceneUtil
osg::ref_ptr<osg::Material> material = new osg::Material;
material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE);
- group->getOrCreateStateSet()->setAttribute(material);
+
+ const float polygonOffsetFactor = SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0;
+ const float polygonOffsetUnits = SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0;
+ osg::ref_ptr<osg::PolygonOffset> polygonOffset = new osg::PolygonOffset(polygonOffsetFactor, polygonOffsetUnits);
+
+ osg::ref_ptr<osg::StateSet> stateSet = group->getOrCreateStateSet();
+ stateSet->setAttribute(material);
+ stateSet->setAttributeAndModes(polygonOffset);
return group;
}
diff --git a/components/sceneutil/recastmesh.hpp b/components/sceneutil/recastmesh.hpp
index ee5d9865e5..674b5b1d2a 100644
--- a/components/sceneutil/recastmesh.hpp
+++ b/components/sceneutil/recastmesh.hpp
@@ -11,13 +11,13 @@ namespace osg
namespace DetourNavigator
{
class RecastMesh;
- struct Settings;
+ struct RecastSettings;
}
namespace SceneUtil
{
osg::ref_ptr<osg::Group> createRecastMeshGroup(const DetourNavigator::RecastMesh& recastMesh,
- const DetourNavigator::Settings& settings);
+ const DetourNavigator::RecastSettings& settings);
}
#endif
diff --git a/components/sceneutil/rtt.cpp b/components/sceneutil/rtt.cpp
index 3b60c80afd..0765a2e835 100644
--- a/components/sceneutil/rtt.cpp
+++ b/components/sceneutil/rtt.cpp
@@ -8,6 +8,7 @@
#include <components/sceneutil/nodecallback.hpp>
#include <components/settings/settings.hpp>
+#include <components/sceneutil/depth.hpp>
namespace SceneUtil
{
@@ -69,6 +70,7 @@ namespace SceneUtil
camera->setClearMask(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT);
camera->setViewport(0, 0, mTextureWidth, mTextureHeight);
+ SceneUtil::setCameraClearDepth(camera);
setDefaults(mViewDependentDataMap[cv]->mCamera.get());
diff --git a/components/sceneutil/serialize.cpp b/components/sceneutil/serialize.cpp
index 9da0d6a40e..964b8bc4c2 100644
--- a/components/sceneutil/serialize.cpp
+++ b/components/sceneutil/serialize.cpp
@@ -123,8 +123,10 @@ void registerSerializers()
"Resource::TemplateRef",
"Resource::TemplateMultiRef",
"SceneUtil::CompositeStateSetUpdater",
+ "SceneUtil::UBOManager",
"SceneUtil::LightListCallback",
"SceneUtil::LightManagerUpdateCallback",
+ "SceneUtil::FFPLightStateAttribute",
"SceneUtil::UpdateRigBounds",
"SceneUtil::UpdateRigGeometry",
"SceneUtil::LightSource",
@@ -133,7 +135,6 @@ void registerSerializers()
"SceneUtil::TextKeyMapHolder",
"Shader::AddedState",
"Shader::RemovedAlphaFunc",
- "NifOsg::LightManagerStateAttribute",
"NifOsg::FlipController",
"NifOsg::KeyframeController",
"NifOsg::Emitter",
diff --git a/components/sceneutil/shadow.cpp b/components/sceneutil/shadow.cpp
index 8c3758e980..dfe4cf1507 100644
--- a/components/sceneutil/shadow.cpp
+++ b/components/sceneutil/shadow.cpp
@@ -27,8 +27,7 @@ namespace SceneUtil
mShadowSettings->setLightNum(0);
mShadowSettings->setReceivesShadowTraversalMask(~0u);
- int numberOfShadowMapsPerLight = Settings::Manager::getInt("number of shadow maps", "Shadows");
- numberOfShadowMapsPerLight = std::max(1, std::min(numberOfShadowMapsPerLight, 8));
+ const int numberOfShadowMapsPerLight = std::clamp(Settings::Manager::getInt("number of shadow maps", "Shadows"), 1, 8);
mShadowSettings->setNumShadowMapsPerLight(numberOfShadowMapsPerLight);
mShadowSettings->setBaseShadowTextureUnit(8 - numberOfShadowMapsPerLight);
@@ -36,7 +35,7 @@ namespace SceneUtil
const float maximumShadowMapDistance = Settings::Manager::getFloat("maximum shadow map distance", "Shadows");
if (maximumShadowMapDistance > 0)
{
- const float shadowFadeStart = std::min(std::max(0.f, Settings::Manager::getFloat("shadow fade start", "Shadows")), 1.f);
+ const float shadowFadeStart = std::clamp(Settings::Manager::getFloat("shadow fade start", "Shadows"), 0.f, 1.f);
mShadowSettings->setMaximumShadowMapDistance(maximumShadowMapDistance);
mShadowTechnique->setShadowFadeStart(maximumShadowMapDistance * shadowFadeStart);
}
@@ -78,8 +77,7 @@ namespace SceneUtil
if (!Settings::Manager::getBool("enable shadows", "Shadows"))
return;
- int numberOfShadowMapsPerLight = Settings::Manager::getInt("number of shadow maps", "Shadows");
- numberOfShadowMapsPerLight = std::max(1, std::min(numberOfShadowMapsPerLight, 8));
+ const int numberOfShadowMapsPerLight = std::clamp(Settings::Manager::getInt("number of shadow maps", "Shadows"), 1, 8);
int baseShadowTextureUnit = 8 - numberOfShadowMapsPerLight;
diff --git a/components/sceneutil/shadowsbin.cpp b/components/sceneutil/shadowsbin.cpp
index af62b581c9..3e933cbb98 100644
--- a/components/sceneutil/shadowsbin.cpp
+++ b/components/sceneutil/shadowsbin.cpp
@@ -42,11 +42,7 @@ namespace
namespace SceneUtil
{
-std::array<osg::ref_ptr<osg::Program>, GL_ALWAYS - GL_NEVER + 1> ShadowsBin::sCastingPrograms = {
- nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr
-};
-
-ShadowsBin::ShadowsBin()
+ShadowsBin::ShadowsBin(const CastingPrograms& castingPrograms)
{
mNoTestStateSet = new osg::StateSet;
mNoTestStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", false));
@@ -57,10 +53,10 @@ ShadowsBin::ShadowsBin()
mShaderAlphaTestStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", true));
mShaderAlphaTestStateSet->setMode(GL_BLEND, osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED | osg::StateAttribute::OVERRIDE);
- for (size_t i = 0; i < sCastingPrograms.size(); ++i)
+ for (size_t i = 0; i < castingPrograms.size(); ++i)
{
mAlphaFuncShaders[i] = new osg::StateSet;
- mAlphaFuncShaders[i]->setAttribute(sCastingPrograms[i], osg::StateAttribute::ON | osg::StateAttribute::PROTECTED | osg::StateAttribute::OVERRIDE);
+ mAlphaFuncShaders[i]->setAttribute(castingPrograms[i], osg::StateAttribute::ON | osg::StateAttribute::PROTECTED | osg::StateAttribute::OVERRIDE);
}
}
@@ -150,13 +146,6 @@ StateGraph* ShadowsBin::cullStateGraph(StateGraph* sg, StateGraph* root, std::un
return sg;
}
-void ShadowsBin::addPrototype(const std::string & name, const std::array<osg::ref_ptr<osg::Program>, GL_ALWAYS - GL_NEVER + 1>& castingPrograms)
-{
- sCastingPrograms = castingPrograms;
- osg::ref_ptr<osgUtil::RenderBin> bin(new ShadowsBin);
- osgUtil::RenderBin::addRenderBinPrototype(name, bin);
-}
-
inline bool ShadowsBin::State::needTexture() const
{
return mAlphaBlend || (mAlphaFunc && mAlphaFunc->getFunction() != GL_ALWAYS);
diff --git a/components/sceneutil/shadowsbin.hpp b/components/sceneutil/shadowsbin.hpp
index 1c63caf4bb..2c838d509e 100644
--- a/components/sceneutil/shadowsbin.hpp
+++ b/components/sceneutil/shadowsbin.hpp
@@ -12,20 +12,17 @@ namespace osg
namespace SceneUtil
{
-
/// renderbin which culls redundant state for shadow map rendering
class ShadowsBin : public osgUtil::RenderBin
{
- private:
- static std::array<osg::ref_ptr<osg::Program>, GL_ALWAYS - GL_NEVER + 1> sCastingPrograms;
+ public:
+ template <class T>
+ using Array = std::array<T, GL_ALWAYS - GL_NEVER + 1>;
- osg::ref_ptr<osg::StateSet> mNoTestStateSet;
- osg::ref_ptr<osg::StateSet> mShaderAlphaTestStateSet;
+ using CastingPrograms = Array<osg::ref_ptr<osg::Program>>;
- std::array<osg::ref_ptr<osg::StateSet>, GL_ALWAYS - GL_NEVER + 1> mAlphaFuncShaders;
- public:
META_Object(SceneUtil, ShadowsBin)
- ShadowsBin();
+ ShadowsBin(const CastingPrograms& castingPrograms);
ShadowsBin(const ShadowsBin& rhs, const osg::CopyOp& copyop)
: osgUtil::RenderBin(rhs, copyop)
, mNoTestStateSet(rhs.mNoTestStateSet)
@@ -65,15 +62,14 @@ namespace SceneUtil
osgUtil::StateGraph* cullStateGraph(osgUtil::StateGraph* sg, osgUtil::StateGraph* root, std::unordered_set<osgUtil::StateGraph*>& uninteresting, bool cullFaceOverridden);
- static void addPrototype(const std::string& name, const std::array<osg::ref_ptr<osg::Program>, GL_ALWAYS - GL_NEVER + 1>& castingPrograms);
- };
+ private:
+ ShadowsBin() {}
- class ShadowsBinAdder
- {
- public:
- ShadowsBinAdder(const std::string& name, const std::array<osg::ref_ptr<osg::Program>, GL_ALWAYS - GL_NEVER + 1>& castingPrograms){ ShadowsBin::addPrototype(name, castingPrograms); }
- };
+ osg::ref_ptr<osg::StateSet> mNoTestStateSet;
+ osg::ref_ptr<osg::StateSet> mShaderAlphaTestStateSet;
+ Array<osg::ref_ptr<osg::StateSet>> mAlphaFuncShaders;
+ };
}
#endif
diff --git a/components/sceneutil/util.cpp b/components/sceneutil/util.cpp
index 5c2f8c2934..33d8f1c8a6 100644
--- a/components/sceneutil/util.cpp
+++ b/components/sceneutil/util.cpp
@@ -4,40 +4,18 @@
#include <sstream>
#include <iomanip>
-#include <SDL_opengl_glext.h>
-
#include <osg/Node>
#include <osg/NodeVisitor>
#include <osg/TexGen>
#include <osg/TexEnvCombine>
-#include <osg/Version>
#include <osg/FrameBufferObject>
#include <osgUtil/RenderStage>
#include <osgUtil/CullVisitor>
#include <components/resource/imagemanager.hpp>
#include <components/resource/scenemanager.hpp>
-#include <components/settings/settings.hpp>
-#include <components/debug/debuglog.hpp>
#include <components/sceneutil/nodecallback.hpp>
-#ifndef GL_DEPTH32F_STENCIL8_NV
-#define GL_DEPTH32F_STENCIL8_NV 0x8DAC
-#endif
-
-namespace
-{
-
-bool isReverseZSupported()
-{
- if (!Settings::Manager::mDefaultSettings.count({"Camera", "reverse z"}))
- return false;
- auto ext = osg::GLExtensions::Get(0, false);
- return Settings::Manager::getBool("reverse z", "Camera") && ext && ext->isClipControlSupported;
-}
-
-}
-
namespace SceneUtil
{
@@ -324,12 +302,6 @@ osg::ref_ptr<GlowUpdater> addEnchantedGlow(osg::ref_ptr<osg::Node> node, Resourc
bool attachAlphaToCoverageFriendlyFramebufferToCamera(osg::Camera* camera, osg::Camera::BufferComponent buffer, osg::Texture * texture, unsigned int level, unsigned int face, bool mipMapGeneration)
{
-#if OSG_VERSION_LESS_THAN(3, 6, 6)
- // hack fix for https://github.com/openscenegraph/OpenSceneGraph/issues/1028
- osg::ref_ptr<osg::GLExtensions> extensions = osg::GLExtensions::Get(0, false);
- if (extensions)
- extensions->glRenderbufferStorageMultisampleCoverageNV = nullptr;
-#endif
unsigned int samples = 0;
unsigned int colourSamples = 0;
bool addMSAAIntermediateTarget = Settings::Manager::getBool("antialias alpha test", "Shaders") && Settings::Manager::getInt("antialiasing", "Video") > 1;
@@ -345,81 +317,20 @@ bool attachAlphaToCoverageFriendlyFramebufferToCamera(osg::Camera* camera, osg::
return addMSAAIntermediateTarget;
}
-void attachAlphaToCoverageFriendlyDepthColor(osg::Camera* camera, osg::Texture2D* colorTex, osg::Texture2D* depthTex, GLenum depthFormat)
-{
- bool addMSAAIntermediateTarget = Settings::Manager::getBool("antialias alpha test", "Shaders") && Settings::Manager::getInt("antialiasing", "Video") > 1;
-
- if (isFloatingPointDepthFormat(depthFormat) && addMSAAIntermediateTarget)
- {
- camera->attach(osg::Camera::COLOR_BUFFER0, colorTex);
- camera->attach(osg::Camera::DEPTH_BUFFER, depthTex);
- camera->addCullCallback(new AttachMultisampledDepthColorCallback(colorTex, depthTex, 2, 1));
- }
- else
- {
- attachAlphaToCoverageFriendlyFramebufferToCamera(camera, osg::Camera::COLOR_BUFFER, colorTex);
- camera->attach(osg::Camera::DEPTH_BUFFER, depthTex);
- }
-}
-
-bool getReverseZ()
-{
- static bool reverseZ = isReverseZSupported();
- return reverseZ;
-}
-
-void setCameraClearDepth(osg::Camera* camera)
-{
- camera->setClearDepth(getReverseZ() ? 0.0 : 1.0);
-}
-
-osg::ref_ptr<osg::Depth> createDepth()
-{
- return new osg::Depth(getReverseZ() ? osg::Depth::GEQUAL : osg::Depth::LEQUAL);
-}
-
-osg::Matrix getReversedZProjectionMatrixAsPerspectiveInf(double fov, double aspect, double near)
-{
- double A = 1.0/std::tan(osg::DegreesToRadians(fov)/2.0);
- return osg::Matrix(
- A/aspect, 0, 0, 0,
- 0, A, 0, 0,
- 0, 0, 0, -1,
- 0, 0, near, 0
- );
-}
-
-osg::Matrix getReversedZProjectionMatrixAsPerspective(double fov, double aspect, double near, double far)
+OperationSequence::OperationSequence(bool keep)
+ : Operation("OperationSequence", keep)
+ , mOperationQueue(new osg::OperationQueue())
{
- double A = 1.0/std::tan(osg::DegreesToRadians(fov)/2.0);
- return osg::Matrix(
- A/aspect, 0, 0, 0,
- 0, A, 0, 0,
- 0, 0, near/(far-near), -1,
- 0, 0, (far*near)/(far - near), 0
- );
}
-osg::Matrix getReversedZProjectionMatrixAsOrtho(double left, double right, double bottom, double top, double near, double far)
+void OperationSequence::operator()(osg::Object* object)
{
- return osg::Matrix(
- 2/(right-left), 0, 0, 0,
- 0, 2/(top-bottom), 0, 0,
- 0, 0, 1/(far-near), 0,
- (right+left)/(left-right), (top+bottom)/(bottom-top), far/(far-near), 1
- );
+ mOperationQueue->runOperations(object);
}
-bool isFloatingPointDepthFormat(GLenum format)
+void OperationSequence::add(osg::Operation* operation)
{
- constexpr std::array<GLenum, 4> formats = {
- GL_DEPTH_COMPONENT32F,
- GL_DEPTH_COMPONENT32F_NV,
- GL_DEPTH32F_STENCIL8,
- GL_DEPTH32F_STENCIL8_NV,
- };
-
- return std::find(formats.cbegin(), formats.cend(), format) != formats.cend();
+ mOperationQueue->add(operation);
}
}
diff --git a/components/sceneutil/util.hpp b/components/sceneutil/util.hpp
index 4d19714d66..89d3a12e97 100644
--- a/components/sceneutil/util.hpp
+++ b/components/sceneutil/util.hpp
@@ -6,7 +6,6 @@
#include <osg/Camera>
#include <osg/Texture2D>
#include <osg/Vec4f>
-#include <osg/Depth>
#include <components/resource/resourcesystem.hpp>
@@ -65,30 +64,17 @@ namespace SceneUtil
// Alpha-to-coverage requires a multisampled framebuffer, so we need to set that up for RTTs
bool attachAlphaToCoverageFriendlyFramebufferToCamera(osg::Camera* camera, osg::Camera::BufferComponent buffer, osg::Texture* texture, unsigned int level = 0, unsigned int face = 0, bool mipMapGeneration = false);
- void attachAlphaToCoverageFriendlyDepthColor(osg::Camera* camera, osg::Texture2D* colorTex, osg::Texture2D* depthTex, GLenum depthFormat);
-
- bool getReverseZ();
-
- void setCameraClearDepth(osg::Camera* camera);
-
- // Returns a suitable depth state attribute dependent on whether a reverse-z
- // depth buffer is in use.
- osg::ref_ptr<osg::Depth> createDepth();
-
- // Returns a perspective projection matrix for use with a reversed z-buffer
- // and an infinite far plane. This is derived by mapping the default z-range
- // of [0,1] to [1,0], then taking the limit as far plane approaches
- // infinity.
- osg::Matrix getReversedZProjectionMatrixAsPerspectiveInf(double fov, double aspect, double near);
-
- // Returns a perspective projection matrix for use with a reversed z-buffer.
- osg::Matrix getReversedZProjectionMatrixAsPerspective(double fov, double aspect, double near, double far);
+ class OperationSequence : public osg::Operation
+ {
+ public:
+ OperationSequence(bool keep);
- // Returns an orthographic projection matrix for use with a reversed z-buffer.
- osg::Matrix getReversedZProjectionMatrixAsOrtho(double left, double right, double bottom, double top, double near, double far);
+ void operator()(osg::Object* object) override;
- // Returns true if the GL format is a floating point depth format
- bool isFloatingPointDepthFormat(GLenum format);
+ void add(osg::Operation* operation);
+ protected:
+ osg::ref_ptr<osg::OperationQueue> mOperationQueue;
+ };
}
#endif
diff --git a/components/sceneutil/waterutil.cpp b/components/sceneutil/waterutil.cpp
index ac171005ae..e22070f40f 100644
--- a/components/sceneutil/waterutil.cpp
+++ b/components/sceneutil/waterutil.cpp
@@ -5,7 +5,7 @@
#include <osg/Material>
#include <osg/StateSet>
-#include "util.hpp"
+#include "depth.hpp"
namespace SceneUtil
{
@@ -78,7 +78,7 @@ namespace SceneUtil
stateset->setMode(GL_BLEND, osg::StateAttribute::ON);
stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
- auto depth = createDepth();
+ osg::ref_ptr<osg::Depth> depth = new SceneUtil::AutoDepth;
depth->setWriteMask(false);
stateset->setAttributeAndModes(depth, osg::StateAttribute::ON);
diff --git a/components/sdlutil/sdlmappings.cpp b/components/sdlutil/sdlmappings.cpp
new file mode 100644
index 0000000000..8306909ee5
--- /dev/null
+++ b/components/sdlutil/sdlmappings.cpp
@@ -0,0 +1,251 @@
+#include "sdlmappings.hpp"
+
+#include <map>
+
+#include <MyGUI_MouseButton.h>
+
+#include <SDL_gamecontroller.h>
+#include <SDL_mouse.h>
+
+namespace SDLUtil
+{
+ std::string sdlControllerButtonToString(int button)
+ {
+ switch(button)
+ {
+ case SDL_CONTROLLER_BUTTON_A:
+ return "A Button";
+ case SDL_CONTROLLER_BUTTON_B:
+ return "B Button";
+ case SDL_CONTROLLER_BUTTON_BACK:
+ return "Back Button";
+ case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
+ return "DPad Down";
+ case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
+ return "DPad Left";
+ case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
+ return "DPad Right";
+ case SDL_CONTROLLER_BUTTON_DPAD_UP:
+ return "DPad Up";
+ case SDL_CONTROLLER_BUTTON_GUIDE:
+ return "Guide Button";
+ case SDL_CONTROLLER_BUTTON_LEFTSHOULDER:
+ return "Left Shoulder";
+ case SDL_CONTROLLER_BUTTON_LEFTSTICK:
+ return "Left Stick Button";
+ case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER:
+ return "Right Shoulder";
+ case SDL_CONTROLLER_BUTTON_RIGHTSTICK:
+ return "Right Stick Button";
+ case SDL_CONTROLLER_BUTTON_START:
+ return "Start Button";
+ case SDL_CONTROLLER_BUTTON_X:
+ return "X Button";
+ case SDL_CONTROLLER_BUTTON_Y:
+ return "Y Button";
+ default:
+ return "Button " + std::to_string(button);
+ }
+ }
+
+ std::string sdlControllerAxisToString(int axis)
+ {
+ switch(axis)
+ {
+ case SDL_CONTROLLER_AXIS_LEFTX:
+ return "Left Stick X";
+ case SDL_CONTROLLER_AXIS_LEFTY:
+ return "Left Stick Y";
+ case SDL_CONTROLLER_AXIS_RIGHTX:
+ return "Right Stick X";
+ case SDL_CONTROLLER_AXIS_RIGHTY:
+ return "Right Stick Y";
+ case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
+ return "Left Trigger";
+ case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
+ return "Right Trigger";
+ default:
+ return "Axis " + std::to_string(axis);
+ }
+ }
+
+ MyGUI::MouseButton sdlMouseButtonToMyGui(Uint8 button)
+ {
+ //The right button is the second button, according to MyGUI
+ if(button == SDL_BUTTON_RIGHT)
+ button = SDL_BUTTON_MIDDLE;
+ else if(button == SDL_BUTTON_MIDDLE)
+ button = SDL_BUTTON_RIGHT;
+
+ //MyGUI's buttons are 0 indexed
+ return MyGUI::MouseButton::Enum(button - 1);
+ }
+
+ Uint8 myGuiMouseButtonToSdl(MyGUI::MouseButton button)
+ {
+ Uint8 value = button.getValue() + 1;
+ if (value == SDL_BUTTON_RIGHT)
+ value = SDL_BUTTON_MIDDLE;
+ else if (value == SDL_BUTTON_MIDDLE)
+ value = SDL_BUTTON_RIGHT;
+ return value;
+ }
+
+ namespace
+ {
+ std::map<SDL_Keycode, MyGUI::KeyCode> initKeyMap()
+ {
+ std::map<SDL_Keycode, MyGUI::KeyCode> keyMap;
+ keyMap[SDLK_UNKNOWN] = MyGUI::KeyCode::None;
+ keyMap[SDLK_ESCAPE] = MyGUI::KeyCode::Escape;
+ keyMap[SDLK_1] = MyGUI::KeyCode::One;
+ keyMap[SDLK_2] = MyGUI::KeyCode::Two;
+ keyMap[SDLK_3] = MyGUI::KeyCode::Three;
+ keyMap[SDLK_4] = MyGUI::KeyCode::Four;
+ keyMap[SDLK_5] = MyGUI::KeyCode::Five;
+ keyMap[SDLK_6] = MyGUI::KeyCode::Six;
+ keyMap[SDLK_7] = MyGUI::KeyCode::Seven;
+ keyMap[SDLK_8] = MyGUI::KeyCode::Eight;
+ keyMap[SDLK_9] = MyGUI::KeyCode::Nine;
+ keyMap[SDLK_0] = MyGUI::KeyCode::Zero;
+ keyMap[SDLK_MINUS] = MyGUI::KeyCode::Minus;
+ keyMap[SDLK_EQUALS] = MyGUI::KeyCode::Equals;
+ keyMap[SDLK_BACKSPACE] = MyGUI::KeyCode::Backspace;
+ keyMap[SDLK_TAB] = MyGUI::KeyCode::Tab;
+ keyMap[SDLK_q] = MyGUI::KeyCode::Q;
+ keyMap[SDLK_w] = MyGUI::KeyCode::W;
+ keyMap[SDLK_e] = MyGUI::KeyCode::E;
+ keyMap[SDLK_r] = MyGUI::KeyCode::R;
+ keyMap[SDLK_t] = MyGUI::KeyCode::T;
+ keyMap[SDLK_y] = MyGUI::KeyCode::Y;
+ keyMap[SDLK_u] = MyGUI::KeyCode::U;
+ keyMap[SDLK_i] = MyGUI::KeyCode::I;
+ keyMap[SDLK_o] = MyGUI::KeyCode::O;
+ keyMap[SDLK_p] = MyGUI::KeyCode::P;
+ keyMap[SDLK_RETURN] = MyGUI::KeyCode::Return;
+ keyMap[SDLK_a] = MyGUI::KeyCode::A;
+ keyMap[SDLK_s] = MyGUI::KeyCode::S;
+ keyMap[SDLK_d] = MyGUI::KeyCode::D;
+ keyMap[SDLK_f] = MyGUI::KeyCode::F;
+ keyMap[SDLK_g] = MyGUI::KeyCode::G;
+ keyMap[SDLK_h] = MyGUI::KeyCode::H;
+ keyMap[SDLK_j] = MyGUI::KeyCode::J;
+ keyMap[SDLK_k] = MyGUI::KeyCode::K;
+ keyMap[SDLK_l] = MyGUI::KeyCode::L;
+ keyMap[SDLK_SEMICOLON] = MyGUI::KeyCode::Semicolon;
+ keyMap[SDLK_QUOTE] = MyGUI::KeyCode::Apostrophe;
+ keyMap[SDLK_BACKQUOTE] = MyGUI::KeyCode::Grave;
+ keyMap[SDLK_LSHIFT] = MyGUI::KeyCode::LeftShift;
+ keyMap[SDLK_BACKSLASH] = MyGUI::KeyCode::Backslash;
+ keyMap[SDLK_z] = MyGUI::KeyCode::Z;
+ keyMap[SDLK_x] = MyGUI::KeyCode::X;
+ keyMap[SDLK_c] = MyGUI::KeyCode::C;
+ keyMap[SDLK_v] = MyGUI::KeyCode::V;
+ keyMap[SDLK_b] = MyGUI::KeyCode::B;
+ keyMap[SDLK_n] = MyGUI::KeyCode::N;
+ keyMap[SDLK_m] = MyGUI::KeyCode::M;
+ keyMap[SDLK_COMMA] = MyGUI::KeyCode::Comma;
+ keyMap[SDLK_PERIOD] = MyGUI::KeyCode::Period;
+ keyMap[SDLK_SLASH] = MyGUI::KeyCode::Slash;
+ keyMap[SDLK_RSHIFT] = MyGUI::KeyCode::RightShift;
+ keyMap[SDLK_KP_MULTIPLY] = MyGUI::KeyCode::Multiply;
+ keyMap[SDLK_LALT] = MyGUI::KeyCode::LeftAlt;
+ keyMap[SDLK_SPACE] = MyGUI::KeyCode::Space;
+ keyMap[SDLK_CAPSLOCK] = MyGUI::KeyCode::Capital;
+ keyMap[SDLK_F1] = MyGUI::KeyCode::F1;
+ keyMap[SDLK_F2] = MyGUI::KeyCode::F2;
+ keyMap[SDLK_F3] = MyGUI::KeyCode::F3;
+ keyMap[SDLK_F4] = MyGUI::KeyCode::F4;
+ keyMap[SDLK_F5] = MyGUI::KeyCode::F5;
+ keyMap[SDLK_F6] = MyGUI::KeyCode::F6;
+ keyMap[SDLK_F7] = MyGUI::KeyCode::F7;
+ keyMap[SDLK_F8] = MyGUI::KeyCode::F8;
+ keyMap[SDLK_F9] = MyGUI::KeyCode::F9;
+ keyMap[SDLK_F10] = MyGUI::KeyCode::F10;
+ keyMap[SDLK_NUMLOCKCLEAR] = MyGUI::KeyCode::NumLock;
+ keyMap[SDLK_SCROLLLOCK] = MyGUI::KeyCode::ScrollLock;
+ keyMap[SDLK_KP_7] = MyGUI::KeyCode::Numpad7;
+ keyMap[SDLK_KP_8] = MyGUI::KeyCode::Numpad8;
+ keyMap[SDLK_KP_9] = MyGUI::KeyCode::Numpad9;
+ keyMap[SDLK_KP_MINUS] = MyGUI::KeyCode::Subtract;
+ keyMap[SDLK_KP_4] = MyGUI::KeyCode::Numpad4;
+ keyMap[SDLK_KP_5] = MyGUI::KeyCode::Numpad5;
+ keyMap[SDLK_KP_6] = MyGUI::KeyCode::Numpad6;
+ keyMap[SDLK_KP_PLUS] = MyGUI::KeyCode::Add;
+ keyMap[SDLK_KP_1] = MyGUI::KeyCode::Numpad1;
+ keyMap[SDLK_KP_2] = MyGUI::KeyCode::Numpad2;
+ keyMap[SDLK_KP_3] = MyGUI::KeyCode::Numpad3;
+ keyMap[SDLK_KP_0] = MyGUI::KeyCode::Numpad0;
+ keyMap[SDLK_KP_PERIOD] = MyGUI::KeyCode::Decimal;
+ keyMap[SDLK_F11] = MyGUI::KeyCode::F11;
+ keyMap[SDLK_F12] = MyGUI::KeyCode::F12;
+ keyMap[SDLK_F13] = MyGUI::KeyCode::F13;
+ keyMap[SDLK_F14] = MyGUI::KeyCode::F14;
+ keyMap[SDLK_F15] = MyGUI::KeyCode::F15;
+ keyMap[SDLK_KP_EQUALS] = MyGUI::KeyCode::NumpadEquals;
+ keyMap[SDLK_COLON] = MyGUI::KeyCode::Colon;
+ keyMap[SDLK_KP_ENTER] = MyGUI::KeyCode::NumpadEnter;
+ keyMap[SDLK_KP_DIVIDE] = MyGUI::KeyCode::Divide;
+ keyMap[SDLK_SYSREQ] = MyGUI::KeyCode::SysRq;
+ keyMap[SDLK_RALT] = MyGUI::KeyCode::RightAlt;
+ keyMap[SDLK_HOME] = MyGUI::KeyCode::Home;
+ keyMap[SDLK_UP] = MyGUI::KeyCode::ArrowUp;
+ keyMap[SDLK_PAGEUP] = MyGUI::KeyCode::PageUp;
+ keyMap[SDLK_LEFT] = MyGUI::KeyCode::ArrowLeft;
+ keyMap[SDLK_RIGHT] = MyGUI::KeyCode::ArrowRight;
+ keyMap[SDLK_END] = MyGUI::KeyCode::End;
+ keyMap[SDLK_DOWN] = MyGUI::KeyCode::ArrowDown;
+ keyMap[SDLK_PAGEDOWN] = MyGUI::KeyCode::PageDown;
+ keyMap[SDLK_INSERT] = MyGUI::KeyCode::Insert;
+ keyMap[SDLK_DELETE] = MyGUI::KeyCode::Delete;
+ keyMap[SDLK_APPLICATION] = MyGUI::KeyCode::AppMenu;
+
+ //The function of the Ctrl and Meta keys are switched on macOS compared to other platforms.
+ //For instance] = Cmd+C versus Ctrl+C to copy from the system clipboard
+ #if defined(__APPLE__)
+ keyMap[SDLK_LGUI] = MyGUI::KeyCode::LeftControl;
+ keyMap[SDLK_RGUI] = MyGUI::KeyCode::RightControl;
+ keyMap[SDLK_LCTRL] = MyGUI::KeyCode::LeftWindows;
+ keyMap[SDLK_RCTRL] = MyGUI::KeyCode::RightWindows;
+ #else
+ keyMap[SDLK_LGUI] = MyGUI::KeyCode::LeftWindows;
+ keyMap[SDLK_RGUI] = MyGUI::KeyCode::RightWindows;
+ keyMap[SDLK_LCTRL] = MyGUI::KeyCode::LeftControl;
+ keyMap[SDLK_RCTRL] = MyGUI::KeyCode::RightControl;
+ #endif
+ return keyMap;
+ }
+
+ std::map<MyGUI::KeyCode, SDL_Keycode> reverseKeyMap(const std::map<SDL_Keycode, MyGUI::KeyCode>& map)
+ {
+ std::map<MyGUI::KeyCode, SDL_Keycode> result;
+ for (auto [sdl, mygui] : map)
+ result[mygui] = sdl;
+ return result;
+ }
+ }
+
+ MyGUI::KeyCode sdlKeyToMyGUI(SDL_Keycode code)
+ {
+ static std::map<SDL_Keycode, MyGUI::KeyCode> keyMap = initKeyMap();
+
+ MyGUI::KeyCode kc = MyGUI::KeyCode::None;
+ auto foundKey = keyMap.find(code);
+ if (foundKey != keyMap.end())
+ kc = foundKey->second;
+
+ return kc;
+ }
+
+ SDL_Keycode myGuiKeyToSdl(MyGUI::KeyCode button)
+ {
+ static auto keyMap = reverseKeyMap(initKeyMap());
+
+ SDL_Keycode kc = 0;
+ auto foundKey = keyMap.find(button);
+ if (foundKey != keyMap.end())
+ kc = foundKey->second;
+
+ return kc;
+ }
+}
diff --git a/apps/openmw/mwinput/sdlmappings.hpp b/components/sdlutil/sdlmappings.hpp
index 0cdd4694f5..3625075009 100644
--- a/apps/openmw/mwinput/sdlmappings.hpp
+++ b/components/sdlutil/sdlmappings.hpp
@@ -1,5 +1,5 @@
-#ifndef MWINPUT_SDLMAPPINGS_H
-#define MWINPUT_SDLMAPPINGS_H
+#ifndef SDLUTIL_SDLMAPPINGS
+#define SDLUTIL_SDLMAPPINGS
#include <string>
@@ -12,14 +12,16 @@ namespace MyGUI
struct MouseButton;
}
-namespace MWInput
+namespace SDLUtil
{
std::string sdlControllerButtonToString(int button);
std::string sdlControllerAxisToString(int axis);
- MyGUI::MouseButton sdlButtonToMyGUI(Uint8 button);
+ MyGUI::MouseButton sdlMouseButtonToMyGui(Uint8 button);
+ Uint8 myGuiMouseButtonToSdl(MyGUI::MouseButton button);
MyGUI::KeyCode sdlKeyToMyGUI(SDL_Keycode code);
+ SDL_Keycode myGuiKeyToSdl(MyGUI::KeyCode button);
}
-#endif
+#endif // !SDLUTIL_SDLMAPPINGS
diff --git a/components/detournavigator/serialization/binaryreader.hpp b/components/serialization/binaryreader.hpp
index 0d75c3ac99..66e09f6ffb 100644
--- a/components/detournavigator/serialization/binaryreader.hpp
+++ b/components/serialization/binaryreader.hpp
@@ -1,13 +1,16 @@
-#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYREADER_H
-#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYREADER_H
+#ifndef OPENMW_COMPONENTS_SERIALIZATION_BINARYREADER_H
+#define OPENMW_COMPONENTS_SERIALIZATION_BINARYREADER_H
+#include <components/misc/endianness.hpp>
+
+#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstring>
#include <stdexcept>
#include <type_traits>
-namespace DetourNavigator::Serialization
+namespace Serialization
{
class BinaryReader
{
@@ -23,12 +26,15 @@ namespace DetourNavigator::Serialization
template <class Format, class T>
void operator()(Format&& format, T& value)
{
- if constexpr (std::is_arithmetic_v<T>)
+ if constexpr (std::is_enum_v<T>)
+ (*this)(std::forward<Format>(format), static_cast<std::underlying_type_t<T>&>(value));
+ else if constexpr (std::is_arithmetic_v<T>)
{
- if (mEnd - mPos < static_cast<std::ptrdiff_t>(sizeof(value)))
+ if (mEnd - mPos < static_cast<std::ptrdiff_t>(sizeof(T)))
throw std::runtime_error("Not enough data");
- std::memcpy(&value, mPos, sizeof(value));
- mPos += sizeof(value);
+ std::memcpy(&value, mPos, sizeof(T));
+ mPos += sizeof(T);
+ value = Misc::toLittleEndian(value);
}
else
{
@@ -39,13 +45,17 @@ namespace DetourNavigator::Serialization
template <class Format, class T>
auto operator()(Format&& format, T* data, std::size_t count)
{
- if constexpr (std::is_arithmetic_v<T>)
+ if constexpr (std::is_enum_v<T>)
+ (*this)(std::forward<Format>(format), reinterpret_cast<std::underlying_type_t<T>*>(data), count);
+ else if constexpr (std::is_arithmetic_v<T>)
{
- if (mEnd - mPos < static_cast<std::ptrdiff_t>(count * sizeof(T)))
- throw std::runtime_error("Not enough data");
const std::size_t size = sizeof(T) * count;
+ if (mEnd - mPos < static_cast<std::ptrdiff_t>(size))
+ throw std::runtime_error("Not enough data");
std::memcpy(data, mPos, size);
mPos += size;
+ if constexpr (!Misc::IS_LITTLE_ENDIAN)
+ std::for_each(data, data + count, [&] (T& v) { v = Misc::fromLittleEndian(v); });
}
else
{
diff --git a/components/detournavigator/serialization/binarywriter.hpp b/components/serialization/binarywriter.hpp
index 5e710d85d5..199f208da9 100644
--- a/components/detournavigator/serialization/binarywriter.hpp
+++ b/components/serialization/binarywriter.hpp
@@ -1,13 +1,16 @@
-#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYWRITER_H
-#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYWRITER_H
+#ifndef OPENMW_COMPONENTS_SERIALIZATION_BINARYWRITER_H
+#define OPENMW_COMPONENTS_SERIALIZATION_BINARYWRITER_H
+#include <components/misc/endianness.hpp>
+
+#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstring>
#include <stdexcept>
#include <type_traits>
-namespace DetourNavigator::Serialization
+namespace Serialization
{
struct BinaryWriter
{
@@ -23,12 +26,13 @@ namespace DetourNavigator::Serialization
template <class Format, class T>
void operator()(Format&& format, const T& value)
{
- if constexpr (std::is_arithmetic_v<T>)
+ if constexpr (std::is_enum_v<T>)
+ (*this)(std::forward<Format>(format), static_cast<std::underlying_type_t<T>>(value));
+ else if constexpr (std::is_arithmetic_v<T>)
{
- if (mEnd - mDest < static_cast<std::ptrdiff_t>(sizeof(value)))
+ if (mEnd - mDest < static_cast<std::ptrdiff_t>(sizeof(T)))
throw std::runtime_error("Not enough space");
- std::memcpy(mDest, &value, sizeof(value));
- mDest += sizeof(value);
+ writeValue(value);
}
else
{
@@ -39,13 +43,20 @@ namespace DetourNavigator::Serialization
template <class Format, class T>
auto operator()(Format&& format, const T* data, std::size_t count)
{
- if constexpr (std::is_arithmetic_v<T>)
+ if constexpr (std::is_enum_v<T>)
+ (*this)(std::forward<Format>(format), reinterpret_cast<const std::underlying_type_t<T>*>(data), count);
+ else if constexpr (std::is_arithmetic_v<T>)
{
const std::size_t size = sizeof(T) * count;
if (mEnd - mDest < static_cast<std::ptrdiff_t>(size))
throw std::runtime_error("Not enough space");
- std::memcpy(mDest, data, size);
- mDest += size;
+ if constexpr (Misc::IS_LITTLE_ENDIAN)
+ {
+ std::memcpy(mDest, data, size);
+ mDest += size;
+ }
+ else
+ std::for_each(data, data + count, [&] (const T& v) { writeValue(v); });
}
else
{
@@ -56,6 +67,14 @@ namespace DetourNavigator::Serialization
private:
std::byte* mDest;
const std::byte* const mEnd;
+
+ template <class T>
+ void writeValue(const T& value) noexcept
+ {
+ T coverted = Misc::toLittleEndian(value);
+ std::memcpy(mDest, &coverted, sizeof(T));
+ mDest += sizeof(T);
+ }
};
}
diff --git a/components/detournavigator/serialization/format.hpp b/components/serialization/format.hpp
index d07ab9da6f..595afd0dad 100644
--- a/components/detournavigator/serialization/format.hpp
+++ b/components/serialization/format.hpp
@@ -1,5 +1,5 @@
-#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H
-#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H
+#ifndef OPENMW_COMPONENTS_SERIALIZATION_FORMAT_H
+#define OPENMW_COMPONENTS_SERIALIZATION_FORMAT_H
#include <algorithm>
#include <cstddef>
@@ -8,7 +8,7 @@
#include <utility>
#include <vector>
-namespace DetourNavigator::Serialization
+namespace Serialization
{
enum class Mode
{
@@ -22,6 +22,9 @@ namespace DetourNavigator::Serialization
template <class ... Args>
struct IsContiguousContainer<std::vector<Args ...>> : std::true_type {};
+ template <class T, std::size_t n>
+ struct IsContiguousContainer<std::array<T, n>> : std::true_type {};
+
template <class T>
constexpr bool isContiguousContainer = IsContiguousContainer<std::decay_t<T>>::value;
@@ -31,24 +34,10 @@ namespace DetourNavigator::Serialization
template <class Visitor, class T>
void operator()(Visitor&& visitor, T* data, std::size_t size) const
{
- if constexpr (std::is_arithmetic_v<T>)
- {
+ if constexpr (std::is_arithmetic_v<T> || std::is_enum_v<T>)
visitor(self(), data, size);
- }
- else if constexpr (std::is_enum_v<T>)
- {
- if constexpr (mode == Mode::Write)
- visitor(self(), reinterpret_cast<const std::underlying_type_t<T>*>(data), size);
- else
- {
- static_assert(mode == Mode::Read);
- visitor(self(), reinterpret_cast<std::underlying_type_t<T>*>(data), size);
- }
- }
else
- {
std::for_each(data, data + size, [&] (auto& v) { visitor(self(), v); });
- }
}
template <class Visitor, class T, std::size_t size>
diff --git a/components/detournavigator/serialization/sizeaccumulator.hpp b/components/serialization/sizeaccumulator.hpp
index 28bdb5c1cb..4dcc004f73 100644
--- a/components/detournavigator/serialization/sizeaccumulator.hpp
+++ b/components/serialization/sizeaccumulator.hpp
@@ -1,10 +1,10 @@
-#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_SIZEACCUMULATOR_H
-#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_SIZEACCUMULATOR_H
+#ifndef OPENMW_COMPONENTS_SERIALIZATION_SIZEACCUMULATOR_H
+#define OPENMW_COMPONENTS_SERIALIZATION_SIZEACCUMULATOR_H
#include <cstddef>
#include <type_traits>
-namespace DetourNavigator::Serialization
+namespace Serialization
{
class SizeAccumulator
{
@@ -18,7 +18,7 @@ namespace DetourNavigator::Serialization
template <class Format, class T>
void operator()(Format&& format, const T& value)
{
- if constexpr (std::is_arithmetic_v<T>)
+ if constexpr (std::is_arithmetic_v<T> || std::is_enum_v<T>)
mValue += sizeof(T);
else
format(*this, value);
@@ -27,7 +27,7 @@ namespace DetourNavigator::Serialization
template <class Format, class T>
auto operator()(Format&& format, const T* data, std::size_t count)
{
- if constexpr (std::is_arithmetic_v<T>)
+ if constexpr (std::is_arithmetic_v<T> || std::is_enum_v<T>)
mValue += count * sizeof(T);
else
format(*this, data, count);
diff --git a/components/settings/categories.hpp b/components/settings/categories.hpp
index d6cd042f61..6a2da2fa10 100644
--- a/components/settings/categories.hpp
+++ b/components/settings/categories.hpp
@@ -9,7 +9,7 @@
namespace Settings
{
using CategorySetting = std::pair<std::string, std::string>;
- using CategorySettingVector = std::set<std::pair<std::string, std::string>>;
+ using CategorySettingVector = std::set<CategorySetting>;
using CategorySettingValueMap = std::map<CategorySetting, std::string>;
}
diff --git a/components/settings/settings.cpp b/components/settings/settings.cpp
index 09a3d1f516..7fa625e4ab 100644
--- a/components/settings/settings.cpp
+++ b/components/settings/settings.cpp
@@ -39,7 +39,7 @@ void Manager::saveUser(const std::string &file)
std::string Manager::getString(const std::string &setting, const std::string &category)
{
- CategorySettingValueMap::key_type key = std::make_pair(category, setting);
+ CategorySettingValueMap::key_type key (category, setting);
CategorySettingValueMap::iterator it = mUserSettings.find(key);
if (it != mUserSettings.end())
return it->second;
@@ -79,6 +79,15 @@ int Manager::getInt (const std::string& setting, const std::string& category)
return number;
}
+std::int64_t Manager::getInt64 (const std::string& setting, const std::string& category)
+{
+ const std::string& value = getString(setting, category);
+ std::stringstream stream(value);
+ std::size_t number = 0;
+ stream >> number;
+ return number;
+}
+
bool Manager::getBool (const std::string& setting, const std::string& category)
{
const std::string& string = getString(setting, category);
@@ -93,7 +102,7 @@ osg::Vec2f Manager::getVector2 (const std::string& setting, const std::string& c
stream >> x >> y;
if (stream.fail())
throw std::runtime_error(std::string("Can't parse 2d vector: " + value));
- return osg::Vec2f(x, y);
+ return {x, y};
}
osg::Vec3f Manager::getVector3 (const std::string& setting, const std::string& category)
@@ -104,14 +113,14 @@ osg::Vec3f Manager::getVector3 (const std::string& setting, const std::string& c
stream >> x >> y >> z;
if (stream.fail())
throw std::runtime_error(std::string("Can't parse 3d vector: " + value));
- return osg::Vec3f(x, y, z);
+ return {x, y, z};
}
void Manager::setString(const std::string &setting, const std::string &category, const std::string &value)
{
- CategorySettingValueMap::key_type key = std::make_pair(category, setting);
+ CategorySettingValueMap::key_type key (category, setting);
- CategorySettingValueMap::iterator found = mUserSettings.find(key);
+ auto found = mUserSettings.find(key);
if (found != mUserSettings.end())
{
if (found->second == value)
@@ -165,18 +174,35 @@ void Manager::setVector3 (const std::string &setting, const std::string &categor
void Manager::resetPendingChange(const std::string &setting, const std::string &category)
{
- CategorySettingValueMap::key_type key = std::make_pair(category, setting);
+ CategorySettingValueMap::key_type key (category, setting);
mChangedSettings.erase(key);
}
-const CategorySettingVector Manager::getPendingChanges()
+CategorySettingVector Manager::getPendingChanges()
{
return mChangedSettings;
}
+CategorySettingVector Manager::getPendingChanges(const CategorySettingVector& filter)
+{
+ CategorySettingVector intersection;
+ std::set_intersection(mChangedSettings.begin(), mChangedSettings.end(),
+ filter.begin(), filter.end(),
+ std::inserter(intersection, intersection.begin()));
+ return intersection;
+}
+
void Manager::resetPendingChanges()
{
mChangedSettings.clear();
}
+void Manager::resetPendingChanges(const CategorySettingVector& filter)
+{
+ for (const auto& key : filter)
+ {
+ mChangedSettings.erase(key);
+ }
+}
+
}
diff --git a/components/settings/settings.hpp b/components/settings/settings.hpp
index e3a29d4c34..a4b1cf3a54 100644
--- a/components/settings/settings.hpp
+++ b/components/settings/settings.hpp
@@ -36,13 +36,22 @@ namespace Settings
///< save user settings to file
static void resetPendingChange(const std::string &setting, const std::string &category);
+ ///< resets a single pending change
static void resetPendingChanges();
+ ///< resets the list of all pending changes
- static const CategorySettingVector getPendingChanges();
- ///< returns the list of changed settings and then clears it
+ static void resetPendingChanges(const CategorySettingVector& filter);
+ ///< resets only the pending changes listed in the filter
+
+ static CategorySettingVector getPendingChanges();
+ ///< returns the list of changed settings
+
+ static CategorySettingVector getPendingChanges(const CategorySettingVector& filter);
+ ///< returns the list of changed settings intersecting with the filter
static int getInt (const std::string& setting, const std::string& category);
+ static std::int64_t getInt64 (const std::string& setting, const std::string& category);
static float getFloat (const std::string& setting, const std::string& category);
static double getDouble (const std::string& setting, const std::string& category);
static std::string getString (const std::string& setting, const std::string& category);
@@ -50,15 +59,15 @@ namespace Settings
static osg::Vec2f getVector2 (const std::string& setting, const std::string& category);
static osg::Vec3f getVector3 (const std::string& setting, const std::string& category);
- static void setInt (const std::string& setting, const std::string& category, const int value);
- static void setFloat (const std::string& setting, const std::string& category, const float value);
- static void setDouble (const std::string& setting, const std::string& category, const double value);
+ static void setInt (const std::string& setting, const std::string& category, int value);
+ static void setFloat (const std::string& setting, const std::string& category, float value);
+ static void setDouble (const std::string& setting, const std::string& category, double value);
static void setString (const std::string& setting, const std::string& category, const std::string& value);
- static void setBool (const std::string& setting, const std::string& category, const bool value);
- static void setVector2 (const std::string& setting, const std::string& category, const osg::Vec2f value);
- static void setVector3 (const std::string& setting, const std::string& category, const osg::Vec3f value);
+ static void setBool (const std::string& setting, const std::string& category, bool value);
+ static void setVector2 (const std::string& setting, const std::string& category, osg::Vec2f value);
+ static void setVector3 (const std::string& setting, const std::string& category, osg::Vec3f value);
};
}
-#endif // _COMPONENTS_SETTINGS_H
+#endif // COMPONENTS_SETTINGS_H
diff --git a/components/shader/shadermanager.cpp b/components/shader/shadermanager.cpp
index e057cfac02..30c7ed1b1d 100644
--- a/components/shader/shadermanager.cpp
+++ b/components/shader/shadermanager.cpp
@@ -345,7 +345,7 @@ namespace Shader
if (found == mPrograms.end())
{
if (!programTemplate) programTemplate = mProgramTemplate;
- osg::ref_ptr<osg::Program> program = programTemplate ? static_cast<osg::Program*>(programTemplate->clone(osg::CopyOp::SHALLOW_COPY)) : new osg::Program;
+ osg::ref_ptr<osg::Program> program = programTemplate ? cloneProgram(programTemplate) : osg::ref_ptr<osg::Program>(new osg::Program);
program->addShader(vertexShader);
program->addShader(fragmentShader);
found = mPrograms.insert(std::make_pair(std::make_pair(vertexShader, fragmentShader), program)).first;
@@ -353,6 +353,14 @@ namespace Shader
return found->second;
}
+ osg::ref_ptr<osg::Program> ShaderManager::cloneProgram(const osg::Program* src)
+ {
+ osg::ref_ptr<osg::Program> program = static_cast<osg::Program*>(src->clone(osg::CopyOp::SHALLOW_COPY));
+ for (auto [name, idx] : src->getUniformBlockBindingList())
+ program->addBindUniformBlock(name, idx);
+ return program;
+ }
+
ShaderManager::DefineMap ShaderManager::getGlobalDefines()
{
return DefineMap(mGlobalDefines);
diff --git a/components/shader/shadermanager.hpp b/components/shader/shadermanager.hpp
index d0ee069b10..613f33b168 100644
--- a/components/shader/shadermanager.hpp
+++ b/components/shader/shadermanager.hpp
@@ -38,6 +38,9 @@ namespace Shader
const osg::Program* getProgramTemplate() const { return mProgramTemplate; }
void setProgramTemplate(const osg::Program* program) { mProgramTemplate = program; }
+ /// Clone an osg::Program including bindUniformBlocks that osg::Program::clone does not copy for some reason.
+ static osg::ref_ptr<osg::Program> cloneProgram(const osg::Program*);
+
/// Get (a copy of) the DefineMap used to construct all shaders
DefineMap getGlobalDefines();
diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp
index 9877ab1863..107665369c 100644
--- a/components/shader/shadervisitor.cpp
+++ b/components/shader/shadervisitor.cpp
@@ -1,6 +1,7 @@
#include "shadervisitor.hpp"
#include <unordered_set>
+#include <unordered_map>
#include <set>
#include <osg/AlphaFunc>
@@ -11,6 +12,8 @@
#include <osg/Texture>
#include <osg/ValueObject>
+#include <osgParticle/ParticleSystem>
+
#include <osgUtil/TangentSpaceGenerator>
#include <components/debug/debuglog.hpp>
@@ -19,12 +22,22 @@
#include <components/vfs/manager.hpp>
#include <components/sceneutil/riggeometry.hpp>
#include <components/sceneutil/morphgeometry.hpp>
+#include <components/sceneutil/depth.hpp>
#include "removedalphafunc.hpp"
#include "shadermanager.hpp"
namespace Shader
{
+ /**
+ * Miniature version of osg::StateSet used to track state added by the shader visitor which should be ignored when
+ * it's applied a second time, and removed when shaders are removed.
+ * Actual StateAttributes aren't kept as they're recoverable from the StateSet this is attached to - we just want
+ * the TypeMemberPair as that uniquely identifies which of those StateAttributes it was we're tracking.
+ * Not all StateSet features have been added yet - we implement an equivalently-named method to each of the StateSet
+ * methods called in createProgram, and implement new ones as they're needed.
+ * When expanding tracking to cover new things, ensure they're accounted for in ensureFFP.
+ */
class AddedState : public osg::Object
{
public:
@@ -34,6 +47,7 @@ namespace Shader
, mUniforms(rhs.mUniforms)
, mModes(rhs.mModes)
, mAttributes(rhs.mAttributes)
+ , mTextureModes(rhs.mTextureModes)
{
}
@@ -57,16 +71,44 @@ namespace Shader
template<typename T>
void setAttributeAndModes(osg::ref_ptr<T> attribute) { setAttributeAndModes(attribute.get()); }
+ void setTextureMode(unsigned int unit, osg::StateAttribute::GLMode mode) { mTextureModes[unit].emplace(mode); }
+ void setTextureAttribute(int unit, osg::StateAttribute::TypeMemberPair typeMemberPair) { mTextureAttributes[unit].emplace(typeMemberPair); }
+
+ void setTextureAttribute(unsigned int unit, const osg::StateAttribute* attribute)
+ {
+ mTextureAttributes[unit].emplace(attribute->getTypeMemberPair());
+ }
+ template<typename T>
+ void setTextureAttribute(unsigned int unit, osg::ref_ptr<T> attribute) { setTextureAttribute(unit, attribute.get()); }
+
+ void setTextureAttributeAndModes(unsigned int unit, const osg::StateAttribute* attribute)
+ {
+ setTextureAttribute(unit, attribute);
+ InterrogateModesHelper helper(this, unit);
+ attribute->getModeUsage(helper);
+ }
+ template<typename T>
+ void setTextureAttributeAndModes(unsigned int unit, osg::ref_ptr<T> attribute) { setTextureAttributeAndModes(unit, attribute.get()); }
+
bool hasUniform(const std::string& name) { return mUniforms.count(name); }
bool hasMode(osg::StateAttribute::GLMode mode) { return mModes.count(mode); }
bool hasAttribute(const osg::StateAttribute::TypeMemberPair &typeMemberPair) { return mAttributes.count(typeMemberPair); }
bool hasAttribute(osg::StateAttribute::Type type, unsigned int member) { return hasAttribute(osg::StateAttribute::TypeMemberPair(type, member)); }
+ bool hasTextureMode(int unit, osg::StateAttribute::GLMode mode)
+ {
+ auto it = mTextureModes.find(unit);
+ if (it == mTextureModes.cend())
+ return false;
+
+ return it->second.count(mode);
+ }
const std::set<osg::StateAttribute::TypeMemberPair>& getAttributes() { return mAttributes; }
+ const std::unordered_map<unsigned int, std::set<osg::StateAttribute::TypeMemberPair>>& getTextureAttributes() { return mTextureAttributes; }
bool empty()
{
- return mUniforms.empty() && mModes.empty() && mAttributes.empty();
+ return mUniforms.empty() && mModes.empty() && mAttributes.empty() && mTextureModes.empty() && mTextureAttributes.empty();
}
META_Object(Shader, AddedState)
@@ -75,17 +117,26 @@ namespace Shader
class InterrogateModesHelper : public osg::StateAttribute::ModeUsage
{
public:
- InterrogateModesHelper(AddedState* tracker) : mTracker(tracker) {}
+ InterrogateModesHelper(AddedState* tracker, unsigned int textureUnit = 0)
+ : mTracker(tracker)
+ , mTextureUnit(textureUnit)
+ {}
void usesMode(osg::StateAttribute::GLMode mode) override { mTracker->setMode(mode); }
- void usesTextureMode(osg::StateAttribute::GLMode mode) override {}
+ void usesTextureMode(osg::StateAttribute::GLMode mode) override { mTracker->setTextureMode(mTextureUnit, mode); }
private:
AddedState* mTracker;
+ unsigned int mTextureUnit;
};
+ using ModeSet = std::unordered_set<osg::StateAttribute::GLMode>;
+ using AttributeSet = std::set<osg::StateAttribute::TypeMemberPair>;
+
std::unordered_set<std::string> mUniforms;
- std::unordered_set<osg::StateAttribute::GLMode> mModes;
- std::set<osg::StateAttribute::TypeMemberPair> mAttributes;
+ ModeSet mModes;
+ AttributeSet mAttributes;
+ std::unordered_map<unsigned int, ModeSet> mTextureModes;
+ std::unordered_map<unsigned int, AttributeSet> mTextureAttributes;
};
ShaderVisitor::ShaderRequirements::ShaderRequirements()
@@ -99,6 +150,8 @@ namespace Shader
, mAlphaBlend(false)
, mNormalHeight(false)
, mTexStageRequiringTangents(-1)
+ , mSoftParticles(false)
+ , mSoftParticleSize(0.f)
, mNode(nullptr)
{
}
@@ -210,6 +263,9 @@ namespace Shader
if (node.getUserValue("shaderRequired", shaderRequired) && shaderRequired)
mRequirements.back().mShaderRequired = true;
+ // Make sure to disregard any state that came from a previous call to createProgram
+ osg::ref_ptr<AddedState> addedState = getAddedState(*stateset);
+
if (!texAttributes.empty())
{
const osg::Texture* diffuseMap = nullptr;
@@ -221,6 +277,11 @@ namespace Shader
const osg::StateAttribute *attr = stateset->getTextureAttribute(unit, osg::StateAttribute::TEXTURE);
if (attr)
{
+ // If textures ever get removed in createProgram, expand this to check we're operating on main texture attribute list
+ // rather than the removed list
+ if (addedState && addedState->hasTextureMode(unit, GL_TEXTURE_2D))
+ continue;
+
const osg::Texture* texture = attr->asTexture();
if (texture)
{
@@ -347,7 +408,6 @@ namespace Shader
osg::StateSet::AttributeList removedAttributes;
if (osg::ref_ptr<osg::StateSet> removedState = getRemovedState(*stateset))
removedAttributes = removedState->getAttributeList();
- osg::ref_ptr<AddedState> addedState = getAddedState(*stateset);
for (const auto* attributeMap : std::initializer_list<const osg::StateSet::AttributeList*>{ &attributes, &removedAttributes })
{
@@ -439,6 +499,22 @@ namespace Shader
return;
}
+ /**
+ * The shader visitor is supposed to be idempotent and undoable.
+ * That means we need to back up state we've removed (so it can be restored and/or considered by further
+ * applications of the visitor) and track which state we added (so it can be removed and/or ignored by further
+ * applications of the visitor).
+ * Before editing writableStateSet in a way that explicitly removes state or might overwrite existing state, it
+ * should be copied to removedState, another StateSet, unless it's there already or was added by a previous
+ * application of the visitor (is in previousAddedState).
+ * If it's a new class of state that's not already handled by ReinstateRemovedStateVisitor::apply, make sure to
+ * add handling there.
+ * Similarly, any time new state is added to writableStateSet, the equivalent method should be called on
+ * addedState.
+ * If that method doesn't exist yet, implement it - we don't use a full StateSet as we only need to check
+ * existence, not equality, and don't need to actually get the value as we can get it from writableStateSet
+ * instead.
+ */
osg::Node& node = *reqs.mNode;
osg::StateSet* writableStateSet = nullptr;
if (mAllowedToModifyStateSets)
@@ -463,7 +539,10 @@ namespace Shader
}
if (defineMap["diffuseMap"] == "0")
+ {
writableStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", false));
+ addedState->addUniform("useDiffuseMapForShadowAlpha");
+ }
defineMap["parallax"] = reqs.mNormalHeight ? "1" : "0";
@@ -472,7 +551,6 @@ namespace Shader
defineMap["alphaFunc"] = std::to_string(reqs.mAlphaFunc);
- // back up removed state in case recreateShaders gets rid of the shader later
osg::ref_ptr<osg::StateSet> removedState;
if ((removedState = getRemovedState(*writableStateSet)) && !mAllowedToModifyStateSets)
removedState = new osg::StateSet(*removedState, osg::CopyOp::SHALLOW_COPY);
@@ -542,18 +620,25 @@ namespace Shader
updateRemovedState(*writableUserData, removedState);
}
- if (!addedState->empty())
+ if (reqs.mSoftParticles)
{
- // user data is normally shallow copied so shared with the original stateset
- osg::ref_ptr<osg::UserDataContainer> writableUserData;
- if (mAllowedToModifyStateSets)
- writableUserData = writableStateSet->getOrCreateUserDataContainer();
- else
- writableUserData = getWritableUserDataContainer(*writableStateSet);
+ osg::ref_ptr<osg::Depth> depth = new SceneUtil::AutoDepth;
+ depth->setWriteMask(false);
+ writableStateSet->setAttributeAndModes(depth, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
+ addedState->setAttributeAndModes(depth);
- updateAddedState(*writableUserData, addedState);
+ writableStateSet->addUniform(new osg::Uniform("particleSize", reqs.mSoftParticleSize));
+ addedState->addUniform("particleSize");
+
+ writableStateSet->addUniform(new osg::Uniform("opaqueDepthTex", 2));
+ addedState->addUniform("opaqueDepthTex");
+
+ writableStateSet->setTextureAttributeAndModes(2, mOpaqueDepthTex, osg::StateAttribute::ON);
+ addedState->setTextureAttributeAndModes(2, mOpaqueDepthTex);
}
+ defineMap["softParticles"] = reqs.mSoftParticles ? "1" : "0";
+
std::string shaderPrefix;
if (!node.getUserValue("shaderPrefix", shaderPrefix))
shaderPrefix = mDefaultShaderPrefix;
@@ -573,6 +658,18 @@ namespace Shader
addedState->addUniform(texIt->second);
}
}
+
+ if (!addedState->empty())
+ {
+ // user data is normally shallow copied so shared with the original stateset
+ osg::ref_ptr<osg::UserDataContainer> writableUserData;
+ if (mAllowedToModifyStateSets)
+ writableUserData = writableStateSet->getOrCreateUserDataContainer();
+ else
+ writableUserData = getWritableUserDataContainer(*writableStateSet);
+
+ updateAddedState(*writableUserData, addedState);
+ }
}
void ShaderVisitor::ensureFFP(osg::Node& node)
@@ -585,6 +682,18 @@ namespace Shader
else
writableStateSet = getWritableStateSet(node);
+ /**
+ * We might have been using shaders temporarily with the node (e.g. if a GlowUpdater applied a temporary
+ * environment map for a temporary enchantment).
+ * We therefore need to remove any state doing so added, and restore any that it removed.
+ * This is kept track of in createProgram in the StateSet's userdata.
+ * If new classes of state get added, handling it here is required - not all StateSet features are implemented
+ * in AddedState yet as so far they've not been necessary.
+ * Removed state requires no particular special handling as it's dealt with by merging StateSets.
+ * We don't need to worry about state in writableStateSet having the OVERRIDE flag as if it's in both, it's also
+ * in addedState, and gets removed first.
+ */
+
// user data is normally shallow copied so shared with the original stateset - we'll need to copy before edits
osg::ref_ptr<osg::UserDataContainer> writableUserData;
@@ -619,6 +728,23 @@ namespace Shader
// We don't have access to the function to do that, and can't call removeAttribute with an iterator
for (const auto& [type, member] : addedState->getAttributes())
writableStateSet->removeAttribute(type, member);
+
+ for (unsigned int unit = 0; unit < writableStateSet->getTextureModeList().size(); ++unit)
+ {
+ for (auto itr = writableStateSet->getTextureModeList()[unit].begin(); itr != writableStateSet->getTextureModeList()[unit].end();)
+ {
+ if (addedState->hasTextureMode(unit, itr->first))
+ writableStateSet->getTextureModeList()[unit].erase(itr++);
+ else
+ ++itr;
+ }
+ }
+
+ for (const auto& [unit, attributeList] : addedState->getTextureAttributes())
+ {
+ for (const auto& [type, member] : attributeList)
+ writableStateSet->removeTextureAttribute(unit, type);
+ }
}
@@ -695,13 +821,22 @@ namespace Shader
void ShaderVisitor::apply(osg::Drawable& drawable)
{
- // non-Geometry drawable (e.g. particle system)
- bool needPop = (drawable.getStateSet() != nullptr);
+ auto partsys = dynamic_cast<osgParticle::ParticleSystem*>(&drawable);
- if (drawable.getStateSet())
+ bool needPop = drawable.getStateSet() || partsys;
+
+ if (needPop)
{
pushRequirements(drawable);
- applyStateSet(drawable.getStateSet(), drawable);
+
+ if (partsys && mOpaqueDepthTex)
+ {
+ mRequirements.back().mSoftParticles = true;
+ mRequirements.back().mSoftParticleSize = partsys->getDefaultParticleTemplate().getSizeRange().maximum;
+ }
+
+ if (drawable.getStateSet())
+ applyStateSet(drawable.getStateSet(), drawable);
}
if (!mRequirements.empty())
@@ -769,6 +904,11 @@ namespace Shader
mConvertAlphaTestToAlphaToCoverage = convert;
}
+ void ShaderVisitor::setOpaqueDepthTex(osg::ref_ptr<osg::Texture2D> texture)
+ {
+ mOpaqueDepthTex = texture;
+ }
+
ReinstateRemovedStateVisitor::ReinstateRemovedStateVisitor(bool allowedToModifyStateSets)
: osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
, mAllowedToModifyStateSets(allowedToModifyStateSets)
@@ -777,6 +917,10 @@ namespace Shader
void ReinstateRemovedStateVisitor::apply(osg::Node& node)
{
+ // TODO: this may eventually need to remove added state.
+ // If so, we can migrate from explicitly copying removed state to just calling osg::StateSet::merge.
+ // Not everything is transferred from removedState yet - implement more when createProgram starts marking more
+ // as removed.
if (node.getStateSet())
{
osg::ref_ptr<osg::StateSet> removedState = getRemovedState(*node.getStateSet());
@@ -802,6 +946,12 @@ namespace Shader
for (const auto& attribute : removedState->getAttributeList())
writableStateSet->setAttribute(attribute.second.first, attribute.second.second);
+
+ for (unsigned int unit = 0; unit < removedState->getTextureModeList().size(); ++unit)
+ {
+ for (const auto&[mode, value] : removedState->getTextureModeList()[unit])
+ writableStateSet->setTextureMode(unit, mode, value);
+ }
}
}
diff --git a/components/shader/shadervisitor.hpp b/components/shader/shadervisitor.hpp
index 5f9739ea90..72dec05b5e 100644
--- a/components/shader/shadervisitor.hpp
+++ b/components/shader/shadervisitor.hpp
@@ -3,6 +3,7 @@
#include <osg/NodeVisitor>
#include <osg/Program>
+#include <osg/Texture2D>
namespace Resource
{
@@ -45,6 +46,8 @@ namespace Shader
void setConvertAlphaTestToAlphaToCoverage(bool convert);
+ void setOpaqueDepthTex(osg::ref_ptr<osg::Texture2D> texture);
+
void apply(osg::Node& node) override;
void apply(osg::Drawable& drawable) override;
@@ -98,6 +101,9 @@ namespace Shader
// -1 == no tangents required
int mTexStageRequiringTangents;
+ bool mSoftParticles;
+ float mSoftParticleSize;
+
// the Node that requested these requirements
osg::Node* mNode;
};
@@ -110,6 +116,7 @@ namespace Shader
bool adjustGeometry(osg::Geometry& sourceGeometry, const ShaderRequirements& reqs);
osg::ref_ptr<const osg::Program> mProgramTemplate;
+ osg::ref_ptr<osg::Texture2D> mOpaqueDepthTex;
};
class ReinstateRemovedStateVisitor : public osg::NodeVisitor
diff --git a/components/sqlite3/request.hpp b/components/sqlite3/request.hpp
index 339c7f7521..0a74bf1cb3 100644
--- a/components/sqlite3/request.hpp
+++ b/components/sqlite3/request.hpp
@@ -2,6 +2,7 @@
#define OPENMW_COMPONENTS_SQLITE3_REQUEST_H
#include "statement.hpp"
+#include "types.hpp"
#include <sqlite3.h>
@@ -53,6 +54,13 @@ namespace Sqlite3
+ ": " + std::string(sqlite3_errmsg(&db)));
}
+ inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, const ConstBlob& value)
+ {
+ if (sqlite3_bind_blob(&stmt, index, value.mData, value.mSize, SQLITE_STATIC) != SQLITE_OK)
+ throw std::runtime_error("Failed to bind blob to parameter " + std::to_string(index)
+ + ": " + std::string(sqlite3_errmsg(&db)));
+ }
+
template <typename T>
inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, const char* name, const T& value)
{
diff --git a/components/sqlite3/types.hpp b/components/sqlite3/types.hpp
new file mode 100644
index 0000000000..325e9e6608
--- /dev/null
+++ b/components/sqlite3/types.hpp
@@ -0,0 +1,15 @@
+#ifndef OPENMW_COMPONENTS_SQLITE3_TYPES_H
+#define OPENMW_COMPONENTS_SQLITE3_TYPES_H
+
+#include <cstddef>
+
+namespace Sqlite3
+{
+ struct ConstBlob
+ {
+ const char* mData;
+ int mSize;
+ };
+}
+
+#endif
diff --git a/components/terrain/buffercache.cpp b/components/terrain/buffercache.cpp
index 399df16d34..658c431b1b 100644
--- a/components/terrain/buffercache.cpp
+++ b/components/terrain/buffercache.cpp
@@ -12,8 +12,8 @@ namespace
template <typename IndexArrayType>
osg::ref_ptr<IndexArrayType> createIndexBuffer(unsigned int flags, unsigned int verts)
{
- // LOD level n means every 2^n-th vertex is kept
- size_t lodLevel = (flags >> (4*4));
+ // LOD level n means every 2^n-th vertex is kept, but we currently handle LOD elsewhere.
+ size_t lodLevel = 0;//(flags >> (4*4));
size_t lodDeltas[4];
for (int i=0; i<4; ++i)
diff --git a/components/terrain/chunkmanager.cpp b/components/terrain/chunkmanager.cpp
index e040cbdd93..d57b73768f 100644
--- a/components/terrain/chunkmanager.cpp
+++ b/components/terrain/chunkmanager.cpp
@@ -52,6 +52,9 @@ struct FindChunkTemplate
osg::ref_ptr<osg::Node> ChunkManager::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile)
{
+ // Override lod with the vertexLodMod adjusted value.
+ // TODO: maybe we can refactor this code by moving all vertexLodMod code into this class.
+ lod = static_cast<unsigned char>(lodFlags >> (4*4));
ChunkId id = std::make_tuple(center, lod, lodFlags);
osg::ref_ptr<osg::Object> obj = mCache->getRefFromObjectCache(id);
if (obj)
diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp
index 22f507b3a2..6ae1a970d3 100644
--- a/components/terrain/material.cpp
+++ b/components/terrain/material.cpp
@@ -8,7 +8,7 @@
#include <osg/BlendFunc>
#include <components/shader/shadermanager.hpp>
-#include <components/sceneutil/util.hpp>
+#include <components/sceneutil/depth.hpp>
#include <mutex>
@@ -87,7 +87,7 @@ namespace
osg::ref_ptr<osg::Depth> mValue;
EqualDepth()
- : mValue(new osg::Depth)
+ : mValue(new SceneUtil::AutoDepth)
{
mValue->setFunction(osg::Depth::EQUAL);
}
@@ -106,7 +106,7 @@ namespace
osg::ref_ptr<osg::Depth> mValue;
LequalDepth()
- : mValue(SceneUtil::createDepth())
+ : mValue(new SceneUtil::AutoDepth(osg::Depth::LEQUAL))
{
}
};
diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp
index c113a629c5..81d3ccb32d 100644
--- a/components/terrain/quadtreenode.cpp
+++ b/components/terrain/quadtreenode.cpp
@@ -10,6 +10,29 @@
namespace Terrain
{
+float distance(const osg::BoundingBox& box, const osg::Vec3f& v)
+{
+ if (box.contains(v))
+ return 0;
+ else
+ {
+ osg::Vec3f maxDist(0,0,0);
+ if (v.x() < box.xMin())
+ maxDist.x() = box.xMin() - v.x();
+ else if (v.x() > box.xMax())
+ maxDist.x() = v.x() - box.xMax();
+ if (v.y() < box.yMin())
+ maxDist.y() = box.yMin() - v.y();
+ else if (v.y() > box.yMax())
+ maxDist.y() = v.y() - box.yMax();
+ if (v.z() < box.zMin())
+ maxDist.z() = box.zMin() - v.z();
+ else if (v.z() > box.zMax())
+ maxDist.z() = v.z() - box.zMax();
+ return maxDist.length();
+ }
+}
+
ChildDirection reflect(ChildDirection dir, Direction dir2)
{
assert(dir != Root);
@@ -78,25 +101,7 @@ QuadTreeNode *QuadTreeNode::getNeighbour(Direction dir)
float QuadTreeNode::distance(const osg::Vec3f& v) const
{
const osg::BoundingBox& box = getBoundingBox();
- if (box.contains(v))
- return 0;
- else
- {
- osg::Vec3f maxDist(0,0,0);
- if (v.x() < box.xMin())
- maxDist.x() = box.xMin() - v.x();
- else if (v.x() > box.xMax())
- maxDist.x() = v.x() - box.xMax();
- if (v.y() < box.yMin())
- maxDist.y() = box.yMin() - v.y();
- else if (v.y() > box.yMax())
- maxDist.y() = v.y() - box.yMax();
- if (v.z() < box.zMin())
- maxDist.z() = box.zMin() - v.z();
- else if (v.z() > box.zMax())
- maxDist.z() = v.z() - box.zMax();
- return maxDist.length();
- }
+ return Terrain::distance(box, v);
}
void QuadTreeNode::initNeighbours()
diff --git a/components/terrain/quadtreenode.hpp b/components/terrain/quadtreenode.hpp
index 7ae59b92f0..aba78b8b3a 100644
--- a/components/terrain/quadtreenode.hpp
+++ b/components/terrain/quadtreenode.hpp
@@ -34,6 +34,8 @@ namespace Terrain
class ViewData;
+ float distance(const osg::BoundingBox&, const osg::Vec3f& v);
+
class QuadTreeNode : public osg::Group
{
public:
diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp
index 7fc0895846..6dbed34331 100644
--- a/components/terrain/quadtreeworld.cpp
+++ b/components/terrain/quadtreeworld.cpp
@@ -40,9 +40,9 @@ namespace
return 1 << depth;
}
- int Log2( unsigned int n )
+ unsigned int Log2( unsigned int n )
{
- int targetlevel = 0;
+ unsigned int targetlevel = 0;
while (n >>= 1) ++targetlevel;
return targetlevel;
}
@@ -78,16 +78,18 @@ public:
if (intersects)
return Deeper;
}
-
dist = std::max(0.f, dist + mDistanceModifier);
-
if (dist > mViewDistance && !activeGrid) // for Scene<->ObjectPaging sync the activegrid must remain loaded
return StopTraversal;
-
- int nativeLodLevel = Log2(static_cast<unsigned int>(node->getSize()/mMinSize));
- int lodLevel = Log2(static_cast<unsigned int>(dist/(Constants::CellSizeInUnits*mMinSize*mFactor)));
-
- return nativeLodLevel <= lodLevel ? StopTraversalAndUse : Deeper;
+ return getNativeLodLevel(node, mMinSize) <= convertDistanceToLodLevel(dist, mMinSize, mFactor) ? StopTraversalAndUse : Deeper;
+ }
+ static unsigned int getNativeLodLevel(const QuadTreeNode* node, float minSize)
+ {
+ return Log2(static_cast<unsigned int>(node->getSize()/minSize));
+ }
+ static unsigned int convertDistanceToLodLevel(float dist, float minSize, float factor)
+ {
+ return Log2(static_cast<unsigned int>(dist/(Constants::CellSizeInUnits*minSize*factor)));
}
private:
@@ -278,7 +280,6 @@ QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resour
, mViewDistance(std::numeric_limits<float>::max())
, mMinSize(1/8.f)
, mDebugTerrainChunks(debugChunks)
- , mRevalidateDistance(0.f)
{
mChunkManager->setCompositeMapSize(compMapResolution);
mChunkManager->setCompositeMapLevel(compMapLevel);
@@ -296,13 +297,14 @@ QuadTreeWorld::~QuadTreeWorld()
{
}
-/// get the level of vertex detail to render this node at, expressed relative to the native resolution of the data set.
+/// get the level of vertex detail to render this node at, expressed relative to the native resolution of the vertex data set,
+/// NOT relative to mMinSize as is the case with node LODs.
unsigned int getVertexLod(QuadTreeNode* node, int vertexLodMod)
{
- int lod = Log2(int(node->getSize()));
+ unsigned int vertexLod = DefaultLodCallback::getNativeLodLevel(node, 1);
if (vertexLodMod > 0)
{
- lod = std::max(0, lod-vertexLodMod);
+ vertexLod = static_cast<unsigned int>(std::max(0, static_cast<int>(vertexLod)-vertexLodMod));
}
else if (vertexLodMod < 0)
{
@@ -313,13 +315,13 @@ unsigned int getVertexLod(QuadTreeNode* node, int vertexLodMod)
size *= 2;
vertexLodMod = std::min(0, vertexLodMod+1);
}
- lod += std::abs(vertexLodMod);
+ vertexLod += std::abs(vertexLodMod);
}
- return lod;
+ return vertexLod;
}
/// get the flags to use for stitching in the index buffer so that chunks of different LOD connect seamlessly
-unsigned int getLodFlags(QuadTreeNode* node, int ourLod, int vertexLodMod, const ViewData* vd)
+unsigned int getLodFlags(QuadTreeNode* node, unsigned int ourVertexLod, int vertexLodMod, const ViewData* vd)
{
unsigned int lodFlags = 0;
for (unsigned int i=0; i<4; ++i)
@@ -332,40 +334,38 @@ unsigned int getLodFlags(QuadTreeNode* node, int ourLod, int vertexLodMod, const
// our detail and the neighbour would handle stitching by itself.
while (neighbour && !vd->contains(neighbour))
neighbour = neighbour->getParent();
- int lod = 0;
+ unsigned int lod = 0;
if (neighbour)
lod = getVertexLod(neighbour, vertexLodMod);
- if (lod <= ourLod) // We only need to worry about neighbours less detailed than we are -
+ if (lod <= ourVertexLod) // We only need to worry about neighbours less detailed than we are -
lod = 0; // neighbours with more detail will do the stitching themselves
// Use 4 bits for each LOD delta
if (lod > 0)
{
- lodFlags |= static_cast<unsigned int>(lod - ourLod) << (4*i);
+ lodFlags |= (lod - ourVertexLod) << (4*i);
}
}
+ // Use the remaining bits for our vertex LOD
+ lodFlags |= (ourVertexLod << (4*4));
return lodFlags;
}
-void QuadTreeWorld::loadRenderingNode(ViewDataEntry& entry, ViewData* vd, float cellWorldSize, const osg::Vec4i &gridbounds, bool compile, float reuseDistance)
+void QuadTreeWorld::loadRenderingNode(ViewDataEntry& entry, ViewData* vd, float cellWorldSize, const osg::Vec4i &gridbounds, bool compile)
{
if (!vd->hasChanged() && entry.mRenderingNode)
return;
- int ourLod = getVertexLod(entry.mNode, mVertexLodMod);
-
if (vd->hasChanged())
{
+ unsigned int ourVertexLod = getVertexLod(entry.mNode, mVertexLodMod);
// have to recompute the lodFlags in case a neighbour has changed LOD.
- unsigned int lodFlags = getLodFlags(entry.mNode, ourLod, mVertexLodMod, vd);
+ unsigned int lodFlags = getLodFlags(entry.mNode, ourVertexLod, mVertexLodMod, vd);
if (lodFlags != entry.mLodFlags)
{
entry.mRenderingNode = nullptr;
entry.mLodFlags = lodFlags;
}
- // have to revalidate chunks within a custom view distance.
- if (mRevalidateDistance && entry.mNode->distance(vd->getViewPoint()) <= mRevalidateDistance + reuseDistance)
- entry.mRenderingNode = nullptr;
}
if (!entry.mRenderingNode)
@@ -378,9 +378,7 @@ void QuadTreeWorld::loadRenderingNode(ViewDataEntry& entry, ViewData* vd, float
for (QuadTreeWorld::ChunkManager* m : mChunkManagers)
{
- if (mRevalidateDistance && m->getViewDistance() && entry.mNode->distance(vd->getViewPoint()) > m->getViewDistance() + reuseDistance)
- continue;
- osg::ref_ptr<osg::Node> n = m->getChunk(entry.mNode->getSize(), entry.mNode->getCenter(), ourLod, entry.mLodFlags, activeGrid, vd->getViewPoint(), compile);
+ osg::ref_ptr<osg::Node> n = m->getChunk(entry.mNode->getSize(), entry.mNode->getCenter(), DefaultLodCallback::getNativeLodLevel(entry.mNode, mMinSize), entry.mLodFlags, activeGrid, vd->getViewPoint(), compile);
if (n) pat->addChild(n);
}
entry.mRenderingNode = pat;
@@ -462,7 +460,7 @@ void QuadTreeWorld::accept(osg::NodeVisitor &nv)
for (unsigned int i=0; i<vd->getNumEntries(); ++i)
{
ViewDataEntry& entry = vd->getEntry(i);
- loadRenderingNode(entry, vd, cellWorldSize, mActiveGrid, false, mViewDataMap->getReuseDistance());
+ loadRenderingNode(entry, vd, cellWorldSize, mActiveGrid, false);
entry.mRenderingNode->accept(nv);
}
@@ -541,12 +539,11 @@ void QuadTreeWorld::preload(View *view, const osg::Vec3f &viewPoint, const osg::
reporter.addTotal(progressTotal);
}
- const float reuseDistance = std::max(mViewDataMap->getReuseDistance(), std::abs(distanceModifier));
for (unsigned int i=startEntry; i<vd->getNumEntries() && !abort; ++i)
{
ViewDataEntry& entry = vd->getEntry(i);
- loadRenderingNode(entry, vd, cellWorldSize, grid, true, reuseDistance);
+ loadRenderingNode(entry, vd, cellWorldSize, grid, true);
if (pass==0) reporter.addProgress(entry.mNode->getSize());
entry.mNode = nullptr; // Clear node lest we break the neighbours search for the next pass
}
@@ -584,7 +581,7 @@ void QuadTreeWorld::addChunkManager(QuadTreeWorld::ChunkManager* m)
mChunkManagers.push_back(m);
mTerrainRoot->setNodeMask(mTerrainRoot->getNodeMask()|m->getNodeMask());
if (m->getViewDistance())
- mRevalidateDistance = std::max(m->getViewDistance(), mRevalidateDistance);
+ m->setMaxLodLevel(DefaultLodCallback::convertDistanceToLodLevel(m->getViewDistance() + mViewDataMap->getReuseDistance(), mMinSize, mLodFactor));
}
void QuadTreeWorld::rebuildViews()
diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp
index 9d21d65fc5..3748f7df98 100644
--- a/components/terrain/quadtreeworld.hpp
+++ b/components/terrain/quadtreeworld.hpp
@@ -56,14 +56,19 @@ namespace Terrain
void setViewDistance(float viewDistance) { mViewDistance = viewDistance; }
float getViewDistance() const { return mViewDistance; }
+
+ // Automatically set by addChunkManager based on getViewDistance()
+ unsigned int getMaxLodLevel() const { return mMaxLodLevel; }
+ void setMaxLodLevel(unsigned int level) { mMaxLodLevel = level; }
private:
float mViewDistance = 0.f;
+ unsigned int mMaxLodLevel = ~0u;
};
void addChunkManager(ChunkManager*);
private:
void ensureQuadTreeBuilt();
- void loadRenderingNode(ViewDataEntry& entry, ViewData* vd, float cellWorldSize, const osg::Vec4i &gridbounds, bool compile, float reuseDistance);
+ void loadRenderingNode(ViewDataEntry& entry, ViewData* vd, float cellWorldSize, const osg::Vec4i &gridbounds, bool compile);
osg::ref_ptr<RootNode> mRootNode;
@@ -79,7 +84,6 @@ namespace Terrain
float mMinSize;
bool mDebugTerrainChunks;
std::unique_ptr<DebugChunkManager> mDebugChunkManager;
- float mRevalidateDistance;
};
}
diff --git a/components/terrain/viewdata.cpp b/components/terrain/viewdata.cpp
index ae23f034a8..033b5f5faa 100644
--- a/components/terrain/viewdata.cpp
+++ b/components/terrain/viewdata.cpp
@@ -145,7 +145,6 @@ ViewData *ViewDataMap::getViewData(osg::Object *viewer, const osg::Vec3f& viewPo
}
vd->setViewPoint(viewPoint);
vd->setActiveGrid(activeGrid);
- vd->setChanged(true);
needsUpdate = true;
}
}
diff --git a/components/terrain/viewdata.hpp b/components/terrain/viewdata.hpp
index b7dbc977b1..d51da011a2 100644
--- a/components/terrain/viewdata.hpp
+++ b/components/terrain/viewdata.hpp
@@ -49,7 +49,7 @@ namespace Terrain
double getLastUsageTimeStamp() const { return mLastUsageTimeStamp; }
void setLastUsageTimeStamp(double timeStamp) { mLastUsageTimeStamp = timeStamp; }
- /// Indicates at least one mNode of mEntries has changed or the view point has moved beyond mReuseDistance.
+ /// Indicates at least one mNode of mEntries has changed.
/// @note Such changes may necessitate a revalidation of cached mRenderingNodes elsewhere depending
/// on the parameters that affect the creation of mRenderingNode.
bool hasChanged() const { return mChanged; }
diff --git a/components/vfs/filesystemarchive.cpp b/components/vfs/filesystemarchive.cpp
index 35f44f46cd..7766a74f49 100644
--- a/components/vfs/filesystemarchive.cpp
+++ b/components/vfs/filesystemarchive.cpp
@@ -1,5 +1,7 @@
#include "filesystemarchive.hpp"
+#include <algorithm>
+
#include <boost/filesystem.hpp>
#include <components/debug/debuglog.hpp>
diff --git a/components/widgets/fontwrapper.hpp b/components/widgets/fontwrapper.hpp
index daa69f9202..16ebba3587 100644
--- a/components/widgets/fontwrapper.hpp
+++ b/components/widgets/fontwrapper.hpp
@@ -31,15 +31,11 @@ namespace Gui
}
private:
- static int clamp(const int& value, const int& lowBound, const int& highBound)
- {
- return std::min(std::max(lowBound, value), highBound);
- }
std::string getFontSize()
{
// Note: we can not use the FontLoader here, so there is a code duplication a bit.
- static const std::string fontSize = std::to_string(clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20));
+ static const std::string fontSize = std::to_string(std::clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20));
return fontSize;
}
};
diff --git a/components/widgets/numericeditbox.cpp b/components/widgets/numericeditbox.cpp
index e8ba226f70..c6ff9628ee 100644
--- a/components/widgets/numericeditbox.cpp
+++ b/components/widgets/numericeditbox.cpp
@@ -31,7 +31,7 @@ namespace Gui
try
{
mValue = std::stoi(newCaption);
- int capped = std::min(mMaxValue, std::max(mValue, mMinValue));
+ int capped = std::clamp(mValue, mMinValue, mMaxValue);
if (capped != mValue)
{
mValue = capped;
diff --git a/docker/Dockerfile.ubuntu b/docker/Dockerfile.ubuntu
new file mode 100644
index 0000000000..b5fadff3e3
--- /dev/null
+++ b/docker/Dockerfile.ubuntu
@@ -0,0 +1,23 @@
+FROM ubuntu
+LABEL maintainer="Wassim DHIF <wassimdhif@gmail.com>"
+
+ENV NPROC=1
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends software-properties-common apt-utils \
+ && add-apt-repository ppa:openmw/openmw \
+ && apt-get update \
+ && apt-get install -y --no-install-recommends openmw openmw-launcher \
+ && apt-get install -y --no-install-recommends git build-essential cmake \
+ libopenal-dev libopenscenegraph-dev libbullet-dev libsdl2-dev \
+ libmygui-dev libunshield-dev liblz4-dev libtinyxml-dev libqt5opengl5-dev \
+ libboost-filesystem-dev libboost-program-options-dev libboost-iostreams-dev \
+ libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev \
+ librecast-dev libsqlite3-dev libluajit-5.1-dev
+
+COPY build.sh /build.sh
+
+RUN mkdir /openmw
+WORKDIR /openmw
+
+ENTRYPOINT ["/build.sh"]
diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 0000000000..4c4131c235
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,17 @@
+# Build OpenMW using Docker
+
+## Build Docker image
+
+Replace `LINUX_VERSION` with the Linux distribution you wish to use.
+```
+docker build -f Dockerfile.LINUX_VERSION -t openmw.LINUX_VERSION .
+```
+
+## Build OpenMW using Docker
+
+Labeling systems like SELinux require that proper labels are placed on volume content mounted into a container.
+Without a label, the security system might prevent the processes running inside the container from using the content.
+The Z option tells Docker to label the content with a private unshared label.
+```
+docker run -v /path/to/openmw:/openmw:Z -e NPROC=2 -it openmw.LINUX_VERSION
+```
diff --git a/docker/build.sh b/docker/build.sh
new file mode 100755
index 0000000000..0f79161379
--- /dev/null
+++ b/docker/build.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -xe
+
+# Creating build directory...
+mkdir -p build
+cd build
+
+# Running CMake...
+cmake ../
+
+# Building with $NPROC CPU...
+make -j $NPROC
diff --git a/docs/source/generate_luadoc.sh b/docs/source/generate_luadoc.sh
index 7a238eca5a..067a1ad4cf 100755
--- a/docs/source/generate_luadoc.sh
+++ b/docs/source/generate_luadoc.sh
@@ -8,6 +8,18 @@
# luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec
# luarocks --local install openmwluadocumentor-0.1.1-1.src.rock
+# How to install on Windows:
+
+# install LuaRocks (heavily recommended to use the standalone package)
+# https://github.com/luarocks/luarocks/wiki/Installation-instructions-for-Windows
+# git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git
+# cd openmw-luadocumentor/luarocks
+# open "Developer Command Prompt for VS <2017/2019>" in this directory and run:
+# luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec
+# luarocks --local install openmwluadocumentor-0.1.1-1.src.rock
+# open "Git Bash" in the same directory and run script:
+# ./generate_luadoc.sh
+
if [ -f /.dockerenv ]; then
# We are inside readthedocs pipeline
echo "Install lua 5.1"
@@ -32,7 +44,6 @@ if [ -f /.dockerenv ]; then
cd ~
git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git
cd openmw-luadocumentor/luarocks
- luarocks --local install checks
luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec
luarocks --local install openmwluadocumentor-0.1.1-1.src.rock
fi
@@ -40,12 +51,19 @@ fi
DOCS_SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
FILES_DIR=$DOCS_SOURCE_DIR/../../files
OUTPUT_DIR=$DOCS_SOURCE_DIR/reference/lua-scripting/generated_html
+DOCUMENTOR_PATH=~/.luarocks/bin/openmwluadocumentor
+
+if [ ! -x $DOCUMENTOR_PATH ]; then
+ # running on Windows?
+ DOCUMENTOR_PATH="$APPDATA/LuaRocks/bin/openmwluadocumentor.bat"
+fi
rm -f $OUTPUT_DIR/*.html
cd $FILES_DIR/lua_api
-~/.luarocks/bin/openmwluadocumentor -f doc -d $OUTPUT_DIR openmw/*lua
+$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR openmw/*lua
cd $FILES_DIR/builtin_scripts
-~/.luarocks/bin/openmwluadocumentor -f doc -d $OUTPUT_DIR openmw_aux/*lua
+$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR openmw_aux/*lua
+$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR scripts/omw/camera.lua
diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst
index 538cfd4c6d..2af7946536 100644
--- a/docs/source/manuals/installation/install-game-files.rst
+++ b/docs/source/manuals/installation/install-game-files.rst
@@ -105,10 +105,10 @@ If you are running macOS, you can also download Morrowind through Steam:
#. Launch the Steam client and let it download. You can then find ``Morrowind.esm`` at
``~/Library/Application Support/Steam/steamapps/common/The Elder Scrolls III - Morrowind/Data Files/``
-Linux
-----
-Debian/Ubuntu - using "Steam Proton" & "OpenMW launcher".
-----
+Linux
+-----
+Debian/Ubuntu - using "Steam Proton" & "OpenMW launcher".
+---------------------------------------------------------
#. Install Steam from "Ubuntu Software" Center
#. Enable Proton (basically WINE under the hood). This is done in the Steam client menu drop down. Select, "Steam | Settings" then in the "SteamPlay" section check the box next to "enable steam play for all other titles"
#. Now Morrowind should be selectable in your game list (as long as you own it). You can install it like any other game, choose to install it and remember the directory path of the location you pick.
diff --git a/docs/source/reference/documentationHowTo.rst b/docs/source/reference/documentationHowTo.rst
index 75dbe8dca2..d2b67d02ca 100644
--- a/docs/source/reference/documentationHowTo.rst
+++ b/docs/source/reference/documentationHowTo.rst
@@ -154,9 +154,9 @@ A push is just copying those "committed" changes to your online repo.
(Commit and push can be combined in one step in PyCharm, so yay)
Once you've pushed all the changes you need to contribute something to the project, you will then submit a pull request,
so called because you are *requesting* that the project maintainers "pull"
- and merge the changes you've made into the project master repository. One of the project maintainers will probably ask
- you to make some corrections or clarifications. Go back and repeat this process to make those changes,
- and repeat until they're good enough to get merged.
+and merge the changes you've made into the project master repository. One of the project maintainers will probably ask
+you to make some corrections or clarifications. Go back and repeat this process to make those changes,
+and repeat until they're good enough to get merged.
So to go over all that again. You rebase *every* time you start working on something to ensure you're working on the most
updated version (I do literally every time I open PyCharm). Then make your edits.
diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst
index 5075f8a5fe..ef831e734a 100644
--- a/docs/source/reference/lua-scripting/api.rst
+++ b/docs/source/reference/lua-scripting/api.rst
@@ -6,8 +6,9 @@ Lua API reference
:hidden:
engine_handlers
+ user_interface
openmw_util
- openmw_settings
+ openmw_storage
openmw_core
openmw_async
openmw_query
@@ -16,10 +17,15 @@ Lua API reference
openmw_nearby
openmw_input
openmw_ui
+ openmw_camera
+ openmw_aux_calendar
openmw_aux_util
+ openmw_aux_time
+ interface_camera
- :ref:`Engine handlers reference`
+- :ref:`User interface reference <User interface reference>`
- `Game object reference <openmw_core.html##(GameObject)>`_
- `Cell reference <openmw_core.html##(Cell)>`_
@@ -39,8 +45,8 @@ Player scripts are local scripts that are attached to a player.
|:ref:`openmw.util <Package openmw.util>` | everywhere | | Defines utility functions and classes like 3D vectors, |
| | | | that don't depend on the game world. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
-|:ref:`openmw.settings <Package openmw.settings>` | everywhere | | Access to GMST records in content files (implemented) and |
-| | | | to mod settings (not implemented). |
+|:ref:`openmw.storage <Package openmw.storage>` | everywhere | | Storage API. In particular can be used to store data |
+| | | | between game sessions. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.core <Package openmw.core>` | everywhere | | Functions that are common for both global and local scripts |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
@@ -54,11 +60,11 @@ Player scripts are local scripts that are attached to a player.
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.nearby <Package openmw.nearby>` | by local scripts | | Read-only access to the nearest area of the game world. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
-|:ref:`openmw.input <Package openmw.input>` | by player scripts | | User input |
+|:ref:`openmw.input <Package openmw.input>` | by player scripts | | User input. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
-|:ref:`openmw.ui <Package openmw.ui>` | by player scripts | | Controls user interface |
+|:ref:`openmw.ui <Package openmw.ui>` | by player scripts | | Controls :ref:`user interface <User interface reference>`. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
-|openmw.camera | by player scripts | | Controls camera (not implemented) |
+|:ref:`openmw.camera <Package openmw.camera>` | by player scripts | | Controls camera. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
**openmw_aux**
@@ -69,6 +75,19 @@ Sources can be found in ``resources/vfs/openmw_aux``. In theory mods can overrid
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
| Built-in library | Can be used | Description |
+=========================================================+====================+===============================================================+
+|:ref:`openmw_aux.calendar <Package openmw_aux.calendar>` | everywhere | | Game time calendar |
++---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw_aux.util <Package openmw_aux.util>` | everywhere | | Miscellaneous utils |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
+|:ref:`openmw_aux.time <Package openmw_aux.time>` | everywhere | | Timers and game time utils |
++---------------------------------------------------------+--------------------+---------------------------------------------------------------+
+
+**Interfaces of built-in scripts**
+
++---------------------------------------------------------+--------------------+---------------------------------------------------------------+
+| Interface | Can be used | Description |
++=========================================================+====================+===============================================================+
+|:ref:`Camera <Interface Camera>` | by player scripts | | Allows to alter behavior of the built-in camera script |
+| | | | without overriding the script completely. |
++---------------------------------------------------------+--------------------+---------------------------------------------------------------+
diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst
index ef74684182..4f5f99965a 100644
--- a/docs/source/reference/lua-scripting/engine_handlers.rst
+++ b/docs/source/reference/lua-scripting/engine_handlers.rst
@@ -10,7 +10,7 @@ Engine handler is a function defined by a script, that can be called by the engi
| | | `be assigned to a script in openmw-cs (not yet implemented)`. |
| | | ``onInterfaceOverride`` can be called before ``onInit``. |
+----------------------------------+----------------------------------------------------------------------+
-| onUpdate(dt) | | Called every frame if game not paused. `dt` is the time |
+| onUpdate(dt) | | Called every frame if the game is not paused. `dt` is the time |
| | | from the last update in seconds. |
+----------------------------------+----------------------------------------------------------------------+
| onSave() -> savedData | | Called when the game is saving. May be called in inactive |
@@ -44,6 +44,9 @@ Engine handler is a function defined by a script, that can be called by the engi
+----------------------------------+----------------------------------------------------------------------+
| **Only for local scripts attached to a player** |
+----------------------------------+----------------------------------------------------------------------+
+| onInputUpdate(dt) | | Called every frame (if the game is not paused) right after |
+| | | processing user input. Use it only for latency-critical stuff. |
++----------------------------------+----------------------------------------------------------------------+
| onKeyPress(key) | | `Key <openmw_input.html##(KeyboardEvent)>`_ is pressed. |
| | | Usage example: ``if key.symbol == 'z' and key.withShift then ...`` |
+----------------------------------+----------------------------------------------------------------------+
diff --git a/docs/source/reference/lua-scripting/interface_camera.rst b/docs/source/reference/lua-scripting/interface_camera.rst
new file mode 100644
index 0000000000..c2db83b721
--- /dev/null
+++ b/docs/source/reference/lua-scripting/interface_camera.rst
@@ -0,0 +1,6 @@
+Interface Camera
+================
+
+.. raw:: html
+ :file: generated_html/scripts_omw_camera.html
+
diff --git a/docs/source/reference/lua-scripting/openmw_aux_calendar.rst b/docs/source/reference/lua-scripting/openmw_aux_calendar.rst
new file mode 100644
index 0000000000..ea60b62852
--- /dev/null
+++ b/docs/source/reference/lua-scripting/openmw_aux_calendar.rst
@@ -0,0 +1,5 @@
+Package openmw_aux.calendar
+===========================
+
+.. raw:: html
+ :file: generated_html/openmw_aux_calendar.html
diff --git a/docs/source/reference/lua-scripting/openmw_aux_time.rst b/docs/source/reference/lua-scripting/openmw_aux_time.rst
new file mode 100644
index 0000000000..120d888a01
--- /dev/null
+++ b/docs/source/reference/lua-scripting/openmw_aux_time.rst
@@ -0,0 +1,5 @@
+Package openmw_aux.time
+=======================
+
+.. raw:: html
+ :file: generated_html/openmw_aux_time.html
diff --git a/docs/source/reference/lua-scripting/openmw_camera.rst b/docs/source/reference/lua-scripting/openmw_camera.rst
new file mode 100644
index 0000000000..4090843920
--- /dev/null
+++ b/docs/source/reference/lua-scripting/openmw_camera.rst
@@ -0,0 +1,5 @@
+Package openmw.camera
+=====================
+
+.. raw:: html
+ :file: generated_html/openmw_camera.html
diff --git a/docs/source/reference/lua-scripting/openmw_settings.rst b/docs/source/reference/lua-scripting/openmw_settings.rst
deleted file mode 100644
index f3d26882bb..0000000000
--- a/docs/source/reference/lua-scripting/openmw_settings.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Package openmw.settings
-=======================
-
-.. raw:: html
- :file: generated_html/openmw_settings.html
diff --git a/docs/source/reference/lua-scripting/openmw_storage.rst b/docs/source/reference/lua-scripting/openmw_storage.rst
new file mode 100644
index 0000000000..5abf664e1a
--- /dev/null
+++ b/docs/source/reference/lua-scripting/openmw_storage.rst
@@ -0,0 +1,5 @@
+Package openmw.storage
+======================
+
+.. raw:: html
+ :file: generated_html/openmw_storage.html
diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst
index c32fc74fa5..9de283b029 100644
--- a/docs/source/reference/lua-scripting/overview.rst
+++ b/docs/source/reference/lua-scripting/overview.rst
@@ -15,10 +15,11 @@ Here are starting points for learning Lua:
Each script works in a separate sandbox and doesn't have any access to the underlying operating system.
Only a limited list of allowed standard libraries can be used:
`coroutine <https://www.lua.org/manual/5.1/manual.html#5.2>`__,
-`math <https://www.lua.org/manual/5.1/manual.html#5.6>`__,
+`math <https://www.lua.org/manual/5.1/manual.html#5.6>`__ (except `math.randomseed` -- it is called by the engine on startup and not available from scripts),
`string <https://www.lua.org/manual/5.1/manual.html#5.4>`__,
-`table <https://www.lua.org/manual/5.1/manual.html#5.5>`__.
-These libraries are loaded automatically and are always available (except the function `math.randomseed` -- it is called by the engine on startup and not available from scripts).
+`table <https://www.lua.org/manual/5.1/manual.html#5.5>`__,
+`os <https://www.lua.org/manual/5.1/manual.html#5.8>`__ (only `os.date`, `os.difftime`, `os.time`).
+These libraries are loaded automatically and are always available.
Allowed `basic functions <https://www.lua.org/manual/5.1/manual.html#5.1>`__:
``assert``, ``error``, ``ipairs``, ``next``, ``pairs``, ``pcall``, ``print``, ``select``, ``tonumber``, ``tostring``, ``type``, ``unpack``, ``xpcall``, ``rawequal``, ``rawget``, ``rawset``, ``getmetatable``, ``setmetatable``.
@@ -333,8 +334,8 @@ Player scripts are local scripts that are attached to a player.
|:ref:`openmw.util <Package openmw.util>` | everywhere | | Defines utility functions and classes like 3D vectors, |
| | | | that don't depend on the game world. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
-|:ref:`openmw.settings <Package openmw.settings>` | everywhere | | Access to GMST records in content files (implemented) and |
-| | | | to mod settings (not implemented). |
+|:ref:`openmw.storage <Package openmw.storage>` | everywhere | | Storage API. In particular can be used to store data |
+| | | | between game sessions. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.core <Package openmw.core>` | everywhere | | Functions that are common for both global and local scripts |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
@@ -350,9 +351,9 @@ Player scripts are local scripts that are attached to a player.
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.input <Package openmw.input>` | by player scripts | | User input |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
-|:ref:`openmw.ui <Package openmw.ui>` | by player scripts | | Controls user interface |
+|:ref:`openmw.ui <Package openmw.ui>` | by player scripts | | Controls :ref:`user interface <User interface reference>` |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
-|openmw.camera | by player scripts | | Controls camera (not implemented) |
+|:ref:`openmw.camera <Package openmw.camera>` | by player scripts | | Controls camera |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
openmw_aux
@@ -364,15 +365,19 @@ Sources can be found in ``resources/vfs/openmw_aux``. In theory mods can overrid
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
| Built-in library | Can be used | Description |
+=========================================================+====================+===============================================================+
+|:ref:`openmw_aux.calendar <Package openmw_aux.calendar>` | everywhere | | Game time calendar |
++---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw_aux.util <Package openmw_aux.util>` | everywhere | | Miscellaneous utils |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
+|:ref:`openmw_aux.time <Package openmw_aux.time>` | everywhere | | Timers and game time utils |
++---------------------------------------------------------+--------------------+---------------------------------------------------------------+
They can be loaded with ``require`` the same as API packages. For example:
.. code-block:: Lua
- local aux_util = require('openmw_aux.util')
- aux_util.runEveryNSeconds(15, doSomething) -- run `doSomething()` every 15 seconds
+ local time = require('openmw_aux.time')
+ time.runRepeatedly(doSomething, 15 * time.second) -- run `doSomething()` every 15 seconds
Script interfaces
@@ -439,6 +444,15 @@ Using the interface:
The order in which the scripts are started is important. So if one mod should override an interface provided by another mod, make sure that load order (i.e. the sequence of `lua-scripts=...` in `openmw.cfg`) is correct.
+**Interfaces of built-in scripts**
+
++---------------------------------------------------------+--------------------+---------------------------------------------------------------+
+| Interface | Can be used | Description |
++=========================================================+====================+===============================================================+
+|:ref:`Camera <Interface Camera>` | by player scripts | | Allows to alter behavior of the built-in camera script |
+| | | | without overriding the script completely. |
++---------------------------------------------------------+--------------------+---------------------------------------------------------------+
+
Event system
============
@@ -719,8 +733,7 @@ you can import these files to get code autocompletion and integrated OpenMW API
.. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-project-settings.png
- Press `Next`, choose the `Libraries` tab, and click `Add External Source Folder`.
-- Specify there the path to ``resources/lua_api`` in your OpenMW installation.
-- If you use `openmw_aux`_, add ``resources/vfs`` as an additional external source folder.
+- Specify there paths to ``resources/lua_api`` and ``resources/vfs`` in your OpenMW installation.
.. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-import-api.png
@@ -749,6 +762,17 @@ You can add special hints to give LDT more information:
.. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-code-completion2.png
-See `LDT Documentation Language <https://wiki.eclipse.org/LDT/User_Area/Documentation_Language>`__ for more details.
+In order to have autocompletion for script interfaces the information where to find these interfaces should be provided.
+For example for the camera interface (defined in ``resources/vfs/scripts/omw/camera.lua``):
+
+.. code-block:: Lua
+
+ --- @type Interfaces
+ -- @field scripts.omw.camera#Interface Camera
+ -- ... other interfaces here
+ --- @field #Interfaces I
+ local I = require('openmw.interfaces')
+ I.Camera.disableZoom()
+See `LDT Documentation Language <https://wiki.eclipse.org/LDT/User_Area/Documentation_Language>`__ for more details.
diff --git a/docs/source/reference/lua-scripting/user_interface.rst b/docs/source/reference/lua-scripting/user_interface.rst
new file mode 100644
index 0000000000..73ca57d1a7
--- /dev/null
+++ b/docs/source/reference/lua-scripting/user_interface.rst
@@ -0,0 +1,172 @@
+User interface reference
+========================
+
+.. toctree::
+ :hidden:
+
+ widgets/widget
+
+Layouts
+-------
+
+Every widget is defined by a layout, which is a Lua table with the following fields (all of them are optional):
+
+1. `type`: One of the available widget types from `openmw.ui.TYPE`.
+2. | `props`: A Lua table, containing all the properties values.
+ | Properties define most of the information about the widget: its position, data it displays, etc.
+ | See the widget pages (table below) for details on specific properties.
+ | Properties of the basic Widget are inherited by all the other widgets.
+3. | `events`: A Lua table, containing `openmw.async.callback` values, which trigger on various interactions with the widget.
+ | See the Widget pages for details on specific events.
+ | Events of the basic Widget are inherited by all the other widgets.
+4. `content`: a Content (`openmw.ui.content`), which contains layouts for the children of this widget.
+5. | `name`: an arbitrary string, the only limitatiion is it being unique within a `Content`.
+ | Helpful for navigatilng through the layouts.
+6. `layer`: only applies for the root widget.
+
+Layers
+------
+Layers control how widgets overlap - layers with higher indexes cover render over layers with lower indexes.
+Widgets within the same layer which were added later overlap the ones created earlier.
+A layer can also be set as non-interactive, which prevents all mouse interactions with the widgets in that layer.
+
+.. TODO: Move this list when layers are de-hardcoded
+
+Pre-defined OpenMW layers:
+
+1. `HUD` interactive
+2. `Windows` interactive
+3. `Notification` non-interactive
+4. `MessageBox` interactive
+
+Elements
+--------
+
+Element is the root widget of a layout.
+It is an independent part of the UI, connected only to a specific layer, but not any other layouts.
+Creating or destroying an element also creates/destroys all of its children.
+
+Content
+-------
+
+A container holding all the widget's children. It has a few important differences from a Lua table:
+
+1. All the keys are integers, i. e. it is an "array"
+2. Holes are not allowed. At any point all keys from `1` to the highest `n` must contain a value.
+3. | You can access the values by their `name` field as a `Content` key.
+ | While there is nothing preventing you from changing the `name` of a table inside a content, it is not supported, and will lead to undefined behaviour.
+ | If you have to change the name, assign a new table to the index instead.
+
+.. TODO: Talk about skins/templates here when they are ready
+
+Events
+------
+
+| A table mapping event names to `openmw.async.callback` s.
+| When an event triggers, the callback is called with two arguments:
+ an event-specific value, and that widget's layout table.
+| See the Widget type pages for information on what events exist, and which first argument they pass.
+
+Widget types
+------------
+
+.. list-table::
+ :widths: 30 70
+
+ * - :ref:`Widget`
+ - Base widget type, all the other widget inherit its properties and events.
+ * - `Text`
+ - Displays text.
+ * - EditText
+ - Accepts text input from the user.
+ * - Window
+ - Can be moved and resized by the user.
+
+Example
+-------
+
+*scripts/requirePassword.lua*
+
+.. code-block:: Lua
+
+ local core = require('openmw.core')
+ local async = require('openmw.async')
+ local ui = require('openmw.ui')
+ local v2 = require('openmw.util').vector2
+
+ local layout = {
+ layers = 'Windows',
+ type = ui.TYPE.Window,
+ skin = 'MW_Window', -- TODO: replace all skins here when they are properly implemented
+ props = {
+ size = v2(200, 250),
+ -- put the window in the middle of the screen
+ relativePosition = v2(0.5, 0.5),
+ anchor = v2(0.5, 0.5),
+ },
+ content = ui.content {
+ {
+ type = ui.TYPE.Text,
+ skin = 'SandText',
+ props = {
+ caption = 'Input password',
+ relativePosition = v2(0.5, 0),
+ anchor = v2(0.5, 0),
+ },
+ },
+ {
+ name = 'input',
+ type = ui.TYPE.TextEdit,
+ skin = "MW_TextEdit",
+ props = {
+ caption = '',
+ relativePosition = v2(0.5, 0.5),
+ anchor = v2(0.5, 0.5),
+ size = v2(125, 50),
+ },
+ events = {}
+ },
+ {
+ name = 'submit',
+ type = ui.TYPE.Text, -- TODO: replace with button when implemented
+ skin = "MW_Button",
+ props = {
+ caption = 'Submit',
+ -- position at the bottom
+ relativePosition = v2(0.5, 1.0),
+ anchor = v2(0.5, 1.0),
+ autoSize = false,
+ size = v2(75, 50),
+ },
+ events = {},
+ },
+ },
+ }
+
+ local element = nil
+
+ local input = layout.content.input
+ -- TODO: replace with a better event when TextEdit is finished
+ input.events.textInput = async:callback(function(text)
+ input.props.caption = input.props.caption .. text
+ end)
+
+ local submit = layout.content.submit
+ submit.events.mouseClick = async:callback(function()
+ if input.props.caption == 'very secret password' then
+ if element then
+ element:destroy()
+ end
+ else
+ print('wrong password', input.props.caption)
+ core.quit()
+ end
+ end)
+
+ element = ui.create(layout)
+
+*requirePassword.omwscripts*
+
+::
+
+ PLAYER: scripts/requirePassword.lua
diff --git a/docs/source/reference/lua-scripting/widgets/widget.rst b/docs/source/reference/lua-scripting/widgets/widget.rst
new file mode 100644
index 0000000000..49058ee278
--- /dev/null
+++ b/docs/source/reference/lua-scripting/widgets/widget.rst
@@ -0,0 +1,77 @@
+Widget
+======
+
+Properties
+----------
+
+.. list-table::
+ :header-rows: 1
+ :widths: 20 20 60
+
+ * - name
+ - type (default value)
+ - description
+ * - position
+ - util.vector2 (0, 0)
+ - | Offsets the position of the widget from its parent's
+ | top-left corner in pixels.
+ * - size
+ - util.vector2 (0, 0)
+ - Increases the widget's size in pixels.
+ * - relativePosition
+ - util.vector2 (0, 0)
+ - | Offsets the position of the widget from its parent's
+ | top-left corner as a fraction of the parent's size.
+ * - relativeSize
+ - util.vector2 (0, 0)
+ - Increases the widget's size by a fraction of its parent's size.
+ * - anchor
+ - util.vector2 (0, 0)
+ - | Offsets the widget's position by a fraction of its size.
+ | Useful for centering or aligning to a corner.
+ * - visible
+ - boolean (true)
+ - Defines if the widget is visible
+
+Events
+------
+
+.. list-table::
+ :header-rows: 1
+ :widths: 20 20 60
+
+ * - name
+ - first argument type
+ - description
+ * - keyPress
+ - `KeyboardEvent <../openmw_input.html##(KeyboardEvent)>`_
+ - A key was pressed with this widget in focus
+ * - keyRelease
+ - `KeyboardEvent <../openmw_input.html##(KeyboardEvent)>`_
+ - A key was released with this widget in focus
+ * - mouseMove
+ - `MouseEvent <../openmw_ui.html##(MouseEvent)>`_
+ - | Mouse cursor moved on this widget
+ | `MouseEvent.button` is the mouse button being held
+ | (nil when simply moving, and not dragging)
+ * - mouseClick
+ - nil
+ - Widget was clicked with left mouse button
+ * - mouseDoubleClick
+ - nil
+ - Widget was double clicked with left mouse button
+ * - mousePress
+ - `MouseEvent <../openmw_ui.html##(MouseEvent)>`_
+ - A mouse button was pressed on this widget
+ * - mouseRelease
+ - `MouseEvent <../openmw_ui.html##(MouseEvent)>`_
+ - A mouse button was released on this widget
+ * - focusGain
+ - nil
+ - Widget gained focus (either through mouse or keyboard)
+ * - focusLoss
+ - nil
+ - Widget lost focus
+ * - textInput
+ - string
+ - Text input with this widget in focus
diff --git a/docs/source/reference/modding/custom-models/index.rst b/docs/source/reference/modding/custom-models/index.rst
index b204dffd99..8b6c5d87f7 100644
--- a/docs/source/reference/modding/custom-models/index.rst
+++ b/docs/source/reference/modding/custom-models/index.rst
@@ -19,5 +19,6 @@ Below is a quick overview of supported formats, followed by separate articles wi
:maxdepth: 1
pipeline-blender-collada
+ pipeline-blender-collada-static-models
pipeline-blender-osgnative
pipeline-blender-nif
diff --git a/docs/source/reference/modding/custom-models/pipeline-blender-collada-static-models.rst b/docs/source/reference/modding/custom-models/pipeline-blender-collada-static-models.rst
new file mode 100644
index 0000000000..5cb336a305
--- /dev/null
+++ b/docs/source/reference/modding/custom-models/pipeline-blender-collada-static-models.rst
@@ -0,0 +1,129 @@
+########################
+Static Model via COLLADA
+########################
+
+This tutorial shows how to get a static model from Blender to OpenMW
+using the COLLADA format. It does not cover using Blender itself, as there are
+many better resources for that. The focus is solely on the pipeline and its
+specific requirements. Static models are those that don’t have any animations
+included in the exported file.
+
+Requirements
+************
+
+To use the Blender to OpenMW pipeline via COLLADA, you will need the following.
+
+* `OpenMW 0.47 <https://openmw.org/downloads/>`_ or later
+* `Blender 2.83 <https://www.blender.org/download/>`_ or later. Latest confirmed, working version is Blender 3.0
+* `OpenMW COLLADA Exporter <https://github.com/openmw/collada-exporter>`_
+* A model you would like to export. In our case, it's a barrel.
+
+The Barrel
+********
+The barrel shown in this tutorial, and its revelant files, are available from
+the `Example Suite repository <https://gitlab.com/OpenMW/example-suite/-/tree/master/example_static_props>`_.
+This should be useful for further study of how to set up a static prop in case
+the tutorial is not clear on any particular thing.
+
+.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/custom-models/_static/barrel-prop-in-blender.webp
+ :align: center
+
+* ``data/meshes/the_barrel.dae`` – exported model
+* ``data/textures/the_barrel.dds`` – diffuse texture
+* ``data/textures/the_barrel_n.dds`` – normal map
+* ``data/textures/the_barrel_n.dds`` – specular map
+* ``source_assets/the_barrel.blend`` – source file configured as this tutorial specifies
+
+Location, Rotation, Scale
+*************************
+
+First, let's take a look at how the fundamental properties of a scene
+in Blender translate to a COLLADA model suitable for use in OpenMW. These apply
+the same to static and animated models.
+
+Location
+========
+
+Objects keep their visual location and origin they had in the original scene.
+
+Rotation
+========
+
+* Blender’s +Z axis is up axis in OpenMW
+* Blender’s +Y axis is front axis in OpenMW
+* Blender’s X axis is left-right axis in OpenMW
+
+Scale
+=====
+
+Scale ratio between Blender and OpenMW is 70 to 1. This means 70 units in
+Blender translate to 1 m in OpenMW.
+
+However, a scale factor like this is impractical to work with. A better
+approach is to work with a scale of 1 Blender unit = 1 m and apply the 70 scale
+factor at export. The exporter will automatically scale all object, mesh,
+armature and animation data.
+
+
+Materials
+*********
+
+OpenMW uses the classic, specular material setup and currently doesn't
+support the more mainstream `PBR <https://en.wikipedia.org/wiki/Physically_based_rendering>`_
+way. In Blender, the mesh needs a default material with a diffuse texture
+connected to the ``Base Color`` socket. This is enough for the material to be
+included in the exported COLLADA file.
+
+.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/custom-models/_static/barrel-prop-in-blender-material.webp
+ :align: center
+
+Additional texture types, such as specular or normal maps, are used
+when enabled in the OpenMW Launcher with the ``Auto use object normal maps``
+and ``Auto use object specular maps`` options. The textures need to follow the
+name of the diffuse texture with an additional suffix, and be in the same
+folder. OpenMW will then automatically recognize and use these textures. In the
+case of the barrel, the textures are named:
+
+* ``the_barrel.dds`` - diffuse texture
+* ``the_barrel_n.dds`` - normal map
+* ``the_barrel_spec.dds`` - specular map
+
+Collision Shapes
+****************
+
+In Blender, a custom collision shape is set up with an empty named
+``Collision`` or ``collision``. Any mesh that is a child of this empty will be
+used for physics collision and will not be visible in-game. There can be
+multiple child meshes under ``collision`` and they will all contribute to the
+collision shapes. The meshes themselves can have an arbitrary name, it's only
+the name of the empty that is important. The ``tcb`` command in OpenMW's in-game
+console will make the collision shapes visible and you will be able to inspect
+them.
+
+.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/custom-models/_static/barrel-prop-in-blender-collision.webp
+ :align: center
+
+If no custom collision shape is present, OpenMW will use the regular
+mesh itself, which is not optimal and should be avoided.
+
+Exporter Settings
+*****************
+
+For static models, use the following exporter settings. Before export, select
+all objects you wish to include in the exported file and have the "Selected
+Objects" option enabled. Without this, the exporter could fail.
+
+
+.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/custom-models/_static/dae-exporter-static.webp
+ :align: center
+
+Getting the Model in-game
+*************************
+
+Once the model is exported, it needs to be placed in the correct folder where
+OpenMW will read it. In OpenMW-CS it can then be assigned to an object and added
+to a world cell.
+
+
+.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/custom-models/_static/barrel-prop-in-openmwcs.webp
+ :align: center
diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst
index fb7b537701..f407c6a126 100644
--- a/docs/source/reference/modding/settings/game.rst
+++ b/docs/source/reference/modding/settings/game.rst
@@ -275,7 +275,7 @@ normalise race speed
:Range: True/False
:Default: False
-By default race weight is factored into horizontal movement speed like in Morrowind.
+By default race weight is factored into horizontal movement and magic projectile speed like in Morrowind.
For example, an NPC which has 1.2 race weight is faster than an NPC with the exact same stats and weight 1.0 by a factor of 120%.
If this setting is true, race weight is ignored in the calculations which allows for a movement behavior
equivalent to the one introduced by the equivalent Morrowind Code Patch feature.
@@ -441,7 +441,7 @@ Some mods add harvestable container models. When this setting is enabled, activa
When this setting is turned off or when activating a regular container, the menu will open as usual.
allow actors to follow over water surface
----------------------
+-----------------------------------------
:Type: boolean
:Range: True/False
@@ -464,3 +464,13 @@ default actor pathfind half extents
:Default: 29.27999496459961 28.479997634887695 66.5
Actor half extents used for exterior cells to generate navmesh.
+Changing the value will invalidate navmesh disk cache.
+
+day night switches
+------------------
+
+:Type: boolean
+:Range: True/False
+:Default: True
+
+Some mods add models which change visuals based on time of day. When this setting is enabled, supporting models will automatically make use of Day/night state.
diff --git a/docs/source/reference/modding/settings/general.rst b/docs/source/reference/modding/settings/general.rst
index f0ebe4f972..ee5b908b4a 100644
--- a/docs/source/reference/modding/settings/general.rst
+++ b/docs/source/reference/modding/settings/general.rst
@@ -61,7 +61,7 @@ Mipmapping is a way of reducing the processing power needed during minification
by pregenerating a series of smaller textures.
notify on saved screenshot
---------------
+--------------------------
:Type: boolean
:Range: True/False
diff --git a/docs/source/reference/modding/settings/groundcover.rst b/docs/source/reference/modding/settings/groundcover.rst
index 3e943e4284..7b060f58ad 100644
--- a/docs/source/reference/modding/settings/groundcover.rst
+++ b/docs/source/reference/modding/settings/groundcover.rst
@@ -51,6 +51,7 @@ Determines whether grass should respond to the player treading on it.
.. list-table:: Modes
:header-rows: 1
+
* - Mode number
- Meaning
* - 0
@@ -77,6 +78,7 @@ How far away from the player grass can be before it's unaffected by being trod o
.. list-table:: Presets
:header-rows: 1
+
* - Preset number
- Range (Units)
- Distance (Units)
diff --git a/docs/source/reference/modding/settings/lua.rst b/docs/source/reference/modding/settings/lua.rst
index 4d1a3ca808..919d530d18 100644
--- a/docs/source/reference/modding/settings/lua.rst
+++ b/docs/source/reference/modding/settings/lua.rst
@@ -1,6 +1,18 @@
Lua Settings
############
+lua debug
+---------
+
+:Type: boolean
+:Range: True/False
+:Default: False
+
+Enables debug tracebacks for Lua actions.
+It adds significant performance overhead, don't enable if you don't need it.
+
+This setting can only be configured by editing the settings configuration file.
+
lua num threads
---------------
@@ -15,3 +27,14 @@ Values >1 are not yet supported.
This setting can only be configured by editing the settings configuration file.
+i18n preferred languages
+------------------------
+
+:Type: string
+:Default: en
+
+List of the preferred languages separated by comma.
+For example "de,en" means German as the first prority and English as a fallback.
+
+This setting can only be configured by editing the settings configuration file.
+
diff --git a/docs/source/reference/modding/settings/map.rst b/docs/source/reference/modding/settings/map.rst
index a4d3cd7e0d..1412d6584f 100644
--- a/docs/source/reference/modding/settings/map.rst
+++ b/docs/source/reference/modding/settings/map.rst
@@ -125,6 +125,7 @@ max local viewing distance
This setting controls the viewing distance on local map when 'distant terrain' is enabled.
If this setting is greater than the viewing distance then only up to the viewing distance is used for local map, otherwise the viewing distance is used.
If view distance is changed in settings menu during the game, then viewable distance on the local map is not updated.
+
.. warning::
Increasing this setting can increase cell load times,
because the localmap take a snapshot of each cell contained in a square of 2 x (max local viewing distance) + 1 square.
diff --git a/docs/source/reference/modding/settings/navigator.rst b/docs/source/reference/modding/settings/navigator.rst
index fee4b2626e..6d00c770bc 100644
--- a/docs/source/reference/modding/settings/navigator.rst
+++ b/docs/source/reference/modding/settings/navigator.rst
@@ -1,5 +1,5 @@
Navigator Settings
-################
+##################
Main settings
*************
@@ -43,7 +43,7 @@ Increasing this value may decrease performance.
It's a limitation of `Recastnavigation <https://github.com/recastnavigation/recastnavigation>`_ library.
wait until min distance to player
-------------------------------
+---------------------------------
:Type: integer
:Range: >= 0
@@ -54,6 +54,26 @@ Allows to complete cell loading only when minimal navigation mesh area is genera
nearby the player. Increasing this value will keep loading screen longer but will slightly increase nav mesh generation
speed on systems bound by CPU. Zero means no waiting.
+enable nav mesh disk cache
+--------------------------
+
+:Type: boolean
+:Range: True/False
+:Default: True
+
+If true navmesh cache stored on disk will be used in addition to memory cache.
+If navmesh tile is not present in memory cache, it will be looked up in the disk cache.
+If it's not found there it will be generated.
+
+write to navmeshdb
+------------------
+
+:Type: boolean
+:Range: True/False
+:Default: False
+
+If true generated navmesh tiles will be stored into disk cache while game is running.
+
Advanced settings
*****************
@@ -87,7 +107,7 @@ Memory will be consumed in approximately linear dependency from number of nav me
But only for new locations or already dropped from cache.
min update interval ms
-----------------
+----------------------
:Type: integer
:Range: >= 0
@@ -181,7 +201,7 @@ Every nav mesh is visible and every update is noticable.
Potentially decreases performance.
enable agents paths render
--------------------
+--------------------------
:Type: boolean
:Range: True/False
@@ -193,7 +213,7 @@ Works even if Navigator is disabled.
Potentially decreases performance.
enable recast mesh render
-----------------------
+-------------------------
:Type: boolean
:Range: True/False
@@ -206,6 +226,17 @@ Absent pieces usually mean a bug in recast mesh tiles building.
Allows to do in-game debug.
Potentially decreases performance.
+nav mesh version
+----------------
+
+:Type: integer
+:Range: > 0
+:Default: 1
+
+Version of navigation mesh generation algorithm.
+Should be increased each time there is a difference between output of makeNavMeshTileData function for the same input.
+Changing the value will invalidate navmesh disk cache.
+
Expert settings
***************
@@ -365,20 +396,20 @@ max verts per poly
The maximum number of vertices allowed for polygons generated during the contour to polygon conversion process.
-region merge size
+region merge area
-----------------
:Type: integer
:Range: >= 0
-:Default: 20
+:Default: 400
Any regions with a span count smaller than this value will, if possible, be merged with larger regions.
-region min size
+region min area
---------------
:Type: integer
:Range: >= 0
-:Default: 8
+:Default: 64
The minimum number of cells allowed to form isolated island areas.
diff --git a/docs/source/reference/modding/settings/shaders.rst b/docs/source/reference/modding/settings/shaders.rst
index 03b7805de6..5629e321d0 100644
--- a/docs/source/reference/modding/settings/shaders.rst
+++ b/docs/source/reference/modding/settings/shaders.rst
@@ -241,7 +241,7 @@ lighting` is on.
This setting has no effect if :ref:`lighting method` is 'legacy'.
minimum interior brightness
-------------------------
+---------------------------
:Type: float
:Range: 0.0-1.0
@@ -260,7 +260,7 @@ aforementioned changes in visuals.
This setting has no effect if :ref:`lighting method` is 'legacy'.
antialias alpha test
----------------------------------------
+--------------------
:Type: boolean
:Range: True/False
@@ -269,3 +269,19 @@ antialias alpha test
Convert the alpha test (cutout/punchthrough alpha) to alpha-to-coverage when :ref:`antialiasing` is on.
This allows MSAA to work with alpha-tested meshes, producing better-looking edges without pixelation.
When MSAA is off, this setting will have no visible effect, but might have a performance cost.
+
+soft particles
+--------------
+
+:Type: boolean
+:Range: True/False
+:Default: False
+
+Enables soft particles for particle effects. This technique softens the
+intersection between individual particles and other opaque geometry by blending
+between them. Note, this relies on overriding specific properties of particle
+systems that potentially differ from the source content, this setting may change
+the look of some particle systems.
+
+Note that the rendering will act as if you have 'force shaders' option enabled.
+This means that shaders will be used to render all objects and the terrain. \ No newline at end of file
diff --git a/docs/source/reference/modding/settings/water.rst b/docs/source/reference/modding/settings/water.rst
index b3a6f8c1f2..fb3b755c6f 100644
--- a/docs/source/reference/modding/settings/water.rst
+++ b/docs/source/reference/modding/settings/water.rst
@@ -77,6 +77,21 @@ Controls what kinds of things are rendered in water reflections.
In interiors the lowest level is 2.
This setting can be changed ingame with the "Reflection shader detail" dropdown under the Water tab of the Video panel in the Options menu.
+rain ripple detail
+-----------------
+
+:Type: integer
+:Range: 0, 1, 2
+:Default: 2
+
+Controls how detailed the raindrop ripples on water are.
+
+0: single, non-normal-mapped ring per raindrop
+1: normal-mapped raindrops, with multiple rings
+2: same as 1, but with a greater number of raindrops
+
+This setting can be changed ingame with the "Rain ripple detail/density" dropdown under the Water tab of the Video panel in the Options menu.
+
small feature culling pixel size
--------------------------------
diff --git a/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst b/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst
index 0ad35d7a50..e05177c268 100644
--- a/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst
+++ b/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst
@@ -15,7 +15,7 @@ Normal maps from Morrowind to OpenMW
- `Tutorial - Morrowind, Part 2`_
General introduction to normal map conversion
-------------------------------------------------
+---------------------------------------------
:Authors: Joakim (Lysol) Berg, Alexei (Capo) Dobrohotov
:Updated: 2020-03-03
@@ -34,7 +34,7 @@ There are several techniques for bump-mapping, and normal-mapping is the most co
So let's get on with it.
OpenMW normal-mapping
-************************
+*********************
Normal-mapping in OpenMW works in a very simple way: The engine just looks for a texture with a *_n.dds* suffix,
and you're done.
@@ -70,7 +70,7 @@ settings.cfg_-file. Add these rows where it would make sense:
See OpenMW's wiki page about `texture modding`_ to read more about it.
Morrowind bump-mapping
-*****************************************************
+**********************
**Conversion difficulty:**
*Varies. Sometimes quick and easy, sometimes time-consuming and hard.*
@@ -93,7 +93,7 @@ In this case you can benefit from OpenMW's normal-mapping support by using these
This means that you will have to drop the bump-mapping references from the model and sometimes rename the texture.
MGE XE normal-mapping
-***************************************
+*********************
**Conversion difficulty:**
*Easy*
@@ -169,7 +169,7 @@ depending on a few circumstances. In this tutorial, we will look at a very easy,
although in some cases a bit time-consuming, example.
Tutorial - Morrowind, Part 1
-**********************
+****************************
We will be converting a quite popular texture replacer of the Hlaalu architecture, namely Lougian's `Hlaalu Bump mapped`_.
Since this is just a texture pack and not a model replacer,
@@ -201,7 +201,7 @@ We ignored those model files since they are not needed with OpenMW. In this tuto
we will convert a mod that includes new, custom-made models. In other words, we cannot just ignore those files this time.
Tutorial - Morrowind, Part 2
-**********************
+****************************
The sacks included in Apel's `Various Things - Sacks`_ come in two versions – without bump-mapping, and with bump-mapping.
Since we want the glory of normal-mapping in our OpenMW setup, we will go with the bump-mapped version.
diff --git a/extern/CMakeLists.txt b/extern/CMakeLists.txt
index 59d3d15176..0bfdc4c233 100644
--- a/extern/CMakeLists.txt
+++ b/extern/CMakeLists.txt
@@ -208,3 +208,5 @@ if (NOT OPENMW_USE_SYSTEM_SQLITE3)
set(SQLite3_INCLUDE_DIR ${sqlite3_SOURCE_DIR}/ PARENT_SCOPE)
set(SQLite3_LIBRARY sqlite3 PARENT_SCOPE)
endif()
+
+add_subdirectory(smhasher)
diff --git a/extern/i18n.lua/CMakeLists.txt b/extern/i18n.lua/CMakeLists.txt
new file mode 100644
index 0000000000..1f7a71a2c2
--- /dev/null
+++ b/extern/i18n.lua/CMakeLists.txt
@@ -0,0 +1,17 @@
+if (NOT DEFINED OPENMW_RESOURCES_ROOT)
+ return()
+endif()
+
+# Copy resource files into the build directory
+set(SDIR ${CMAKE_CURRENT_SOURCE_DIR})
+set(DDIRRELATIVE resources/lua_libs/i18n)
+
+set(I18N_LUA_FILES
+ i18n/init.lua
+ i18n/interpolate.lua
+ i18n/plural.lua
+ i18n/variants.lua
+ i18n/version.lua
+)
+
+copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "${I18N_LUA_FILES}")
diff --git a/extern/i18n.lua/LICENSE b/extern/i18n.lua/LICENSE
new file mode 100644
index 0000000000..ddf484685b
--- /dev/null
+++ b/extern/i18n.lua/LICENSE
@@ -0,0 +1,22 @@
+MIT License Terms
+=================
+
+Copyright (c) 2012 Enrique García Cota.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/extern/i18n.lua/README.md b/extern/i18n.lua/README.md
new file mode 100644
index 0000000000..8b3271c321
--- /dev/null
+++ b/extern/i18n.lua/README.md
@@ -0,0 +1,164 @@
+i18n.lua
+========
+
+[![Build Status](https://travis-ci.org/kikito/i18n.lua.png?branch=master)](https://travis-ci.org/kikito/i18n.lua)
+
+A very complete i18n lib for Lua
+
+Description
+===========
+
+``` lua
+i18n = require 'i18n'
+
+-- loading stuff
+i18n.set('en.welcome', 'welcome to this program')
+i18n.load({
+ en = {
+ good_bye = "good-bye!",
+ age_msg = "your age is %{age}.",
+ phone_msg = {
+ one = "you have one new message.",
+ other = "you have %{count} new messages."
+ }
+ }
+})
+i18n.loadFile('path/to/your/project/i18n/de.lua') -- load German language file
+i18n.loadFile('path/to/your/project/i18n/fr.lua') -- load French language file
+… -- section 'using language files' below describes structure of files
+
+-- setting the translation context
+i18n.setLocale('en') -- English is the default locale anyway
+
+-- getting translations
+i18n.translate('welcome') -- Welcome to this program
+i18n('welcome') -- Welcome to this program
+i18n('age_msg', {age = 18}) -- Your age is 18.
+i18n('phone_msg', {count = 1}) -- You have one new message.
+i18n('phone_msg', {count = 2}) -- You have 2 new messages.
+i18n('good_bye') -- Good-bye!
+
+```
+
+Interpolation
+=============
+
+You can interpolate variables in 3 different ways:
+
+``` lua
+-- the most usual one
+i18n.set('variables', 'Interpolating variables: %{name} %{age}')
+i18n('variables', {name='john', 'age'=10}) -- Interpolating variables: john 10
+
+i18n.set('lua', 'Traditional Lua way: %d %s')
+i18n('lua', {1, 'message'}) -- Traditional Lua way: 1 message
+
+i18n.set('combined', 'Combined: %<name>.q %<age>.d %<age>.o')
+i18n('combined', {name='john', 'age'=10}) -- Combined: john 10 12k
+```
+
+
+
+Pluralization
+=============
+
+This lib implements the [unicode.org plural rules](http://cldr.unicode.org/index/cldr-spec/plural-rules). Just set the locale you want to use and it will deduce the appropiate pluralization rules:
+
+``` lua
+i18n = require 'i18n'
+
+i18n.load({
+ en = {
+ msg = {
+ one = "one message",
+ other = "%{count} messages"
+ }
+ },
+ ru = {
+ msg = {
+ one = "1 сообщение",
+ few = "%{count} сообщения",
+ many = "%{count} сообщений",
+ other = "%{count} сообщения"
+ }
+ }
+})
+
+i18n('msg', {count = 1}) -- one message
+i18n.setLocale('ru')
+i18n('msg', {count = 5}) -- 5 сообщений
+```
+
+The appropiate rule is chosen by finding the 'root' of the locale used: for example if the current locale is 'fr-CA', the 'fr' rules will be applied.
+
+If the provided functions are not enough (i.e. invented languages) it's possible to specify a custom pluralization function in the second parameter of setLocale. This function must return 'one', 'few', 'other', etc given a number.
+
+Fallbacks
+=========
+
+When a value is not found, the lib has several fallback mechanisms:
+
+* First, it will look in the current locale's parents. For example, if the locale was set to 'en-US' and the key 'msg' was not found there, it will be looked over in 'en'.
+* Second, if the value is not found in the locale ancestry, a 'fallback locale' (by default: 'en') can be used. If the fallback locale has any parents, they will be looked over too.
+* Third, if all the locales have failed, but there is a param called 'default' on the provided data, it will be used.
+* Otherwise the translation will return nil.
+
+The parents of a locale are found by splitting the locale by its hyphens. Other separation characters (spaces, underscores, etc) are not supported.
+
+Using language files
+====================
+
+It might be a good idea to store each translation in a different file. This is supported via the 'i18n.loadFile' directive:
+
+``` lua
+…
+i18n.loadFile('path/to/your/project/i18n/de.lua') -- German translation
+i18n.loadFile('path/to/your/project/i18n/en.lua') -- English translation
+i18n.loadFile('path/to/your/project/i18n/fr.lua') -- French translation
+…
+```
+
+The German language file 'de.lua' should read:
+
+``` lua
+return {
+ de = {
+ good_bye = "Auf Wiedersehen!",
+ age_msg = "Ihr Alter beträgt %{age}.",
+ phone_msg = {
+ one = "Sie haben eine neue Nachricht.",
+ other = "Sie haben %{count} neue Nachrichten."
+ }
+ }
+}
+```
+
+If desired, you can also store all translations in one single file (eg. 'translations.lua'):
+
+``` lua
+return {
+ de = {
+ good_bye = "Auf Wiedersehen!",
+ age_msg = "Ihr Alter beträgt %{age}.",
+ phone_msg = {
+ one = "Sie haben eine neue Nachricht.",
+ other = "Sie haben %{count} neue Nachrichten."
+ }
+ },
+ fr = {
+ good_bye = "Au revoir !",
+ age_msg = "Vous avez %{age} ans.",
+ phone_msg = {
+ one = "Vous avez une noveau message.",
+ other = "Vous avez %{count} noveaux messages."
+ }
+ },
+ …
+}
+```
+
+Specs
+=====
+This project uses [busted](https://github.com/Olivine-Labs/busted) for its specs. If you want to run the specs, you will have to install it first. Then just execute the following from the root inspect folder:
+
+ busted
diff --git a/extern/i18n.lua/i18n/init.lua b/extern/i18n.lua/i18n/init.lua
new file mode 100644
index 0000000000..6bcccd0572
--- /dev/null
+++ b/extern/i18n.lua/i18n/init.lua
@@ -0,0 +1,188 @@
+local i18n = {}
+
+local store
+local locale
+local pluralizeFunction
+local defaultLocale = 'en'
+local fallbackLocale = defaultLocale
+
+local currentFilePath = (...):gsub("%.init$","")
+
+local plural = require(currentFilePath .. '.plural')
+local interpolate = require(currentFilePath .. '.interpolate')
+local variants = require(currentFilePath .. '.variants')
+local version = require(currentFilePath .. '.version')
+
+i18n.plural, i18n.interpolate, i18n.variants, i18n.version, i18n._VERSION =
+ plural, interpolate, variants, version, version
+
+-- private stuff
+
+local function dotSplit(str)
+ local fields, length = {},0
+ str:gsub("[^%.]+", function(c)
+ length = length + 1
+ fields[length] = c
+ end)
+ return fields, length
+end
+
+local function isPluralTable(t)
+ return type(t) == 'table' and type(t.other) == 'string'
+end
+
+local function isPresent(str)
+ return type(str) == 'string' and #str > 0
+end
+
+local function assertPresent(functionName, paramName, value)
+ if isPresent(value) then return end
+
+ local msg = "i18n.%s requires a non-empty string on its %s. Got %s (a %s value)."
+ error(msg:format(functionName, paramName, tostring(value), type(value)))
+end
+
+local function assertPresentOrPlural(functionName, paramName, value)
+ if isPresent(value) or isPluralTable(value) then return end
+
+ local msg = "i18n.%s requires a non-empty string or plural-form table on its %s. Got %s (a %s value)."
+ error(msg:format(functionName, paramName, tostring(value), type(value)))
+end
+
+local function assertPresentOrTable(functionName, paramName, value)
+ if isPresent(value) or type(value) == 'table' then return end
+
+ local msg = "i18n.%s requires a non-empty string or table on its %s. Got %s (a %s value)."
+ error(msg:format(functionName, paramName, tostring(value), type(value)))
+end
+
+local function assertFunctionOrNil(functionName, paramName, value)
+ if value == nil or type(value) == 'function' then return end
+
+ local msg = "i18n.%s requires a function (or nil) on param %s. Got %s (a %s value)."
+ error(msg:format(functionName, paramName, tostring(value), type(value)))
+end
+
+local function defaultPluralizeFunction(count)
+ return plural.get(variants.root(i18n.getLocale()), count)
+end
+
+local function pluralize(t, data)
+ assertPresentOrPlural('interpolatePluralTable', 't', t)
+ data = data or {}
+ local count = data.count or 1
+ local plural_form = pluralizeFunction(count)
+ return t[plural_form]
+end
+
+local function treatNode(node, data)
+ if type(node) == 'string' then
+ return interpolate(node, data)
+ elseif isPluralTable(node) then
+ return interpolate(pluralize(node, data), data)
+ end
+ return node
+end
+
+local function recursiveLoad(currentContext, data)
+ local composedKey
+ for k,v in pairs(data) do
+ composedKey = (currentContext and (currentContext .. '.') or "") .. tostring(k)
+ assertPresent('load', composedKey, k)
+ assertPresentOrTable('load', composedKey, v)
+ if type(v) == 'string' then
+ i18n.set(composedKey, v)
+ else
+ recursiveLoad(composedKey, v)
+ end
+ end
+end
+
+local function localizedTranslate(key, loc, data)
+ local path, length = dotSplit(loc .. "." .. key)
+ local node = store
+
+ for i=1, length do
+ node = node[path[i]]
+ if not node then return nil end
+ end
+
+ return treatNode(node, data)
+end
+
+-- public interface
+
+function i18n.set(key, value)
+ assertPresent('set', 'key', key)
+ assertPresentOrPlural('set', 'value', value)
+
+ local path, length = dotSplit(key)
+ local node = store
+
+ for i=1, length-1 do
+ key = path[i]
+ node[key] = node[key] or {}
+ node = node[key]
+ end
+
+ local lastKey = path[length]
+ node[lastKey] = value
+end
+
+function i18n.translate(key, data)
+ assertPresent('translate', 'key', key)
+
+ data = data or {}
+ local usedLocale = data.locale or locale
+
+ local fallbacks = variants.fallbacks(usedLocale, fallbackLocale)
+ for i=1, #fallbacks do
+ local value = localizedTranslate(key, fallbacks[i], data)
+ if value then return value end
+ end
+
+ return data.default
+end
+
+function i18n.setLocale(newLocale, newPluralizeFunction)
+ assertPresent('setLocale', 'newLocale', newLocale)
+ assertFunctionOrNil('setLocale', 'newPluralizeFunction', newPluralizeFunction)
+ locale = newLocale
+ pluralizeFunction = newPluralizeFunction or defaultPluralizeFunction
+end
+
+function i18n.setFallbackLocale(newFallbackLocale)
+ assertPresent('setFallbackLocale', 'newFallbackLocale', newFallbackLocale)
+ fallbackLocale = newFallbackLocale
+end
+
+function i18n.getFallbackLocale()
+ return fallbackLocale
+end
+
+function i18n.getLocale()
+ return locale
+end
+
+function i18n.reset()
+ store = {}
+ plural.reset()
+ i18n.setLocale(defaultLocale)
+ i18n.setFallbackLocale(defaultLocale)
+end
+
+function i18n.load(data)
+ recursiveLoad(nil, data)
+end
+
+function i18n.loadFile(path)
+ local chunk = assert(loadfile(path))
+ local data = chunk()
+ i18n.load(data)
+end
+
+setmetatable(i18n, {__call = function(_, ...) return i18n.translate(...) end})
+
+i18n.reset()
+
+return i18n
diff --git a/extern/i18n.lua/i18n/interpolate.lua b/extern/i18n.lua/i18n/interpolate.lua
new file mode 100644
index 0000000000..c6bb242f05
--- /dev/null
+++ b/extern/i18n.lua/i18n/interpolate.lua
@@ -0,0 +1,60 @@
+local unpack = unpack or table.unpack -- lua 5.2 compat
+
+local FORMAT_CHARS = { c=1, d=1, E=1, e=1, f=1, g=1, G=1, i=1, o=1, u=1, X=1, x=1, s=1, q=1, ['%']=1 }
+
+-- matches a string of type %{age}
+local function interpolateValue(string, variables)
+ return string:gsub("(.?)%%{%s*(.-)%s*}",
+ function (previous, key)
+ if previous == "%" then
+ return
+ else
+ return previous .. tostring(variables[key])
+ end
+ end)
+end
+
+-- matches a string of type %<age>.d
+local function interpolateField(string, variables)
+ return string:gsub("(.?)%%<%s*(.-)%s*>%.([cdEefgGiouXxsq])",
+ function (previous, key, format)
+ if previous == "%" then
+ return
+ else
+ return previous .. string.format("%" .. format, variables[key] or "nil")
+ end
+ end)
+end
+
+local function escapePercentages(string)
+ return string:gsub("(%%)(.?)", function(_, char)
+ if FORMAT_CHARS[char] then
+ return "%" .. char
+ else
+ return "%%" .. char
+ end
+ end)
+end
+
+local function unescapePercentages(string)
+ return string:gsub("(%%%%)(.?)", function(_, char)
+ if FORMAT_CHARS[char] then
+ return "%" .. char
+ else
+ return "%%" .. char
+ end
+ end)
+end
+
+local function interpolate(pattern, variables)
+ variables = variables or {}
+ local result = pattern
+ result = interpolateValue(result, variables)
+ result = interpolateField(result, variables)
+ result = escapePercentages(result)
+ result = string.format(result, unpack(variables))
+ result = unescapePercentages(result)
+ return result
+end
+
+return interpolate
diff --git a/extern/i18n.lua/i18n/plural.lua b/extern/i18n.lua/i18n/plural.lua
new file mode 100644
index 0000000000..bb99804ee8
--- /dev/null
+++ b/extern/i18n.lua/i18n/plural.lua
@@ -0,0 +1,280 @@
+local plural = {}
+local defaultFunction = nil
+-- helper functions
+
+local function assertPresentString(functionName, paramName, value)
+ if type(value) ~= 'string' or #value == 0 then
+ local msg = "Expected param %s of function %s to be a string, but got %s (a value of type %s) instead"
+ error(msg:format(paramName, functionName, tostring(value), type(value)))
+ end
+end
+
+local function assertNumber(functionName, paramName, value)
+ if type(value) ~= 'number' then
+ local msg = "Expected param %s of function %s to be a number, but got %s (a value of type %s) instead"
+ error(msg:format(paramName, functionName, tostring(value), type(value)))
+ end
+end
+
+-- transforms "foo bar baz" into {'foo','bar','baz'}
+local function words(str)
+ local result, length = {}, 0
+ str:gsub("%S+", function(word)
+ length = length + 1
+ result[length] = word
+ end)
+ return result
+end
+
+local function isInteger(n)
+ return n == math.floor(n)
+end
+
+local function between(value, min, max)
+ return value >= min and value <= max
+end
+
+local function inside(v, list)
+ for i=1, #list do
+ if v == list[i] then return true end
+ end
+ return false
+end
+
+
+-- pluralization functions
+
+local pluralization = {}
+
+local f1 = function(n)
+ return n == 1 and "one" or "other"
+end
+pluralization[f1] = words([[
+ af asa bem bez bg bn brx ca cgg chr da de dv ee el
+ en eo es et eu fi fo fur fy gl gsw gu ha haw he is
+ it jmc kaj kcg kk kl ksb ku lb lg mas ml mn mr nah
+ nb nd ne nl nn no nr ny nyn om or pa pap ps pt rm
+ rof rwk saq seh sn so sq ss ssy st sv sw syr ta te
+ teo tig tk tn ts ur ve vun wae xh xog zu
+]])
+
+local f2 = function(n)
+ return (n == 0 or n == 1) and "one" or "other"
+end
+pluralization[f2] = words("ak am bh fil guw hi ln mg nso ti tl wa")
+
+local f3 = function(n)
+ if not isInteger(n) then return 'other' end
+ return (n == 0 and "zero") or
+ (n == 1 and "one") or
+ (n == 2 and "two") or
+ (between(n % 100, 3, 10) and "few") or
+ (between(n % 100, 11, 99) and "many") or
+ "other"
+end
+pluralization[f3] = {'ar'}
+
+local f4 = function()
+ return "other"
+end
+pluralization[f4] = words([[
+ az bm bo dz fa hu id ig ii ja jv ka kde kea km kn
+ ko lo ms my root sah ses sg th to tr vi wo yo zh
+]])
+
+local f5 = function(n)
+ if not isInteger(n) then return 'other' end
+ local n_10, n_100 = n % 10, n % 100
+ return (n_10 == 1 and n_100 ~= 11 and 'one') or
+ (between(n_10, 2, 4) and not between(n_100, 12, 14) and 'few') or
+ ((n_10 == 0 or between(n_10, 5, 9) or between(n_100, 11, 14)) and 'many') or
+ 'other'
+end
+pluralization[f5] = words('be bs hr ru sh sr uk')
+
+local f6 = function(n)
+ if not isInteger(n) then return 'other' end
+ local n_10, n_100 = n % 10, n % 100
+ return (n_10 == 1 and not inside(n_100, {11,71,91}) and 'one') or
+ (n_10 == 2 and not inside(n_100, {12,72,92}) and 'two') or
+ (inside(n_10, {3,4,9}) and
+ not between(n_100, 10, 19) and
+ not between(n_100, 70, 79) and
+ not between(n_100, 90, 99)
+ and 'few') or
+ (n ~= 0 and n % 1000000 == 0 and 'many') or
+ 'other'
+end
+pluralization[f6] = {'br'}
+
+local f7 = function(n)
+ return (n == 1 and 'one') or
+ ((n == 2 or n == 3 or n == 4) and 'few') or
+ 'other'
+end
+pluralization[f7] = {'cz', 'sk'}
+
+local f8 = function(n)
+ return (n == 0 and 'zero') or
+ (n == 1 and 'one') or
+ (n == 2 and 'two') or
+ (n == 3 and 'few') or
+ (n == 6 and 'many') or
+ 'other'
+end
+pluralization[f8] = {'cy'}
+
+local f9 = function(n)
+ return (n >= 0 and n < 2 and 'one') or
+ 'other'
+end
+pluralization[f9] = {'ff', 'fr', 'kab'}
+
+local f10 = function(n)
+ return (n == 1 and 'one') or
+ (n == 2 and 'two') or
+ ((n == 3 or n == 4 or n == 5 or n == 6) and 'few') or
+ ((n == 7 or n == 8 or n == 9 or n == 10) and 'many') or
+ 'other'
+end
+pluralization[f10] = {'ga'}
+
+local f11 = function(n)
+ return ((n == 1 or n == 11) and 'one') or
+ ((n == 2 or n == 12) and 'two') or
+ (isInteger(n) and (between(n, 3, 10) or between(n, 13, 19)) and 'few') or
+ 'other'
+end
+pluralization[f11] = {'gd'}
+
+local f12 = function(n)
+ local n_10 = n % 10
+ return ((n_10 == 1 or n_10 == 2 or n % 20 == 0) and 'one') or
+ 'other'
+end
+pluralization[f12] = {'gv'}
+
+local f13 = function(n)
+ return (n == 1 and 'one') or
+ (n == 2 and 'two') or
+ 'other'
+end
+pluralization[f13] = words('iu kw naq se sma smi smj smn sms')
+
+local f14 = function(n)
+ return (n == 0 and 'zero') or
+ (n == 1 and 'one') or
+ 'other'
+end
+pluralization[f14] = {'ksh'}
+
+local f15 = function(n)
+ return (n == 0 and 'zero') or
+ (n > 0 and n < 2 and 'one') or
+ 'other'
+end
+pluralization[f15] = {'lag'}
+
+local f16 = function(n)
+ if not isInteger(n) then return 'other' end
+ if between(n % 100, 11, 19) then return 'other' end
+ local n_10 = n % 10
+ return (n_10 == 1 and 'one') or
+ (between(n_10, 2, 9) and 'few') or
+ 'other'
+end
+pluralization[f16] = {'lt'}
+
+local f17 = function(n)
+ return (n == 0 and 'zero') or
+ ((n % 10 == 1 and n % 100 ~= 11) and 'one') or
+ 'other'
+end
+pluralization[f17] = {'lv'}
+
+local f18 = function(n)
+ return((n % 10 == 1 and n ~= 11) and 'one') or
+ 'other'
+end
+pluralization[f18] = {'mk'}
+
+local f19 = function(n)
+ return (n == 1 and 'one') or
+ ((n == 0 or
+ (n ~= 1 and isInteger(n) and between(n % 100, 1, 19)))
+ and 'few') or
+ 'other'
+end
+pluralization[f19] = {'mo', 'ro'}
+
+local f20 = function(n)
+ if n == 1 then return 'one' end
+ if not isInteger(n) then return 'other' end
+ local n_100 = n % 100
+ return ((n == 0 or between(n_100, 2, 10)) and 'few') or
+ (between(n_100, 11, 19) and 'many') or
+ 'other'
+end
+pluralization[f20] = {'mt'}
+
+local f21 = function(n)
+ if n == 1 then return 'one' end
+ if not isInteger(n) then return 'other' end
+ local n_10, n_100 = n % 10, n % 100
+
+ return ((between(n_10, 2, 4) and not between(n_100, 12, 14)) and 'few') or
+ ((n_10 == 0 or n_10 == 1 or between(n_10, 5, 9) or between(n_100, 12, 14)) and 'many') or
+ 'other'
+end
+pluralization[f21] = {'pl'}
+
+local f22 = function(n)
+ return (n == 0 or n == 1) and 'one' or
+ 'other'
+end
+pluralization[f22] = {'shi'}
+
+local f23 = function(n)
+ local n_100 = n % 100
+ return (n_100 == 1 and 'one') or
+ (n_100 == 2 and 'two') or
+ ((n_100 == 3 or n_100 == 4) and 'few') or
+ 'other'
+end
+pluralization[f23] = {'sl'}
+
+local f24 = function(n)
+ return (isInteger(n) and (n == 0 or n == 1 or between(n, 11, 99)) and 'one')
+ or 'other'
+end
+pluralization[f24] = {'tzm'}
+
+local pluralizationFunctions = {}
+for f,locales in pairs(pluralization) do
+ for _,locale in ipairs(locales) do
+ pluralizationFunctions[locale] = f
+ end
+end
+
+-- public interface
+
+function plural.get(locale, n)
+ assertPresentString('i18n.plural.get', 'locale', locale)
+ assertNumber('i18n.plural.get', 'n', n)
+
+ local f = pluralizationFunctions[locale] or defaultFunction
+
+ return f(math.abs(n))
+end
+
+function plural.setDefaultFunction(f)
+ defaultFunction = f
+end
+
+function plural.reset()
+ defaultFunction = pluralizationFunctions['en']
+end
+
+plural.reset()
+
+return plural
diff --git a/extern/i18n.lua/i18n/variants.lua b/extern/i18n.lua/i18n/variants.lua
new file mode 100644
index 0000000000..0cfad42f6c
--- /dev/null
+++ b/extern/i18n.lua/i18n/variants.lua
@@ -0,0 +1,49 @@
+local variants = {}
+
+local function reverse(arr, length)
+ local result = {}
+ for i=1, length do result[i] = arr[length-i+1] end
+ return result, length
+end
+
+local function concat(arr1, len1, arr2, len2)
+ for i = 1, len2 do
+ arr1[len1 + i] = arr2[i]
+ end
+ return arr1, len1 + len2
+end
+
+function variants.ancestry(locale)
+ local result, length, accum = {},0,nil
+ locale:gsub("[^%-]+", function(c)
+ length = length + 1
+ accum = accum and (accum .. '-' .. c) or c
+ result[length] = accum
+ end)
+ return reverse(result, length)
+end
+
+function variants.isParent(parent, child)
+ return not not child:match("^".. parent .. "%-")
+end
+
+function variants.root(locale)
+ return locale:match("[^%-]+")
+end
+
+function variants.fallbacks(locale, fallbackLocale)
+ if locale == fallbackLocale or
+ variants.isParent(fallbackLocale, locale) then
+ return variants.ancestry(locale)
+ end
+ if variants.isParent(locale, fallbackLocale) then
+ return variants.ancestry(fallbackLocale)
+ end
+
+ local ancestry1, length1 = variants.ancestry(locale)
+ local ancestry2, length2 = variants.ancestry(fallbackLocale)
+
+ return concat(ancestry1, length1, ancestry2, length2)
+end
+
+return variants
diff --git a/extern/i18n.lua/i18n/version.lua b/extern/i18n.lua/i18n/version.lua
new file mode 100644
index 0000000000..eb788884ac
--- /dev/null
+++ b/extern/i18n.lua/i18n/version.lua
@@ -0,0 +1 @@
+return '0.9.2'
diff --git a/extern/osg-ffmpeg-videoplayer/videostate.cpp b/extern/osg-ffmpeg-videoplayer/videostate.cpp
index 7fcad33697..9b07ece14d 100644
--- a/extern/osg-ffmpeg-videoplayer/videostate.cpp
+++ b/extern/osg-ffmpeg-videoplayer/videostate.cpp
@@ -4,10 +4,12 @@
#include <cassert>
#include <cstddef>
#include <iostream>
+#include <memory>
#include <thread>
#include <chrono>
#include <osg/Texture2D>
+#include <utility>
#if defined(_MSC_VER)
#pragma warning (push)
@@ -62,20 +64,20 @@ namespace
av_frame_free(&frame);
}
};
-
- template<class T>
- struct AVFree
- {
- void operator()(T* frame) const
- {
- av_free(&frame);
- }
- };
}
namespace Video
{
+struct PacketListFree
+{
+ void operator()(Video::PacketList* list) const
+ {
+ av_packet_free(&list->pkt);
+ av_free(&list);
+ }
+};
+
VideoState::VideoState()
: mAudioFactory(nullptr)
, format_ctx(nullptr)
@@ -95,7 +97,7 @@ VideoState::VideoState()
{
mFlushPktData = flush_pkt.data;
-// This is not needed anymore above FFMpeg version 4.0
+// This is not needed any more above FFMpeg version 4.0
#if LIBAVCODEC_VERSION_INT < 3805796
av_register_all();
#endif
@@ -114,25 +116,29 @@ void VideoState::setAudioFactory(MovieAudioFactory *factory)
void PacketQueue::put(AVPacket *pkt)
{
- std::unique_ptr<AVPacketList, AVFree<AVPacketList>> pkt1(static_cast<AVPacketList*>(av_malloc(sizeof(AVPacketList))));
+ std::unique_ptr<PacketList, PacketListFree> pkt1(static_cast<PacketList*>(av_malloc(sizeof(PacketList))));
if(!pkt1) throw std::bad_alloc();
+
if(pkt == &flush_pkt)
- pkt1->pkt = *pkt;
+ pkt1->pkt = pkt;
else
- av_packet_move_ref(&pkt1->pkt, pkt);
+ {
+ pkt1->pkt = av_packet_alloc();
+ av_packet_move_ref(pkt1->pkt, pkt);
+ }
pkt1->next = nullptr;
std::lock_guard<std::mutex> lock(this->mutex);
- AVPacketList* ptr = pkt1.release();
+ PacketList* ptr = pkt1.release();
if(!last_pkt)
this->first_pkt = ptr;
else
this->last_pkt->next = ptr;
this->last_pkt = ptr;
this->nb_packets++;
- this->size += ptr->pkt.size;
+ this->size += ptr->pkt->size;
this->cond.notify_one();
}
@@ -141,17 +147,17 @@ int PacketQueue::get(AVPacket *pkt, VideoState *is)
std::unique_lock<std::mutex> lock(this->mutex);
while(!is->mQuit)
{
- AVPacketList *pkt1 = this->first_pkt;
+ PacketList *pkt1 = this->first_pkt;
if(pkt1)
{
this->first_pkt = pkt1->next;
if(!this->first_pkt)
this->last_pkt = nullptr;
this->nb_packets--;
- this->size -= pkt1->pkt.size;
+ this->size -= pkt1->pkt->size;
av_packet_unref(pkt);
- av_packet_move_ref(pkt, &pkt1->pkt);
+ av_packet_move_ref(pkt, pkt1->pkt);
av_free(pkt1);
return 1;
@@ -173,14 +179,14 @@ void PacketQueue::flush()
void PacketQueue::clear()
{
- AVPacketList *pkt, *pkt1;
+ PacketList *pkt, *pkt1;
std::lock_guard<std::mutex> lock(this->mutex);
for(pkt = this->first_pkt; pkt != nullptr; pkt = pkt1)
{
pkt1 = pkt->next;
- if (pkt->pkt.data != flush_pkt.data)
- av_packet_unref(&pkt->pkt);
+ if (pkt->pkt->data != flush_pkt.data)
+ av_packet_unref(pkt->pkt);
av_freep(&pkt);
}
this->last_pkt = nullptr;
@@ -304,7 +310,7 @@ void VideoState::video_refresh()
VideoPicture* vp = &this->pictq[this->pictq_rindex];
this->video_display(vp);
- this->pictq_rindex = (pictq_rindex+1) % VIDEO_PICTURE_ARRAY_SIZE;
+ this->pictq_rindex = (this->pictq_rindex+1) % this->pictq.size();
this->frame_last_pts = vp->pts;
this->pictq_size--;
this->pictq_cond.notify_one();
@@ -320,13 +326,13 @@ void VideoState::video_refresh()
int i=0;
for (; i<this->pictq_size-1; ++i)
{
- if (this->pictq[pictq_rindex].pts + threshold <= this->get_master_clock())
- this->pictq_rindex = (this->pictq_rindex+1) % VIDEO_PICTURE_ARRAY_SIZE; // not enough time to show this picture
+ if (this->pictq[this->pictq_rindex].pts + threshold <= this->get_master_clock())
+ this->pictq_rindex = (this->pictq_rindex+1) % this->pictq.size(); // not enough time to show this picture
else
break;
}
- assert (this->pictq_rindex < VIDEO_PICTURE_ARRAY_SIZE);
+ assert (this->pictq_rindex < this->pictq.size());
VideoPicture* vp = &this->pictq[this->pictq_rindex];
this->video_display(vp);
@@ -336,7 +342,7 @@ void VideoState::video_refresh()
this->pictq_size -= i;
// update queue for next picture
this->pictq_size--;
- this->pictq_rindex = (this->pictq_rindex+1) % VIDEO_PICTURE_ARRAY_SIZE;
+ this->pictq_rindex = (this->pictq_rindex+1) % this->pictq.size();
this->pictq_cond.notify_one();
}
}
@@ -386,7 +392,7 @@ int VideoState::queue_picture(const AVFrame &pFrame, double pts)
0, this->video_ctx->height, vp->rgbaFrame->data, vp->rgbaFrame->linesize);
// now we inform our display thread that we have a pic ready
- this->pictq_windex = (this->pictq_windex+1) % VIDEO_PICTURE_ARRAY_SIZE;
+ this->pictq_windex = (this->pictq_windex+1) % this->pictq.size();
this->pictq_size++;
return 0;
@@ -415,7 +421,7 @@ double VideoState::synchronize_video(const AVFrame &src_frame, double pts)
class VideoThread
{
public:
- VideoThread(VideoState* self)
+ explicit VideoThread(VideoState* self)
: mVideoState(self)
, mThread([this]
{
@@ -439,9 +445,8 @@ public:
void run()
{
VideoState* self = mVideoState;
- AVPacket packetData;
- av_init_packet(&packetData);
- std::unique_ptr<AVPacket, AVPacketUnref> packet(&packetData);
+ AVPacket* packetData = av_packet_alloc();
+ std::unique_ptr<AVPacket, AVPacketUnref> packet(packetData);
std::unique_ptr<AVFrame, AVFrameFree> pFrame{av_frame_alloc()};
while(self->videoq.get(packet.get(), self) >= 0)
@@ -491,7 +496,7 @@ private:
class ParseThread
{
public:
- ParseThread(VideoState* self)
+ explicit ParseThread(VideoState* self)
: mVideoState(self)
, mThread([this] { run(); })
{
@@ -507,9 +512,8 @@ public:
VideoState* self = mVideoState;
AVFormatContext *pFormatCtx = self->format_ctx;
- AVPacket packetData;
- av_init_packet(&packetData);
- std::unique_ptr<AVPacket, AVPacketUnref> packet(&packetData);
+ AVPacket* packetData = av_packet_alloc();
+ std::unique_ptr<AVPacket, AVPacketUnref> packet(packetData);
try
{
@@ -632,13 +636,11 @@ bool VideoState::update()
int VideoState::stream_open(int stream_index, AVFormatContext *pFormatCtx)
{
- const AVCodec *codec;
-
if(stream_index < 0 || stream_index >= static_cast<int>(pFormatCtx->nb_streams))
return -1;
// Get a pointer to the codec context for the video stream
- codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codecpar->codec_id);
+ const AVCodec *codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codecpar->codec_id);
if(!codec)
{
fprintf(stderr, "Unsupported codec!\n");
@@ -654,7 +656,7 @@ int VideoState::stream_open(int stream_index, AVFormatContext *pFormatCtx)
this->audio_ctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(this->audio_ctx, pFormatCtx->streams[stream_index]->codecpar);
-// This is not needed anymore above FFMpeg version 4.0
+// This is not needed any more above FFMpeg version 4.0
#if LIBAVCODEC_VERSION_INT < 3805796
av_codec_set_pkt_timebase(this->audio_ctx, pFormatCtx->streams[stream_index]->time_base);
#endif
@@ -674,7 +676,7 @@ int VideoState::stream_open(int stream_index, AVFormatContext *pFormatCtx)
}
mAudioDecoder = mAudioFactory->createDecoder(this);
- if (!mAudioDecoder.get())
+ if (!mAudioDecoder)
{
std::cerr << "Failed to create audio decoder, can not play audio stream" << std::endl;
avcodec_free_context(&this->audio_ctx);
@@ -691,7 +693,7 @@ int VideoState::stream_open(int stream_index, AVFormatContext *pFormatCtx)
this->video_ctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(this->video_ctx, pFormatCtx->streams[stream_index]->codecpar);
-// This is not needed anymore above FFMpeg version 4.0
+// This is not needed any more above FFMpeg version 4.0
#if LIBAVCODEC_VERSION_INT < 3805796
av_codec_set_pkt_timebase(this->video_ctx, pFormatCtx->streams[stream_index]->time_base);
#endif
@@ -702,7 +704,7 @@ int VideoState::stream_open(int stream_index, AVFormatContext *pFormatCtx)
return -1;
}
- this->video_thread.reset(new VideoThread(this));
+ this->video_thread = std::make_unique<VideoThread>(this);
break;
default:
@@ -721,8 +723,8 @@ void VideoState::init(std::shared_ptr<std::istream> inputstream, const std::stri
this->av_sync_type = AV_SYNC_DEFAULT;
this->mQuit = false;
- this->stream = inputstream;
- if(!this->stream.get())
+ this->stream = std::move(inputstream);
+ if(!this->stream)
throw std::runtime_error("Failed to open video resource");
AVIOContext *ioCtx = avio_alloc_context(nullptr, 0, 0, this, istream_read, istream_write, istream_seek);
@@ -789,7 +791,7 @@ void VideoState::init(std::shared_ptr<std::istream> inputstream, const std::stri
}
- this->parse_thread.reset(new ParseThread(this));
+ this->parse_thread = std::make_unique<ParseThread>(this);
}
void VideoState::deinit()
@@ -801,11 +803,11 @@ void VideoState::deinit()
mAudioDecoder.reset();
- if (this->parse_thread.get())
+ if (this->parse_thread)
{
this->parse_thread.reset();
}
- if (this->video_thread.get())
+ if (this->video_thread)
{
this->video_thread.reset();
}
@@ -850,9 +852,9 @@ void VideoState::deinit()
mTexture = nullptr;
}
- // Dellocate RGBA frame queue.
- for (std::size_t i = 0; i < VIDEO_PICTURE_ARRAY_SIZE; ++i)
- this->pictq[i].rgbaFrame = nullptr;
+ // Deallocate RGBA frame queue.
+ for (auto & i : this->pictq)
+ i.rgbaFrame = nullptr;
}
@@ -870,14 +872,14 @@ double VideoState::get_master_clock()
return this->get_external_clock();
}
-double VideoState::get_video_clock()
+double VideoState::get_video_clock() const
{
return this->frame_last_pts;
}
double VideoState::get_audio_clock()
{
- if (!mAudioDecoder.get())
+ if (!mAudioDecoder)
return 0.0;
return mAudioDecoder->getAudioClock();
}
@@ -896,7 +898,7 @@ void VideoState::seekTo(double time)
mSeekRequested = true;
}
-double VideoState::getDuration()
+double VideoState::getDuration() const
{
return this->format_ctx->duration / 1000000.0;
}
diff --git a/extern/osg-ffmpeg-videoplayer/videostate.hpp b/extern/osg-ffmpeg-videoplayer/videostate.hpp
index 32a772f299..d1592bd910 100644
--- a/extern/osg-ffmpeg-videoplayer/videostate.hpp
+++ b/extern/osg-ffmpeg-videoplayer/videostate.hpp
@@ -1,8 +1,9 @@
#ifndef VIDEOPLAYER_VIDEOSTATE_H
#define VIDEOPLAYER_VIDEOSTATE_H
-#include <stdint.h>
+#include <cstdint>
#include <atomic>
+#include <array>
#include <vector>
#include <memory>
#include <string>
@@ -40,13 +41,10 @@ extern "C"
#include "videodefs.hpp"
#define VIDEO_PICTURE_QUEUE_SIZE 50
-// allocate one extra to make sure we do not overwrite the osg::Image currently set on the texture
-#define VIDEO_PICTURE_ARRAY_SIZE (VIDEO_PICTURE_QUEUE_SIZE+1)
extern "C"
{
struct SwsContext;
- struct AVPacketList;
struct AVPacket;
struct AVFormatContext;
struct AVStream;
@@ -78,6 +76,13 @@ struct ExternalClock
void set(uint64_t time);
};
+class PacketList
+{
+public:
+ AVPacket* pkt = nullptr;
+ PacketList *next = nullptr;
+};
+
struct PacketQueue {
PacketQueue()
: first_pkt(nullptr), last_pkt(nullptr), flushing(false), nb_packets(0), size(0)
@@ -85,7 +90,7 @@ struct PacketQueue {
~PacketQueue()
{ clear(); }
- AVPacketList *first_pkt, *last_pkt;
+ PacketList *first_pkt, *last_pkt;
std::atomic<bool> flushing;
std::atomic<int> nb_packets;
std::atomic<int> size;
@@ -129,7 +134,7 @@ struct VideoState {
void setPaused(bool isPaused);
void seekTo(double time);
- double getDuration();
+ double getDuration() const;
int stream_open(int stream_index, AVFormatContext *pFormatCtx);
@@ -145,7 +150,7 @@ struct VideoState {
double synchronize_video(const AVFrame &src_frame, double pts);
double get_audio_clock();
- double get_video_clock();
+ double get_video_clock() const;
double get_external_clock();
double get_master_clock();
@@ -178,8 +183,9 @@ struct VideoState {
PacketQueue videoq;
SwsContext* sws_context;
int sws_context_w, sws_context_h;
- VideoPicture pictq[VIDEO_PICTURE_ARRAY_SIZE];
- int pictq_size, pictq_rindex, pictq_windex;
+ std::array<VideoPicture, VIDEO_PICTURE_QUEUE_SIZE+1> pictq; // allocate one extra to make sure we do not overwrite the osg::Image currently set on the texture
+ int pictq_size;
+ unsigned long pictq_rindex, pictq_windex;
std::mutex pictq_mutex;
std::condition_variable pictq_cond;
diff --git a/extern/smhasher/CMakeLists.txt b/extern/smhasher/CMakeLists.txt
new file mode 100644
index 0000000000..ee03e6c38e
--- /dev/null
+++ b/extern/smhasher/CMakeLists.txt
@@ -0,0 +1,2 @@
+add_library(smhasher STATIC MurmurHash3.cpp)
+target_include_directories(smhasher INTERFACE .)
diff --git a/extern/smhasher/MurmurHash3.cpp b/extern/smhasher/MurmurHash3.cpp
new file mode 100644
index 0000000000..c8b774bab9
--- /dev/null
+++ b/extern/smhasher/MurmurHash3.cpp
@@ -0,0 +1,152 @@
+//-----------------------------------------------------------------------------
+// MurmurHash3 was written by Austin Appleby, and is placed in the public
+// domain. The author hereby disclaims copyright to this source code.
+
+// Note - The x86 and x64 versions do _not_ produce the same results, as the
+// algorithms are optimized for their respective platforms. You can still
+// compile and run any of them on any platform, but your performance with the
+// non-native version will be less than optimal.
+
+#include "MurmurHash3.h"
+
+#include <cstring>
+
+//-----------------------------------------------------------------------------
+// Platform-specific functions and macros
+
+// Microsoft Visual Studio
+
+#if defined(_MSC_VER)
+
+#define FORCE_INLINE __forceinline
+
+#include <stdlib.h>
+
+#define ROTL64(x,y) _rotl64(x,y)
+
+#define BIG_CONSTANT(x) (x)
+
+// Other compilers
+
+#else // defined(_MSC_VER)
+
+#define FORCE_INLINE inline __attribute__((always_inline))
+
+inline uint64_t rotl64 ( uint64_t x, int8_t r )
+{
+ return (x << r) | (x >> (64 - r));
+}
+
+#define ROTL64(x,y) rotl64(x,y)
+
+#define BIG_CONSTANT(x) (x##LLU)
+
+#endif // !defined(_MSC_VER)
+
+//-----------------------------------------------------------------------------
+// Block read - if your platform needs to do endian-swapping or can only
+// handle aligned reads, do the conversion here
+
+FORCE_INLINE uint64_t getblock64 ( const uint64_t * p, int i )
+{
+ uint64_t result = 0;
+ std::memcpy(&result, p + i, sizeof(result));
+ return result;
+}
+
+//----------
+
+FORCE_INLINE uint64_t fmix64 ( uint64_t k )
+{
+ k ^= k >> 33;
+ k *= BIG_CONSTANT(0xff51afd7ed558ccd);
+ k ^= k >> 33;
+ k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53);
+ k ^= k >> 33;
+
+ return k;
+}
+
+//-----------------------------------------------------------------------------
+
+void MurmurHash3_x64_128 ( const void * key, const int len,
+ const uint64_t * seed, void * out )
+{
+ const uint8_t * data = (const uint8_t*)key;
+ const int nblocks = len / 16;
+
+ uint64_t h1 = seed[0];
+ uint64_t h2 = seed[1];
+
+ const uint64_t c1 = BIG_CONSTANT(0x87c37b91114253d5);
+ const uint64_t c2 = BIG_CONSTANT(0x4cf5ad432745937f);
+
+ //----------
+ // body
+
+ const uint64_t * blocks = (const uint64_t *)(data);
+
+ for(int i = 0; i < nblocks; i++)
+ {
+ uint64_t k1 = getblock64(blocks,i*2+0);
+ uint64_t k2 = getblock64(blocks,i*2+1);
+
+ k1 *= c1; k1 = ROTL64(k1,31); k1 *= c2; h1 ^= k1;
+
+ h1 = ROTL64(h1,27); h1 += h2; h1 = h1*5+0x52dce729;
+
+ k2 *= c2; k2 = ROTL64(k2,33); k2 *= c1; h2 ^= k2;
+
+ h2 = ROTL64(h2,31); h2 += h1; h2 = h2*5+0x38495ab5;
+ }
+
+ //----------
+ // tail
+
+ const uint8_t * tail = (const uint8_t*)(data + nblocks*16);
+
+ uint64_t k1 = 0;
+ uint64_t k2 = 0;
+
+ switch(len & 15)
+ {
+ case 15: k2 ^= ((uint64_t)tail[14]) << 48;
+ case 14: k2 ^= ((uint64_t)tail[13]) << 40;
+ case 13: k2 ^= ((uint64_t)tail[12]) << 32;
+ case 12: k2 ^= ((uint64_t)tail[11]) << 24;
+ case 11: k2 ^= ((uint64_t)tail[10]) << 16;
+ case 10: k2 ^= ((uint64_t)tail[ 9]) << 8;
+ case 9: k2 ^= ((uint64_t)tail[ 8]) << 0;
+ k2 *= c2; k2 = ROTL64(k2,33); k2 *= c1; h2 ^= k2;
+
+ case 8: k1 ^= ((uint64_t)tail[ 7]) << 56;
+ case 7: k1 ^= ((uint64_t)tail[ 6]) << 48;
+ case 6: k1 ^= ((uint64_t)tail[ 5]) << 40;
+ case 5: k1 ^= ((uint64_t)tail[ 4]) << 32;
+ case 4: k1 ^= ((uint64_t)tail[ 3]) << 24;
+ case 3: k1 ^= ((uint64_t)tail[ 2]) << 16;
+ case 2: k1 ^= ((uint64_t)tail[ 1]) << 8;
+ case 1: k1 ^= ((uint64_t)tail[ 0]) << 0;
+ k1 *= c1; k1 = ROTL64(k1,31); k1 *= c2; h1 ^= k1;
+ };
+
+ //----------
+ // finalization
+
+ h1 ^= len; h2 ^= len;
+
+ h1 += h2;
+ h2 += h1;
+
+ h1 = fmix64(h1);
+ h2 = fmix64(h2);
+
+ h1 += h2;
+ h2 += h1;
+
+ ((uint64_t*)out)[0] = h1;
+ ((uint64_t*)out)[1] = h2;
+}
+
+//-----------------------------------------------------------------------------
+
diff --git a/extern/smhasher/MurmurHash3.h b/extern/smhasher/MurmurHash3.h
new file mode 100644
index 0000000000..8aebdc304d
--- /dev/null
+++ b/extern/smhasher/MurmurHash3.h
@@ -0,0 +1,33 @@
+//-----------------------------------------------------------------------------
+// MurmurHash3 was written by Austin Appleby, and is placed in the public
+// domain. The author hereby disclaims copyright to this source code.
+
+#ifndef _MURMURHASH3_H_
+#define _MURMURHASH3_H_
+
+//-----------------------------------------------------------------------------
+// Platform-specific functions and macros
+
+// Microsoft Visual Studio
+
+#if defined(_MSC_VER) && (_MSC_VER < 1600)
+
+typedef unsigned char uint8_t;
+typedef unsigned int uint32_t;
+typedef unsigned __int64 uint64_t;
+
+// Other compilers
+
+#else // defined(_MSC_VER)
+
+#include <stdint.h>
+
+#endif // !defined(_MSC_VER)
+
+//-----------------------------------------------------------------------------
+
+void MurmurHash3_x64_128 ( const void * key, int len, const uint64_t * seed, void * out );
+
+//-----------------------------------------------------------------------------
+
+#endif // _MURMURHASH3_H_
diff --git a/files/CMakeLists.txt b/files/CMakeLists.txt
index cea33f0f40..607ddeca49 100644
--- a/files/CMakeLists.txt
+++ b/files/CMakeLists.txt
@@ -3,3 +3,4 @@ add_subdirectory(shaders)
add_subdirectory(vfs)
add_subdirectory(builtin_scripts)
add_subdirectory(lua_api)
+add_subdirectory(../extern/i18n.lua ${CMAKE_CURRENT_BINARY_DIR}/files)
diff --git a/files/builtin_scripts/CMakeLists.txt b/files/builtin_scripts/CMakeLists.txt
index 5906680e1b..1ef67a2e15 100644
--- a/files/builtin_scripts/CMakeLists.txt
+++ b/files/builtin_scripts/CMakeLists.txt
@@ -1,8 +1,26 @@
-file(GLOB_RECURSE BUILTIN_SCRIPTS LIST_DIRECTORIES false RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} CONFIGURE_DEPENDS "*")
+if (NOT DEFINED OPENMW_RESOURCES_ROOT)
+ return()
+endif()
-foreach (f ${BUILTIN_SCRIPTS})
- if (NOT ("CMakeLists.txt" STREQUAL "${f}"))
- copy_resource_file("${CMAKE_CURRENT_SOURCE_DIR}/${f}" "${OpenMW_BINARY_DIR}" "resources/vfs/${f}")
- endif()
-endforeach (f)
+# Copy resource files into the build directory
+set(SDIR ${CMAKE_CURRENT_SOURCE_DIR})
+set(DDIRRELATIVE resources/vfs)
+copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "builtin.omwscripts")
+set(LUA_AUX_FILES
+ openmw_aux/util.lua
+ openmw_aux/time.lua
+ openmw_aux/calendar.lua
+)
+
+set(DDIRRELATIVE resources/vfs/openmw_aux)
+copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "${LUA_AUX_FILES}")
+
+set(LUA_SCRIPTS_FILES
+ scripts/omw/camera.lua
+ scripts/omw/head_bobbing.lua
+ scripts/omw/third_person.lua
+)
+
+set(DDIRRELATIVE resources/vfs/scripts/omw)
+copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "${LUA_SCRIPTS_FILES}")
diff --git a/files/builtin_scripts/builtin.omwscripts b/files/builtin_scripts/builtin.omwscripts
new file mode 100644
index 0000000000..30fccad9fa
--- /dev/null
+++ b/files/builtin_scripts/builtin.omwscripts
@@ -0,0 +1 @@
+PLAYER: scripts/omw/camera.lua
diff --git a/files/builtin_scripts/i18n/Calendar/en.lua b/files/builtin_scripts/i18n/Calendar/en.lua
new file mode 100644
index 0000000000..a4e8183a92
--- /dev/null
+++ b/files/builtin_scripts/i18n/Calendar/en.lua
@@ -0,0 +1,42 @@
+-- source: https://en.uesp.net/wiki/Lore:Calendar
+
+return {
+ month1 = "Morning Star",
+ month2 = "Sun's Dawn",
+ month3 = "First Seed",
+ month4 = "Rain's Hand",
+ month5 = "Second Seed",
+ month6 = "Midyear",
+ month7 = "Sun's Height",
+ month8 = "Last Seed",
+ month9 = "Hearthfire",
+ month10 = "Frostfall",
+ month11 = "Sun's Dusk",
+ month12 = "Evening Star",
+
+ -- The variant of month names in the context "day X of month Y".
+ -- In English it is the same, but some languages require a different form.
+ monthInGenitive1 = "Morning Star",
+ monthInGenitive2 = "Sun's Dawn",
+ monthInGenitive3 = "First Seed",
+ monthInGenitive4 = "Rain's Hand",
+ monthInGenitive5 = "Second Seed",
+ monthInGenitive6 = "Midyear",
+ monthInGenitive7 = "Sun's Height",
+ monthInGenitive8 = "Last Seed",
+ monthInGenitive9 = "Hearthfire",
+ monthInGenitive10 = "Frostfall",
+ monthInGenitive11 = "Sun's Dusk",
+ monthInGenitive12 = "Evening Star",
+
+ dateFormat = "day %{day} of %{monthInGenitive} %{year}",
+
+ weekday1 = "Sundas",
+ weekday2 = "Morndas",
+ weekday3 = "Tirdas",
+ weekday4 = "Middas",
+ weekday5 = "Turdas",
+ weekday6 = "Fredas",
+ weekday7 = "Loredas",
+}
+
diff --git a/files/builtin_scripts/openmw_aux/calendar.lua b/files/builtin_scripts/openmw_aux/calendar.lua
new file mode 100644
index 0000000000..58e7298f1d
--- /dev/null
+++ b/files/builtin_scripts/openmw_aux/calendar.lua
@@ -0,0 +1,159 @@
+---
+-- `openmw_aux.calendar` defines utility functions for formatting game time.
+-- Implementation can be found in `resources/vfs/openmw_aux/calendar.lua`.
+-- @module calendar
+-- @usage local calendar = require('openmw_aux.calendar')
+
+local core = require('openmw.core')
+local time = require('openmw_aux.time')
+local i18n = core.i18n('Calendar')
+
+local monthsDuration = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
+local daysInWeek = 7
+local daysInYear = 0
+for _, d in ipairs(monthsDuration) do daysInYear = daysInYear + d end
+
+local startingYear = 427
+local startingYearDay = 227
+local startingWeekDay = 1
+
+local function gameTime(t)
+ if not t then
+ return core.getGameTime()
+ else
+ local days = (t.year or 0) * daysInYear + (t.day or 0)
+ for i = 1, (t.month or 1)-1 do
+ days = days + monthsDuration[i]
+ end
+ return days * time.day + (t.hour or 0) * time.hour +
+ (t.min or 0) * time.minute + (t.sec or 0) * time.second
+ end
+end
+
+local function defaultDateFormat(t)
+ return i18n('dateFormat', {
+ day = t.day,
+ month = i18n('month' .. t.month),
+ monthInGenitive = i18n('monthInGenitive' .. t.month),
+ year = t.year,
+ })
+end
+
+local function formatGameTime(formatStr, timestamp)
+ timestamp = timestamp or core.getGameTime()
+
+ local t = {}
+ local day = math.floor(timestamp / time.day)
+ t.year = math.floor(day / daysInYear) + startingYear
+ t.yday = (day + startingYearDay - 1) % daysInYear + 1
+ t.wday = (day + startingWeekDay - 1) % daysInWeek + 1
+ timestamp = timestamp % time.day
+ t.hour = math.floor(timestamp / time.hour)
+ timestamp = timestamp % time.hour
+ t.min = math.floor(timestamp / time.minute)
+ t.sec = math.floor(timestamp) % time.minute
+
+ t.day = t.yday
+ t.month = 1
+ while t.day > monthsDuration[t.month] do
+ t.day = t.day - monthsDuration[t.month]
+ t.month = t.month + 1
+ end
+
+ if formatStr == '*t' then return t end
+
+ local replFn = function(tag)
+ if tag == '%a' or tag == '%A' then return i18n('weekday' .. t.wday) end
+ if tag == '%b' or tag == '%B' then return i18n('monthInGenitive' .. t.month) end
+ if tag == '%c' then
+ return string.format('%02d:%02d %s', t.hour, t.min, defaultDateFormat(t))
+ end
+ if tag == '%d' then return string.format('%02d', t.day) end
+ if tag == '%e' then return string.format('%2d', t.day) end
+ if tag == '%H' then return string.format('%02d', t.hour) end
+ if tag == '%I' then return string.format('%02d', (t.hour - 1) % 12 + 1) end
+ if tag == '%M' then return string.format('%02d', t.min) end
+ if tag == '%m' then return string.format('%02d', t.month) end
+ if tag == '%p' then
+ if t.hour > 0 and t.hour <= 12 then
+ return 'am'
+ else
+ return 'pm'
+ end
+ end
+ if tag == '%S' then return string.format('%02d', t.sec) end
+ if tag == '%w' then return t.wday - 1 end
+ if tag == '%x' then return defaultDateFormat(t) end
+ if tag == '%X' then return string.format('%02d:%02d', t.hour, t.min) end
+ if tag == '%Y' then return t.year end
+ if tag == '%y' then return string.format('%02d', t.year % 100) end
+ if tag == '%%' then return '%' end
+ error('Unknown tag "'..tag..'"')
+ end
+
+ res, _ = string.gsub(formatStr or '%c', '%%.', replFn)
+ return res
+end
+
+return {
+ --- An equivalent of `os.time` for game time.
+ -- See [https://www.lua.org/pil/22.1.html](https://www.lua.org/pil/22.1.html)
+ -- @function [parent=#calendar] gameTime
+ -- @param #table table a table which describes a date (optional).
+ -- @return #number a timestamp.
+ gameTime = gameTime,
+
+ --- An equivalent of `os.date` for game time.
+ -- See [https://www.lua.org/pil/22.1.html](https://www.lua.org/pil/22.1.html).
+ -- It is a slow function. Please try not to use it in every frame.
+ -- @function [parent=#calendar] formatGameTime
+ -- @param #string format format of date (optional)
+ -- @param #number time time to format (default value is current time)
+ -- @return #string a formatted string representation of `time`.
+ formatGameTime = formatGameTime,
+
+ --- The number of months in a year
+ -- @field [parent=#calendar] #number monthCount
+ monthCount = #monthsDuration,
+
+ --- The number of days in a year
+ -- @field [parent=#calendar] #number daysInYear
+ daysInYear = daysInYear,
+
+ --- The number of days in a week
+ -- @field [parent=#calendar] #number daysInWeek
+ daysInWeek = daysInWeek,
+
+ --- The number of days in a month
+ -- @function [parent=#calendar] daysInMonth
+ -- @param monthIndex
+ -- @return #number
+ daysInMonth = function(m)
+ return monthsDuration[(m-1) % #monthsDuration + 1]
+ end,
+
+ --- The name of a month
+ -- @function [parent=#calendar] monthName
+ -- @param monthIndex
+ -- @return #string
+ monthName = function(m)
+ return i18n('month' .. ((m-1) % #monthsDuration + 1))
+ end,
+
+ --- The name of a month in genitive (for English is the same as `monthName`, but in some languages the form can differ).
+ -- @function [parent=#calendar] monthNameInGenitive
+ -- @param monthIndex
+ -- @return #string
+ monthNameInGenitive = function(m)
+ return i18n('monthInGenitive' .. ((m-1) % #monthsDuration + 1))
+ end,
+
+ --- The name of a weekday
+ -- @function [parent=#calendar] weekdayName
+ -- @param dayIndex
+ -- @return #string
+ weekdayName = function(d)
+ return i18n('weekday' .. ((d-1) % daysInWeek + 1))
+ end,
+}
+
diff --git a/files/builtin_scripts/openmw_aux/time.lua b/files/builtin_scripts/openmw_aux/time.lua
new file mode 100644
index 0000000000..a6aac0fa1a
--- /dev/null
+++ b/files/builtin_scripts/openmw_aux/time.lua
@@ -0,0 +1,104 @@
+---
+-- `openmw_aux.time` defines utility functions for timers.
+-- Implementation can be found in `resources/vfs/openmw_aux/time.lua`.
+-- @module time
+-- @usage local time = require('openmw_aux.time')
+
+local time = {
+ second = 1,
+ minute = 60,
+ hour = 3600,
+ day = 3600 * 24,
+ GameTime = 'GameTime',
+ SimulationTime = 'SimulationTime',
+}
+
+---
+-- Alias of async:registerTimerCallback ; register a function as a timer callback.
+-- @function [parent=#time] registerTimerCallback
+-- @param #string name
+-- @param #function func
+-- @return openmw.async#TimerCallback
+function time.registerTimerCallback(name, fn)
+ local async = require('openmw.async')
+ return async:registerTimerCallback(name, fn)
+end
+
+---
+-- Alias of async:newSimulationTimer ; call callback(arg) in `delay` game seconds.
+-- Callback must be registered in advance.
+-- @function [parent=#time] newGameTimer
+-- @param #number delay
+-- @param openmw.async#TimerCallback callback A callback returned by `registerTimerCallback`
+-- @param arg An argument for `callback`; can be `nil`.
+function time.newGameTimer(delay, callback, callbackArg)
+ local async = require('openmw.async')
+ return async:newGameTimer(delay, callback, callbackArg)
+end
+
+---
+-- Alias of async:newSimulationTimer ; call callback(arg) in `delay` simulation seconds.
+-- Callback must be registered in advance.
+-- @function [parent=#time] newSimulationTimer
+-- @param #number delay
+-- @param openmw.async#TimerCallback callback A callback returned by `registerTimerCallback`
+-- @param arg An argument for `callback`; can be `nil`.
+function time.newSimulationTimer(delay, callback, callbackArg)
+ local async = require('openmw.async')
+ return async:newSimulationTimer(delay, callback, callbackArg)
+end
+
+---
+-- Run given function repeatedly.
+-- Note that loading a save stops the evaluation. If it should work always, call it during initialization of the script (i.e. not in a handler)
+-- @function [parent=#time] runRepeatedly
+-- @param #function fn the function that should be called
+-- @param #number period interval
+-- @param #table options additional options `initialDelay` and `type`.
+-- `initialDelay` - delay before the first call. If missed then the delay is a random number in range [0, N]. Randomization is used for performance reasons -- to prevent all scripts from doing time consuming operations at the same time.
+-- `type` - either `time.SimulationTime` (by default, timer uses simulation time) or `time.GameTime` (timer uses game time).
+-- @return #function a function without arguments that can be used to stop the periodical evaluation.
+-- @usage
+-- local stopFn = time.runRepeatedly(function() print('Test') end,
+-- 5 * time.second) -- print 'Test' every 5 seconds
+-- stopFn() -- stop printing 'Test'
+-- time.runRepeatedly( -- print 'Test' every 5 minutes with initial 30 second delay
+-- function() print('Test2') end, 5 * time.minute,
+-- { initialDelay = 30 * time.second })
+-- @usage
+-- local timeBeforeMidnight = time.day - time.gameTime() % time.day
+-- time.runRepeatedly(doSomething, time.day, {
+-- initialDelay = timeBeforeMidnight,
+-- type = time.GameTime,
+-- }) -- call `doSomething` at the end of every game day.
+function time.runRepeatedly(fn, period, options)
+ if period <= 0 then
+ error('Period must be positive. If you want it to be as small '..
+ 'as possible, use the engine handler `onUpdate` instead', 2)
+ end
+ local async = require('openmw.async')
+ local core = require('openmw.core')
+ local initialDelay = (options and options.initialDelay) or math.random() * period
+ local getTimeFn, newTimerFn
+ if (options and options.type) == time.GameTime then
+ getTimeFn = core.getGameTime
+ newTimerFn = async.newUnsavableGameTimer
+ else
+ getTimeFn = core.getSimulationTime
+ newTimerFn = async.newUnsavableSimulationTimer
+ end
+ local baseTime = getTimeFn() + initialDelay
+ local breakFlag = false
+ local wrappedFn
+ wrappedFn = function()
+ if breakFlag then return end
+ fn()
+ local nextDelay = 1.5 * period - math.fmod(getTimeFn() - baseTime + period / 2, period)
+ newTimerFn(async, nextDelay, wrappedFn)
+ end
+ newTimerFn(async, initialDelay, wrappedFn)
+ return function() breakFlag = true end
+end
+
+return time
+
diff --git a/files/builtin_scripts/openmw_aux/util.lua b/files/builtin_scripts/openmw_aux/util.lua
index ba52ced2bd..8d4f7c1fd7 100644
--- a/files/builtin_scripts/openmw_aux/util.lua
+++ b/files/builtin_scripts/openmw_aux/util.lua
@@ -28,72 +28,5 @@ function aux_util.findNearestTo(point, objectList)
return res, resDist
end
--------------------------------------------------------------------------------
--- Runs given function every N game seconds (seconds when the game is not paused).
--- Note that loading a save stops the evaluation. If it should work always, call it in 2 places --
--- when a script starts and in the engine handler `onLoad`.
--- @function [parent=#util] runEveryNSeconds
--- @param #number N interval in seconds
--- @param #function fn the function that should be called every N seconds
--- @param #number initialDelay optional argument -- delay in seconds before the first call. If missed then the delay is a random number in range [0, N]. Randomization is used for performance reasons -- to prevent all scripts from doing time consuming operations at the same time.
--- @return #function a function without arguments that can be used to stop the periodical evaluation.
--- @usage
--- local stopFn = aux_util.runEveryNSeconds(5, function() print('Test') end) -- print 'Test' every 5 seconds
--- stopFn() -- stop printing 'Test'
--- aux_util.runEveryNSeconds(5, function() print('Test2') end, 1) -- print 'Test' every 5 seconds starting from the next second
-function aux_util.runEveryNSeconds(N, fn, initialDelay)
- if N <= 0 then
- error('Interval must be positive. If you want it to be as small '..
- 'as possible, use the engine handler `onUpdate` instead', 2)
- end
- local async = require('openmw.async')
- local core = require('openmw.core')
- local breakFlag = false
- local initialDelay = initialDelay or math.random() * N
- local baseTime = core.getGameTimeInSeconds() + initialDelay
- local wrappedFn
- wrappedFn = function()
- if breakFlag then return end
- fn()
- local nextDelay = 1.5 * N - math.fmod(core.getGameTimeInSeconds() - baseTime + N / 2, N)
- async:newUnsavableTimerInSeconds(nextDelay, wrappedFn)
- end
- async:newUnsavableTimerInSeconds(initialDelay, wrappedFn)
- return function() breakFlag = true end
-end
-
--------------------------------------------------------------------------------
--- Runs given function every N game hours.
--- Note that loading a save stops the evaluation. If it should work always, call it in 2 places --
--- when a script starts and in the engine handler `onLoad`.
--- @function [parent=#util] runEveryNHours
--- @param #number N interval in game hours
--- @param #function fn the function that should be called every N game hours
--- @param #number initialDelay optional argument -- delay in game hours before the first call. If missed then the delay is a random number in range [0, N]. Randomization is used for performance reasons -- to prevent all scripts from doing time consuming operations at the same time.
--- @return #function a function without arguments that can be used to stop the periodical evaluation.
--- @usage
--- local timeBeforeMidnight = 24 - math.fmod(core.getGameTimeInHours(), 24)
--- aux_util.runEveryNHours(24, doSomething, timeBeforeMidnight) -- call `doSomething` at the end of every game day.
-function aux_util.runEveryNHours(N, fn, initialDelay)
- if N <= 0 then
- error('Interval must be positive. If you want it to be as small '..
- 'as possible, use the engine handler `onUpdate` instead', 2)
- end
- local async = require('openmw.async')
- local core = require('openmw.core')
- local breakFlag = false
- local initialDelay = initialDelay or math.random() * N
- local baseTime = core.getGameTimeInHours() + initialDelay
- local wrappedFn
- wrappedFn = function()
- if breakFlag then return end
- fn()
- local nextDelay = 1.5 * N - math.fmod(core.getGameTimeInHours() - baseTime + N / 2, N)
- async:newUnsavableTimerInHours(nextDelay, wrappedFn)
- end
- async:newUnsavableTimerInHours(initialDelay, wrappedFn)
- return function() breakFlag = true end
-end
-
return aux_util
diff --git a/files/builtin_scripts/scripts/omw/camera.lua b/files/builtin_scripts/scripts/omw/camera.lua
new file mode 100644
index 0000000000..cd4d0dccfb
--- /dev/null
+++ b/files/builtin_scripts/scripts/omw/camera.lua
@@ -0,0 +1,247 @@
+local camera = require('openmw.camera')
+local core = require('openmw.core')
+local input = require('openmw.input')
+local settings = require('openmw.settings')
+local util = require('openmw.util')
+local self = require('openmw.self')
+
+local head_bobbing = require('scripts.omw.head_bobbing')
+local third_person = require('scripts.omw.third_person')
+
+local MODE = camera.MODE
+
+local previewIfStandSill = settings._getBoolFromSettingsCfg('Camera', 'preview if stand still')
+local showCrosshairInThirdPerson = settings._getBoolFromSettingsCfg('Camera', 'view over shoulder')
+
+local primaryMode
+
+local noModeControl = 0
+local noStandingPreview = 0
+local noHeadBobbing = 0
+local noZoom = 0
+
+local function init()
+ camera.allowCharacterDeferredRotation(settings._getBoolFromSettingsCfg('Camera', 'deferred preview rotation'))
+ if camera.getMode() == MODE.FirstPerson then
+ primaryMode = MODE.FirstPerson
+ else
+ primaryMode = MODE.ThirdPerson
+ camera.setMode(MODE.ThirdPerson)
+ end
+end
+
+local smoothedSpeed = 0
+local previewTimer = 0
+
+local function updatePOV(dt)
+ local switchLimit = 0.25
+ if input.isActionPressed(input.ACTION.TogglePOV) and input.getControlSwitch(input.CONTROL_SWITCH.ViewMode) then
+ previewTimer = previewTimer + dt
+ if primaryMode == MODE.ThirdPerson or previewTimer >= switchLimit then
+ third_person.standingPreview = false
+ camera.setMode(MODE.Preview)
+ end
+ elseif previewTimer > 0 then
+ if previewTimer <= switchLimit then
+ if primaryMode == MODE.FirstPerson then
+ primaryMode = MODE.ThirdPerson
+ else
+ primaryMode = MODE.FirstPerson
+ end
+ end
+ camera.setMode(primaryMode)
+ previewTimer = 0
+ end
+end
+
+local idleTimer = 0
+local vanityDelay = settings.getGMST('fVanityDelay')
+
+local function updateVanity(dt)
+ if input.isIdle() then
+ idleTimer = idleTimer + dt
+ else
+ idleTimer = 0
+ end
+ local vanityAllowed = input.getControlSwitch(input.CONTROL_SWITCH.VanityMode)
+ if vanityAllowed and idleTimer > vanityDelay and camera.getMode() ~= MODE.Vanity then
+ camera.setMode(MODE.Vanity)
+ end
+ if camera.getMode() == MODE.Vanity then
+ if not vanityAllowed or idleTimer == 0 then
+ camera.setMode(primaryMode)
+ else
+ camera.setYaw(camera.getYaw() + math.rad(3) * dt)
+ end
+ end
+end
+
+local function updateSmoothedSpeed(dt)
+ local speed = self:getCurrentSpeed()
+ speed = speed / (1 + speed / 500)
+ local maxDelta = 300 * dt
+ smoothedSpeed = smoothedSpeed + util.clamp(speed - smoothedSpeed, -maxDelta, maxDelta)
+end
+
+local minDistance = 30
+local maxDistance = 800
+
+local function zoom(delta)
+ if not input.getControlSwitch(input.CONTROL_SWITCH.ViewMode) or
+ not input.getControlSwitch(input.CONTROL_SWITCH.Controls) or
+ camera.getMode() == MODE.Static or noZoom > 0 then
+ return
+ end
+ if camera.getMode() ~= MODE.FirstPerson then
+ local obstacleDelta = third_person.preferredDistance - camera.getThirdPersonDistance()
+ if delta > 0 and third_person.baseDistance == minDistance and
+ (camera.getMode() ~= MODE.Preview or third_person.standingPreview) and noModeControl == 0 then
+ primaryMode = MODE.FirstPerson
+ camera.setMode(primaryMode)
+ elseif delta > 0 or obstacleDelta < -delta then
+ third_person.baseDistance = util.clamp(third_person.baseDistance - delta - obstacleDelta, minDistance, maxDistance)
+ end
+ elseif delta < 0 and noModeControl == 0 then
+ primaryMode = MODE.ThirdPerson
+ camera.setMode(primaryMode)
+ third_person.baseDistance = minDistance
+ end
+end
+
+local function applyControllerZoom(dt)
+ if camera.getMode() == MODE.Preview then
+ local triggerLeft = input.getAxisValue(input.CONTROLLER_AXIS.TriggerLeft)
+ local triggerRight = input.getAxisValue(input.CONTROLLER_AXIS.TriggerRight)
+ local controllerZoom = (triggerRight - triggerLeft) * 100 * dt
+ if controllerZoom ~= 0 then
+ zoom(controllerZoom)
+ end
+ end
+end
+
+local function updateStandingPreview()
+ local mode = camera.getMode()
+ if not previewIfStandSill or noStandingPreview > 0
+ or mode == MODE.FirstPerson or mode == MODE.Static or mode == MODE.Vanity then
+ third_person.standingPreview = false
+ return
+ end
+ local standingStill = self:getCurrentSpeed() == 0 and not self:isInWeaponStance() and not self:isInMagicStance()
+ if standingStill and mode == MODE.ThirdPerson then
+ third_person.standingPreview = true
+ camera.setMode(MODE.Preview)
+ elseif not standingStill and third_person.standingPreview then
+ third_person.standingPreview = false
+ camera.setMode(primaryMode)
+ end
+end
+
+local function updateCrosshair()
+ camera.showCrosshair(
+ camera.getMode() == MODE.FirstPerson or
+ (showCrosshairInThirdPerson and (camera.getMode() == MODE.ThirdPerson or third_person.standingPreview)))
+end
+
+local function onUpdate(dt)
+ camera.setExtraPitch(0)
+ camera.setExtraYaw(0)
+ camera.setRoll(0)
+ camera.setFirstPersonOffset(util.vector3(0, 0, 0))
+ updateSmoothedSpeed(dt)
+end
+
+local function onInputUpdate(dt)
+ local mode = camera.getMode()
+ if mode == MODE.FirstPerson or mode == MODE.ThirdPerson then
+ primaryMode = mode
+ end
+ if mode ~= MODE.Static then
+ if not camera.getQueuedMode() or camera.getQueuedMode() == MODE.Preview then
+ if noModeControl == 0 then
+ updatePOV(dt)
+ updateVanity(dt)
+ end
+ updateStandingPreview()
+ end
+ updateCrosshair()
+ end
+ applyControllerZoom(dt)
+ third_person.update(dt, smoothedSpeed)
+ if noHeadBobbing == 0 then head_bobbing.update(dt, smoothedSpeed) end
+end
+
+return {
+ interfaceName = 'Camera',
+ --- @module Camera
+ -- @usage require('openmw.interfaces').Camera
+ interface = {
+ --- Interface version
+ -- @field [parent=#Camera] #number version
+ version = 0,
+
+ --- Return primary mode (MODE.FirstPerson or MODE.ThirdPerson).
+ -- @function [parent=#Camera] getPrimaryMode
+ getPrimaryMode = function() return primaryMode end,
+ --- @function [parent=#Camera] getBaseThirdPersonDistance
+ getBaseThirdPersonDistance = function() return third_person.baseDistance end,
+ --- @function [parent=#Camera] setBaseThirdPersonDistance
+ setBaseThirdPersonDistance = function(v) third_person.baseDistance = v end,
+ --- @function [parent=#Camera] getTargetThirdPersonDistance
+ getTargetThirdPersonDistance = function() return third_person.preferredDistance end,
+
+ --- @function [parent=#Camera] isModeControlEnabled
+ isModeControlEnabled = function() return noModeControl == 0 end,
+ --- @function [parent=#Camera] disableModeControl
+ disableModeControl = function() noModeControl = noModeControl + 1 end,
+ --- @function [parent=#Camera] enableModeControl
+ enableModeControl = function() noModeControl = math.max(0, noModeControl - 1) end,
+
+ --- @function [parent=#Camera] isStandingPreviewEnabled
+ isStandingPreviewEnabled = function() return previewIfStandSill and noStandingPreview == 0 end,
+ --- @function [parent=#Camera] disableStandingPreview
+ disableStandingPreview = function() noStandingPreview = noStandingPreview + 1 end,
+ --- @function [parent=#Camera] enableStandingPreview
+ enableStandingPreview = function() noStandingPreview = math.max(0, noStandingPreview - 1) end,
+
+ --- @function [parent=#Camera] isHeadBobbingEnabled
+ isHeadBobbingEnabled = function() return head_bobbing.enabled and noHeadBobbing == 0 end,
+ --- @function [parent=#Camera] disableHeadBobbing
+ disableHeadBobbing = function() noHeadBobbing = noHeadBobbing + 1 end,
+ --- @function [parent=#Camera] enableHeadBobbing
+ enableHeadBobbing = function() noHeadBobbing = math.max(0, noHeadBobbing - 1) end,
+
+ --- @function [parent=#Camera] isZoomEnabled
+ isZoomEnabled = function() return noZoom == 0 end,
+ --- @function [parent=#Camera] disableZoom
+ disableZoom = function() noZoom = noZoom + 1 end,
+ --- @function [parent=#Camera] enableZoom
+ enableZoom = function() noZoom = math.max(0, noZoom - 1) end,
+
+ --- @function [parent=#Camera] isThirdPersonOffsetControlEnabled
+ isThirdPersonOffsetControlEnabled = function() return third_person.noOffsetControl == 0 end,
+ --- @function [parent=#Camera] disableThirdPersonOffsetControl
+ disableThirdPersonOffsetControl = function() third_person.noOffsetControl = third_person.noOffsetControl + 1 end,
+ --- @function [parent=#Camera] enableThirdPersonOffsetControl
+ enableThirdPersonOffsetControl = function() third_person.noOffsetControl = math.max(0, third_person.noOffsetControl - 1) end,
+ },
+ engineHandlers = {
+ onUpdate = onUpdate,
+ onInputUpdate = onInputUpdate,
+ onInputAction = function(action)
+ if core.isWorldPaused() then return end
+ if action == input.ACTION.ZoomIn then
+ zoom(10)
+ elseif action == input.ACTION.ZoomOut then
+ zoom(-10)
+ end
+ end,
+ onActive = init,
+ onLoad = function(data)
+ if data and data.distance then third_person.baseDistance = data.distance end
+ end,
+ onSave = function()
+ return {version = 0, distance = third_person.baseDistance}
+ end,
+ },
+}
+
diff --git a/files/builtin_scripts/scripts/omw/head_bobbing.lua b/files/builtin_scripts/scripts/omw/head_bobbing.lua
new file mode 100644
index 0000000000..fe809fca8a
--- /dev/null
+++ b/files/builtin_scripts/scripts/omw/head_bobbing.lua
@@ -0,0 +1,51 @@
+local camera = require('openmw.camera')
+local self = require('openmw.self')
+local settings = require('openmw.settings')
+local util = require('openmw.util')
+
+local doubleStepLength = settings._getFloatFromSettingsCfg('Camera', 'head bobbing step') * 2
+local stepHeight = settings._getFloatFromSettingsCfg('Camera', 'head bobbing height')
+local maxRoll = math.rad(settings._getFloatFromSettingsCfg('Camera', 'head bobbing roll'))
+
+local effectWeight = 0
+local totalMovement = 0
+
+local M = {
+ enabled = settings._getBoolFromSettingsCfg('Camera', 'head bobbing')
+}
+
+-- Trajectory of each step is a scaled arc of 60 degrees.
+local halfArc = math.rad(30)
+local sampleArc = function(x) return 1 - math.cos(x * halfArc) end
+local arcHeight = sampleArc(1)
+
+function M.update(dt, smoothedSpeed)
+ local speed = self:getCurrentSpeed()
+ speed = speed / (1 + speed / 500) -- limit bobbing frequency if the speed is very high
+ totalMovement = totalMovement + speed * dt
+ if not M.enabled or camera.getMode() ~= camera.MODE.FirstPerson then
+ effectWeight = 0
+ return
+ end
+ if self:isOnGround() then
+ effectWeight = math.min(1, effectWeight + dt * 5)
+ else
+ effectWeight = math.max(0, effectWeight - dt * 5)
+ end
+
+ local doubleStepState = totalMovement / doubleStepLength
+ doubleStepState = doubleStepState - math.floor(doubleStepState) -- from 0 to 1 during 2 steps
+ local stepState = math.abs(doubleStepState * 4 - 2) - 1 -- from -1 to 1 on even steps and from 1 to -1 on odd steps
+ local effect = sampleArc(stepState) / arcHeight -- range from 0 to 1
+
+ -- Smoothly reduce the effect to zero when the player stops
+ local coef = math.min(smoothedSpeed / 300, 1) * effectWeight
+
+ local zOffset = (0.5 - effect) * coef * stepHeight -- range from -stepHeight/2 to stepHeight/2
+ local roll = ((stepState > 0 and 1) or -1) * effect * coef * maxRoll -- range from -maxRoll to maxRoll
+ camera.setFirstPersonOffset(camera.getFirstPersonOffset() + util.vector3(0, 0, zOffset))
+ camera.setRoll(camera.getRoll() + roll)
+end
+
+return M
+
diff --git a/files/builtin_scripts/scripts/omw/third_person.lua b/files/builtin_scripts/scripts/omw/third_person.lua
new file mode 100644
index 0000000000..ca00ef5ee9
--- /dev/null
+++ b/files/builtin_scripts/scripts/omw/third_person.lua
@@ -0,0 +1,139 @@
+local camera = require('openmw.camera')
+local settings = require('openmw.settings')
+local util = require('openmw.util')
+local self = require('openmw.self')
+local nearby = require('openmw.nearby')
+
+local MODE = camera.MODE
+local STATE = { RightShoulder = 0, LeftShoulder = 1, Combat = 2, Swimming = 3 }
+
+local M = {
+ baseDistance = settings._getFloatFromSettingsCfg('Camera', 'third person camera distance'),
+ preferredDistance = 0,
+ standingPreview = false,
+ noOffsetControl = 0,
+}
+
+local viewOverShoulder = settings._getBoolFromSettingsCfg('Camera', 'view over shoulder')
+local autoSwitchShoulder = settings._getBoolFromSettingsCfg('Camera', 'auto switch shoulder')
+local shoulderOffset = settings._getVector2FromSettingsCfg('Camera', 'view over shoulder offset')
+local zoomOutWhenMoveCoef = settings._getFloatFromSettingsCfg('Camera', 'zoom out when move coef')
+
+local defaultShoulder = (shoulderOffset.x > 0 and STATE.RightShoulder) or STATE.LeftShoulder
+local rightShoulderOffset = util.vector2(math.abs(shoulderOffset.x), shoulderOffset.y)
+local leftShoulderOffset = util.vector2(-math.abs(shoulderOffset.x), shoulderOffset.y)
+local combatOffset = util.vector2(0, 15)
+
+local state = defaultShoulder
+
+local rayOptions = {collisionType = nearby.COLLISION_TYPE.Default - nearby.COLLISION_TYPE.Actor}
+local function ray(from, angle, limit)
+ local to = from + util.transform.rotateZ(angle) * util.vector3(0, limit, 0)
+ local res = nearby.castRay(from, to, rayOptions)
+ if res.hit then
+ return (res.hitPos - from):length()
+ else
+ return limit
+ end
+end
+
+local function trySwitchShoulder()
+ local limitToSwitch = 120 -- switch to other shoulder if wall is closer than this limit
+ local limitToSwitchBack = 300 -- switch back to default shoulder if there is no walls at this distance
+
+ local pos = camera.getTrackedPosition()
+ local rayRight = ray(pos, camera.getYaw() + math.rad(90), limitToSwitchBack + 1)
+ local rayLeft = ray(pos, camera.getYaw() - math.rad(90), limitToSwitchBack + 1)
+ local rayRightForward = ray(pos, camera.getYaw() + math.rad(30), limitToSwitchBack + 1)
+ local rayLeftForward = ray(pos, camera.getYaw() - math.rad(30), limitToSwitchBack + 1)
+
+ local distRight = math.min(rayRight, rayRightForward)
+ local distLeft = math.min(rayLeft, rayLeftForward)
+
+ if distLeft < limitToSwitch and distRight > limitToSwitchBack then
+ state = STATE.RightShoulder
+ elseif distRight < limitToSwitch and distLeft > limitToSwitchBack then
+ state = STATE.LeftShoulder
+ elseif distRight > limitToSwitchBack and distLeft > limitToSwitchBack then
+ state = defaultShoulder
+ end
+end
+
+local function calculateDistance(smoothedSpeed)
+ local smoothedSpeedSqr = smoothedSpeed * smoothedSpeed
+ return (M.baseDistance + math.max(camera.getPitch(), 0) * 50
+ + smoothedSpeedSqr / (smoothedSpeedSqr + 300*300) * zoomOutWhenMoveCoef)
+end
+
+local noThirdPersonLastFrame = true
+
+local function updateState()
+ local mode = camera.getMode()
+ local oldState = state
+ if (self:isInWeaponStance() or self:isInMagicStance()) and mode == MODE.ThirdPerson then
+ state = STATE.Combat
+ elseif self:isSwimming() then
+ state = STATE.Swimming
+ elseif oldState == STATE.Combat or oldState == STATE.Swimming then
+ state = defaultShoulder
+ end
+ if autoSwitchShoulder and (mode == MODE.ThirdPerson or state ~= oldState or noThirdPersonLastFrame)
+ and (state == STATE.LeftShoulder or state == STATE.RightShoulder) then
+ trySwitchShoulder()
+ end
+ if oldState ~= state or noThirdPersonLastFrame then
+ -- State was changed, start focal point transition.
+ if mode == MODE.Vanity then
+ -- Player doesn't touch controls for a long time. Transition should be very slow.
+ camera.setFocalTransitionSpeed(0.2)
+ elseif (oldState == STATE.Combat or state == STATE.Combat) and
+ (mode ~= MODE.Preview or M.standingPreview) then
+ -- Transition to/from combat mode and we are not in preview mode. Should be fast.
+ camera.setFocalTransitionSpeed(5.0)
+ else
+ camera.setFocalTransitionSpeed(1.0) -- Default transition speed.
+ end
+
+ if state == STATE.RightShoulder then
+ camera.setFocalPreferredOffset(rightShoulderOffset)
+ elseif state == STATE.LeftShoulder then
+ camera.setFocalPreferredOffset(leftShoulderOffset)
+ else
+ camera.setFocalPreferredOffset(combatOffset)
+ end
+ end
+end
+
+function M.update(dt, smoothedSpeed)
+ local mode = camera.getMode()
+ if mode == MODE.FirstPerson or mode == MODE.Static then
+ noThirdPersonLastFrame = true
+ return
+ end
+ if not viewOverShoulder then
+ M.preferredDistance = M.baseDistance
+ camera.setPreferredThirdPersonDistance(M.baseDistance)
+ noThirdPersonLastFrame = false
+ return
+ end
+
+ if M.noOffsetControl == 0 then
+ updateState()
+ else
+ state = nil
+ end
+
+ M.preferredDistance = calculateDistance(smoothedSpeed)
+ if noThirdPersonLastFrame then -- just switched to third person view
+ camera.setPreferredThirdPersonDistance(M.preferredDistance)
+ camera.instantTransition()
+ noThirdPersonLastFrame = false
+ else
+ local maxIncrease = dt * (100 + M.baseDistance)
+ camera.setPreferredThirdPersonDistance(math.min(
+ M.preferredDistance, camera.getThirdPersonDistance() + maxIncrease))
+ end
+end
+
+return M
+
diff --git a/files/lua_api/openmw/async.lua b/files/lua_api/openmw/async.lua
index cc3a233f8a..61d5763a0b 100644
--- a/files/lua_api/openmw/async.lua
+++ b/files/lua_api/openmw/async.lua
@@ -15,35 +15,35 @@
-- @return #TimerCallback
-------------------------------------------------------------------------------
--- Calls callback(arg) in `delay` seconds.
+-- Calls callback(arg) in `delay` simulation seconds.
-- Callback must be registered in advance.
--- @function [parent=#async] newTimerInSeconds
+-- @function [parent=#async] newSimulationTimer
-- @param self
-- @param #number delay
-- @param #TimerCallback callback A callback returned by `registerTimerCallback`
-- @param arg An argument for `callback`; can be `nil`.
-------------------------------------------------------------------------------
--- Calls callback(arg) in `delay` game hours.
+-- Calls callback(arg) in `delay` game seconds.
-- Callback must be registered in advance.
--- @function [parent=#async] newTimerInHours
+-- @function [parent=#async] newGameTimer
-- @param self
-- @param #number delay
-- @param #TimerCallback callback A callback returned by `registerTimerCallback`
-- @param arg An argument for `callback`; can be `nil`.
-------------------------------------------------------------------------------
--- Calls `func()` in `delay` seconds.
+-- Calls `func()` in `delay` simulation seconds.
-- The timer will be lost if the game is saved and loaded.
--- @function [parent=#async] newUnsavableTimerInSeconds
+-- @function [parent=#async] newUnsavableSimulationTimer
-- @param self
-- @param #number delay
-- @param #function func
-------------------------------------------------------------------------------
--- Calls `func()` in `delay` game hours.
+-- Calls `func()` in `delay` game seconds.
-- The timer will be lost if the game is saved and loaded.
--- @function [parent=#async] newUnsavableTimerInHours
+-- @function [parent=#async] newUnsavableGameTimer
-- @param self
-- @param #number delay
-- @param #function func
diff --git a/files/lua_api/openmw/camera.lua b/files/lua_api/openmw/camera.lua
new file mode 100644
index 0000000000..0d72214414
--- /dev/null
+++ b/files/lua_api/openmw/camera.lua
@@ -0,0 +1,171 @@
+-------------------------------------------------------------------------------
+-- `openmw.camera` controls camera.
+-- Can be used only by player scripts.
+-- @module camera
+-- @usage local camera = require('openmw.camera')
+
+
+-------------------------------------------------------------------------------
+-- @type MODE Camera modes.
+-- @field #number Static Camera doesn't track player; player inputs doesn't affect camera; use `setStaticPosition` to move the camera.
+-- @field #number FirstPerson First person mode.
+-- @field #number ThirdPerson Third person mode; player character turns to the view direction.
+-- @field #number Vanity Similar to Preview; camera slowly moves around the player.
+-- @field #number Preview Third person mode, but player character doesn't turn to the view direction.
+
+-------------------------------------------------------------------------------
+-- Camera modes.
+-- @field [parent=#camera] #MODE MODE
+
+-------------------------------------------------------------------------------
+-- Return the current @{openmw.camera#MODE}.
+-- @function [parent=#camera] getMode
+-- @return #MODE
+
+-------------------------------------------------------------------------------
+-- Return the mode the camera will switch to after the end of the current animation. Can be nil.
+-- @function [parent=#camera] getQueuedMode
+-- @return #MODE
+
+-------------------------------------------------------------------------------
+-- Change @{openmw.camera#MODE}; if the second (optional, true by default) argument is set to false, the switching can be delayed (see `getQueuedMode`).
+-- @function [parent=#camera] setMode
+-- @param #MODE mode
+-- @param #boolean force
+
+-------------------------------------------------------------------------------
+-- If set to true then after switching from Preview to ThirdPerson the player character turns to the camera view direction. Otherwise the camera turns to the character view direction.
+-- @function [parent=#camera] allowCharacterDeferredRotation
+-- @param #boolean boolValue
+
+-------------------------------------------------------------------------------
+-- Show/hide crosshair.
+-- @function [parent=#camera] showCrosshair
+-- @param #boolean boolValue
+
+-------------------------------------------------------------------------------
+-- Current position of the tracked object (the characters head if there is no animation).
+-- @function [parent=#camera] getTrackedPosition
+-- @return openmw.util#Vector3
+
+-------------------------------------------------------------------------------
+-- Current position of the camera.
+-- @function [parent=#camera] getPosition
+-- @return openmw.util#Vector3
+
+-------------------------------------------------------------------------------
+-- Camera pitch angle (radians) without taking extraPitch into account.
+-- Full pitch is `getPitch()+getExtraPitch()`.
+-- @function [parent=#camera] getPitch
+-- @return #number
+
+-------------------------------------------------------------------------------
+-- Force the pitch angle to the given value (radians); player input on this axis is ignored in this frame.
+-- @function [parent=#camera] setPitch
+-- @param #number value
+
+-------------------------------------------------------------------------------
+-- Camera yaw angle (radians) without taking extraYaw into account.
+-- Full yaw is `getYaw()+getExtraYaw()`.
+-- @function [parent=#camera] getYaw
+-- @return #number
+
+-------------------------------------------------------------------------------
+-- Force the yaw angle to the given value (radians); player input on this axis is ignored in this frame.
+-- @function [parent=#camera] setYaw
+-- @param #number value
+
+-------------------------------------------------------------------------------
+-- Get camera roll angle (radians).
+-- @function [parent=#camera] getRoll
+-- @return #number
+
+-------------------------------------------------------------------------------
+-- Set camera roll angle (radians).
+-- @function [parent=#camera] setRoll
+-- @param #number value
+
+-------------------------------------------------------------------------------
+-- Additional summand for the pitch angle that is not affected by player input.
+-- Full pitch is `getPitch()+getExtraPitch()`.
+-- @function [parent=#camera] getExtraPitch
+-- @return #number
+
+-------------------------------------------------------------------------------
+-- Additional summand for the pitch angle; useful for camera shaking effects.
+-- Setting extra pitch doesn't block player input.
+-- Full pitch is `getPitch()+getExtraPitch()`.
+-- @function [parent=#camera] setExtraPitch
+-- @param #number value
+
+-------------------------------------------------------------------------------
+-- Additional summand for the yaw angle that is not affected by player input.
+-- Full yaw is `getYaw()+getExtraYaw()`.
+-- @function [parent=#camera] getExtraYaw
+-- @return #number
+
+-------------------------------------------------------------------------------
+-- Additional summand for the yaw angle; useful for camera shaking effects.
+-- Setting extra pitch doesn't block player input.
+-- Full yaw is `getYaw()+getExtraYaw()`.
+-- @function [parent=#camera] setExtraYaw
+-- @param #number value
+
+-------------------------------------------------------------------------------
+-- Set camera position; can be used only if camera is in Static mode.
+-- @function [parent=#camera] setStaticPosition
+-- @param openmw.util#Vector3 pos
+
+-------------------------------------------------------------------------------
+-- The offset between the characters head and the camera in first person mode (3d vector).
+-- @function [parent=#camera] getFirstPersonOffset
+-- @return openmw.util#Vector3
+
+-------------------------------------------------------------------------------
+-- Set the offset between the characters head and the camera in first person mode (3d vector).
+-- @function [parent=#camera] setFirstPersonOffset
+-- @param openmw.util#Vector3 offset
+
+-------------------------------------------------------------------------------
+-- Preferred offset between tracked position (see `getTrackedPosition`) and the camera focal point (the center of the screen) in third person mode.
+-- See `setFocalPreferredOffset`.
+-- @function [parent=#camera] getFocalPreferredOffset
+-- @return openmw.util#Vector2
+
+-------------------------------------------------------------------------------
+-- Set preferred offset between tracked position (see `getTrackedPosition`) and the camera focal point (the center of the screen) in third person mode.
+-- The offset is a 2d vector (X, Y) where X is horizontal (to the right from the character) and Y component is vertical (upward).
+-- The real offset can differ from the preferred one during smooth transition of if blocked by an obstacle.
+-- Smooth transition happens by default every time when the preferred offset was changed. Use `instantTransition()` to skip the current transition.
+-- @function [parent=#camera] setFocalPreferredOffset
+-- @param openmw.util#Vector2 offset
+
+-------------------------------------------------------------------------------
+-- The actual distance between the camera and the character in third person mode; can differ from the preferred one if there is an obstacle.
+-- @function [parent=#camera] getThirdPersonDistance
+-- @return #number
+
+-------------------------------------------------------------------------------
+-- Set preferred distance between the camera and the character in third person mode.
+-- @function [parent=#camera] setPreferredThirdPersonDistance
+-- @param #number distance
+
+-------------------------------------------------------------------------------
+-- The current speed coefficient of focal point (the center of the screen in third person mode) smooth transition.
+-- @function [parent=#camera] getFocalTransitionSpeed
+-- @return #number
+
+-------------------------------------------------------------------------------
+-- Set the speed coefficient of focal point (the center of the screen in third person mode) smooth transition.
+-- Smooth transition happens by default every time when the preferred offset was changed. Use `instantTransition()` to skip the current transition.
+-- @function [parent=#camera] setFocalTransitionSpeed
+-- Set the speed coefficient
+-- @param #number speed
+
+-------------------------------------------------------------------------------
+-- Make instant the current transition of camera focal point and the current deferred rotation (see `allowCharacterDeferredRotation`).
+-- @function [parent=#camera] instantTransition
+
+
+return nil
+
diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua
index 35513bbb79..8415690067 100644
--- a/files/lua_api/openmw/core.lua
+++ b/files/lua_api/openmw/core.lua
@@ -21,17 +21,70 @@
-- @param eventData
-------------------------------------------------------------------------------
+-- Simulation time in seconds.
+-- The number of simulation seconds passed in the game world since starting a new game.
+-- @function [parent=#core] getSimulationTime
+-- @return #number
+
+-------------------------------------------------------------------------------
+-- The scale of simulation time relative to real time.
+-- @function [parent=#core] getSimulationTimeScale
+-- @return #number
+
+-------------------------------------------------------------------------------
-- Game time in seconds.
--- The number of seconds in the game world, passed from starting a new game.
--- @function [parent=#core] getGameTimeInSeconds
+-- @function [parent=#core] getGameTime
-- @return #number
-------------------------------------------------------------------------------
--- Current time of the game world in hours.
--- Note that the number of game seconds in a game hour is not guaranteed to be fixed.
--- @function [parent=#core] getGameTimeInHours
+-- The scale of game time relative to simulation time.
+-- @function [parent=#core] getGameTimeScale
-- @return #number
+-------------------------------------------------------------------------------
+-- Whether the world is paused (onUpdate doesn't work when the world is paused).
+-- @function [parent=#core] isWorldPaused
+-- @return #boolean
+
+-------------------------------------------------------------------------------
+-- Get a GMST setting from content files.
+-- @function [parent=#core] getGMST
+-- @param #string setting Setting name
+-- @return #any
+
+-------------------------------------------------------------------------------
+-- Return i18n formatting function for the given context.
+-- It is based on `i18n.lua` library.
+-- Language files should be stored in VFS as `i18n/<ContextName>/<Lang>.lua`.
+-- See https://github.com/kikito/i18n.lua for format details.
+-- @function [parent=#core] i18n
+-- @param #string context I18n context; recommended to use the name of the mod.
+-- @return #function
+-- @usage
+-- -- DataFiles/i18n/MyMod/en.lua
+-- return {
+-- good_morning = 'Good morning.',
+-- you_have_arrows = {
+-- one = 'You have one arrow.',
+-- other = 'You have %{count} arrows.',
+-- },
+-- }
+-- @usage
+-- -- DataFiles/i18n/MyMod/de.lua
+-- return {
+-- good_morning = "Guten Morgen.",
+-- you_have_arrows = {
+-- one = "Du hast ein Pfeil.",
+-- other = "Du hast %{count} Pfeile.",
+-- },
+-- ["Hello %{name}!"] = "Hallo %{name}!",
+-- }
+-- @usage
+-- local myMsg = core.i18n('MyMod')
+-- print( myMsg('good_morning') )
+-- print( myMsg('you_have_arrows', {count=5}) )
+-- print( myMsg('Hello %{name}!', {name='World'}) )
+
-------------------------------------------------------------------------------
-- @type OBJECT_TYPE
diff --git a/files/lua_api/openmw/input.lua b/files/lua_api/openmw/input.lua
index 0975ce6e6a..361073e79d 100644
--- a/files/lua_api/openmw/input.lua
+++ b/files/lua_api/openmw/input.lua
@@ -18,6 +18,38 @@
-- @return #boolean
-------------------------------------------------------------------------------
+-- Is a keyboard button currently pressed.
+-- @function [parent=#input] isKeyPressed
+-- @param #number keyCode Key code (see @{openmw.input#KEY})
+-- @return #boolean
+
+-------------------------------------------------------------------------------
+-- Is a controller button currently pressed.
+-- @function [parent=#input] isControllerButtonPressed
+-- @param #number buttonId Button index (see @{openmw.input#CONTROLLER_BUTTON})
+-- @return #boolean
+
+-------------------------------------------------------------------------------
+-- Is `Shift` key pressed.
+-- @function [parent=#input] isShiftPressed
+-- @return #boolean
+
+-------------------------------------------------------------------------------
+-- Is `Ctrl` key pressed.
+-- @function [parent=#input] isCtrlPressed
+-- @return #boolean
+
+-------------------------------------------------------------------------------
+-- Is `Alt` key pressed.
+-- @function [parent=#input] isAltPressed
+-- @return #boolean
+
+-------------------------------------------------------------------------------
+-- Is `Super`/`Win` key pressed.
+-- @function [parent=#input] isSuperPressed
+-- @return #boolean
+
+-------------------------------------------------------------------------------
-- Is a mouse button currently pressed.
-- @function [parent=#input] isMouseButtonPressed
-- @param #number buttonId Button index (1 - left, 2 - middle, 3 - right, 4 - X1, 5 - X2)
@@ -52,6 +84,12 @@
-- @param #boolean value
-------------------------------------------------------------------------------
+-- Returns a human readable name for the given key code
+-- @function [parent=#input] getKeyName
+-- @param #number code A key code (see @{openmw.input#KEY})
+-- @return #string
+
+-------------------------------------------------------------------------------
-- @type CONTROL_SWITCH
-- @field [parent=#CONTROL_SWITCH] #string Controls Ability to move
-- @field [parent=#CONTROL_SWITCH] #string Fighting Ability to attack
@@ -157,9 +195,118 @@
-- @field [parent=#input] #CONTROLLER_AXIS CONTROLLER_AXIS
-------------------------------------------------------------------------------
+-- @type KEY
+-- @field #number _0
+-- @field #number _1
+-- @field #number _2
+-- @field #number _3
+-- @field #number _4
+-- @field #number _5
+-- @field #number _6
+-- @field #number _7
+-- @field #number _8
+-- @field #number _9
+-- @field #number NP_0
+-- @field #number NP_1
+-- @field #number NP_2
+-- @field #number NP_3
+-- @field #number NP_4
+-- @field #number NP_5
+-- @field #number NP_6
+-- @field #number NP_7
+-- @field #number NP_8
+-- @field #number NP_9
+-- @field #number NP_Divide
+-- @field #number NP_Enter
+-- @field #number NP_Minus
+-- @field #number NP_Multiply
+-- @field #number NP_Delete
+-- @field #number NP_Plus
+-- @field #number F1
+-- @field #number F2
+-- @field #number F3
+-- @field #number F4
+-- @field #number F5
+-- @field #number F6
+-- @field #number F7
+-- @field #number F8
+-- @field #number F9
+-- @field #number F10
+-- @field #number F11
+-- @field #number F12
+-- @field #number A
+-- @field #number B
+-- @field #number C
+-- @field #number D
+-- @field #number E
+-- @field #number F
+-- @field #number G
+-- @field #number H
+-- @field #number I
+-- @field #number J
+-- @field #number K
+-- @field #number L
+-- @field #number M
+-- @field #number N
+-- @field #number O
+-- @field #number P
+-- @field #number Q
+-- @field #number R
+-- @field #number S
+-- @field #number T
+-- @field #number U
+-- @field #number V
+-- @field #number W
+-- @field #number X
+-- @field #number Y
+-- @field #number Z
+-- @field #number LeftArrow
+-- @field #number RightArrow
+-- @field #number UpArrow
+-- @field #number DownArrow
+-- @field #number LeftAlt
+-- @field #number LeftCtrl
+-- @field #number LeftBracket
+-- @field #number LeftSuper
+-- @field #number LeftShift
+-- @field #number RightAlt
+-- @field #number RightCtrl
+-- @field #number RightBracket
+-- @field #number RightSuper
+-- @field #number RightShift
+-- @field #number Apostrophe
+-- @field #number BackSlash
+-- @field #number Backspace
+-- @field #number CapsLock
+-- @field #number Comma
+-- @field #number Delete
+-- @field #number End
+-- @field #number Enter
+-- @field #number Equals
+-- @field #number Escape
+-- @field #number Home
+-- @field #number Insert
+-- @field #number Minus
+-- @field #number NumLock
+-- @field #number PageDown
+-- @field #number PageUp
+-- @field #number Pause
+-- @field #number Period
+-- @field #number PrintScreen
+-- @field #number ScrollLock
+-- @field #number Semicolon
+-- @field #number Slash
+-- @field #number Space
+-- @field #number Tab
+
+-------------------------------------------------------------------------------
+-- Key codes.
+-- @field [parent=#input] #KEY KEY
+
+-------------------------------------------------------------------------------
-- The argument of `onKeyPress`/`onKeyRelease` engine handlers.
-- @type KeyboardEvent
--- @field [parent=#KeyboardEvent] #string symbol The pressed symbol (1-symbol string).
+-- @field [parent=#KeyboardEvent] #string symbol The pressed symbol (1-symbol string if can be represented or an empty string otherwise).
-- @field [parent=#KeyboardEvent] #string code Key code.
-- @field [parent=#KeyboardEvent] #boolean withShift Is `Shift` key pressed.
-- @field [parent=#KeyboardEvent] #boolean withCtrl Is `Control` key pressed.
diff --git a/files/lua_api/openmw/settings.lua b/files/lua_api/openmw/settings.lua
deleted file mode 100644
index 6c35caa8af..0000000000
--- a/files/lua_api/openmw/settings.lua
+++ /dev/null
@@ -1,14 +0,0 @@
--------------------------------------------------------------------------------
--- `openmw.settings` provides read-only access to GMST records in content files.
--- @module settings
--- @usage
--- local settings = require('openmw.settings')
-
--------------------------------------------------------------------------------
--- Get a GMST setting from content files.
--- @function [parent=#settings] getGMST
--- @param #string setting
-
-
-return nil
-
diff --git a/files/lua_api/openmw/storage.lua b/files/lua_api/openmw/storage.lua
new file mode 100644
index 0000000000..5499eefb9c
--- /dev/null
+++ b/files/lua_api/openmw/storage.lua
@@ -0,0 +1,96 @@
+---
+-- `openmw.storage` contains functions to work with permanent Lua storage.
+-- @module storage
+-- @usage
+-- local storage = require('openmw.storage')
+-- local myModData = storage.globalSection('MyModExample')
+-- myModData:set("someVariable", 1.0)
+-- myModData:set("anotherVariable", { exampleStr='abc', exampleBool=true })
+-- local function update()
+-- if myModCfg:checkChanged() then
+-- print('Data was changes by another script:')
+-- print('MyModExample.someVariable =', myModData:get('someVariable'))
+-- print('MyModExample.anotherVariable.exampleStr =',
+-- myModData:get('anotherVariable').exampleStr)
+-- end
+-- end
+
+---
+-- Get a section of the global storage; can be used by any script, but only global scripts can change values.
+-- Creates the section if it doesn't exist.
+-- @function [parent=#storage] globalSection
+-- @param #string sectionName
+-- @return #StorageSection
+
+---
+-- Get a section of the player storage; can be used by player scripts only.
+-- Creates the section if it doesn't exist.
+-- @function [parent=#storage] playerSection
+-- @param #string sectionName
+-- @return #StorageSection
+
+---
+-- Get all global sections as a table; can be used by global scripts only.
+-- Note that adding/removing items to the returned table doesn't create or remove sections.
+-- @function [parent=#storage] allGlobalSections
+-- @return #table
+
+---
+-- Get all global sections as a table; can be used by player scripts only.
+-- Note that adding/removing items to the returned table doesn't create or remove sections.
+-- @function [parent=#storage] allPlayerSections
+-- @return #table
+
+---
+-- A map `key -> value` that represents a storage section.
+-- @type StorageSection
+
+---
+-- Get value by a string key; if value is a table makes it readonly.
+-- @function [parent=#StorageSection] get
+-- @param self
+-- @param #string key
+
+---
+-- Get value by a string key; if value is a table returns a copy.
+-- @function [parent=#StorageSection] getCopy
+-- @param self
+-- @param #string key
+
+---
+-- Return `True` if any value in this section was changed by another script since the last `wasChanged`.
+-- @function [parent=#StorageSection] wasChanged
+-- @param self
+-- @return #boolean
+
+---
+-- Copy all values and return them as a table.
+-- @function [parent=#StorageSection] asTable
+-- @param self
+-- @return #table
+
+---
+-- Remove all existing values and assign values from given (the arg is optional) table.
+-- Note: `section:reset()` removes all values, but not the section itself. Use `section:removeOnExit()` to remove the section completely.
+-- @function [parent=#StorageSection] reset
+-- @param self
+-- @param #table values (optional) New values
+
+---
+-- Make the whole section temporary: will be removed on exit or when load a save.
+-- No section can be removed immediately because other scripts may use it at the moment.
+-- Temporary sections have the same interface to get/set values, the only difference is they will not
+-- be saved to the permanent storage on exit.
+-- This function can not be used for a global storage section from a local script.
+-- @function [parent=#StorageSection] removeOnExit
+-- @param self
+
+---
+-- Set value by a string key; can not be used for global storage from a local script.
+-- @function [parent=#StorageSection] set
+-- @param self
+-- @param #string key
+-- @param #any value
+
+return nil
+
diff --git a/files/lua_api/openmw/ui.lua b/files/lua_api/openmw/ui.lua
index bf6976c76b..c60a4e1abd 100644
--- a/files/lua_api/openmw/ui.lua
+++ b/files/lua_api/openmw/ui.lua
@@ -1,15 +1,140 @@
--------------------------------------------------------------------------------
+---
-- `openmw.ui` controls user interface.
-- Can be used only by local scripts, that are attached to a player.
-- @module ui
--- @usage local ui = require('openmw.ui')
+-- @usage
+-- local ui = require('openmw.ui')
+---
+-- @field [parent=#ui] #WIDGET_TYPE WIDGET_TYPE
+---
+-- Tools for working with layers
+-- @field [parent=#ui] #Layers layers
--------------------------------------------------------------------------------
+---
+-- @type WIDGET_TYPE
+-- @field [parent=#WIDGET_TYPE] Widget Base widget type
+-- @field [parent=#WIDGET_TYPE] Text Display text
+-- @field [parent=#WIDGET_TYPE] TextEdit Accepts user text input
+-- @field [parent=#WIDGET_TYPE] Window Can be moved and resized by the user
+
+---
-- Shows given message at the bottom of the screen.
-- @function [parent=#ui] showMessage
-- @param #string msg
-return nil
+---
+-- Converts a given table of tables into an @{openmw.ui#Content}
+-- @function [parent=#ui] content
+-- @param #table table
+-- @return #Content
+
+---
+-- Creates a UI element from the given layout table
+-- @function [parent=#ui] create
+-- @param #Layout layout
+-- @return #Element
+
+---
+-- Layout
+-- @type Layout
+-- @field #string name Optional name of the layout. Allows access by name from Content
+-- @field #table props Optional table of widget properties
+-- @field #table events Optional table of event callbacks
+-- @field #Content content Optional @{openmw.ui#Content} of children layouts
+
+---
+-- Layers
+-- @type Layers
+-- @usage
+-- ui.layers.insertAfter('HUD', 'NewLayer', { interactive = true })
+-- local fourthLayerName = ui.layers[4]
+-- local windowsIndex = ui.layers.indexOf('Windows')
+
+---
+-- Index of the layer with the givent name. Returns nil if the layer doesn't exist
+-- @function [parent=#Layers] indexOf
+-- @param #string name Name of the layer
+-- @return #number, #nil index
+
+---
+-- Creates a layer and inserts it after another layer (shifts indexes of some other layers).
+-- @function [parent=#Layers] insertAfter
+-- @param #string afterName Name of the layer after which the new layer will be inserted
+-- @param #string name Name of the new layer
+-- @param #table options Table with a boolean `interactive` field (default is true). Layers with interactive = false will ignore all mouse interactions.
+
+---
+-- Content. An array-like container, which allows to reference elements by their name
+-- @type Content
+-- @list <#Layout>
+-- @usage
+-- local content = ui.content {
+-- { name = 'input' },
+-- }
+-- -- bad idea!
+-- -- content[1].name = 'otherInput'
+-- -- do this instead:
+-- content.input = { name = 'otherInput' }
+-- @usage
+-- local content = ui.content {
+-- { name = 'display' },
+-- { name = 'submit' },
+-- }
+-- -- allowed, but shifts all the items after it "up" the array
+-- content.display = nil
+-- -- still no holes after this!
+-- @usage
+-- -- iterate over a Content
+-- for i = 1, #content do
+-- print('widget',content[i].name,'at',i)
+-- end
+
+---
+-- Puts the layout at given index by shifting all the elements after it
+-- @function [parent=#Content] insert
+-- @param self
+-- @param #number index
+-- @param #Layout layout
+---
+-- Adds the layout at the end of the Content
+-- (same as calling insert with `last index + 1`)
+-- @function [parent=#Content] add
+-- @param self
+-- @param #Layout layout
+
+---
+-- Finds the index of the given layout. If it is not in the container, returns nil
+-- @function [parent=#Content] indexOf
+-- @param self
+-- @param #Layout layout
+-- @return #number, #nil index
+
+---
+-- Element. An element of the user interface
+-- @type Element
+
+---
+-- Refreshes the rendered element to match the current layout state
+-- @function [parent=#Element] update
+-- @param self
+
+---
+-- Destroys the element
+-- @function [parent=#Element] destroy
+-- @param self
+
+---
+-- Access or replace the element's layout
+-- @field [parent=#Element] #Layout layout
+
+---
+-- Mouse event, passed as an argument to relevant UI events
+-- @type MouseEvent
+-- @field openmw.util#Vector2 position Absolute position of the mouse cursor
+-- @field openmw.util#Vector2 offset Position of the mouse cursor relative to the widget
+-- @field #number button Mouse button which triggered the event (could be nil)
+
+return nil
diff --git a/files/lua_api/openmw/util.lua b/files/lua_api/openmw/util.lua
index 84e640ff40..22986f09a6 100644
--- a/files/lua_api/openmw/util.lua
+++ b/files/lua_api/openmw/util.lua
@@ -19,6 +19,12 @@
-- @param #number angle Angle in radians
-- @return #number Angle in range `[-pi, pi]`
+-------------------------------------------------------------------------------
+-- Makes a table read only.
+-- @function [parent=#util] makeReadOnly
+-- @param #table table Any table.
+-- @return #table The same table wrapped with read only userdata.
+
-------------------------------------------------------------------------------
-- Immutable 2D vector
@@ -32,7 +38,7 @@
-- v:length() -- 5.0 length
-- v:length2() -- 25.0 square of the length
-- v:normalize() -- vector2(3/5, 4/5)
--- v:rotate(radians) -- rotate clockwise (returns rotated vector)
+-- v:rotate(radians) -- rotate counterclockwise (returns rotated vector)
-- v1:dot(v2) -- dot product (returns a number)
-- v1 * v2 -- dot product
-- v1 + v2 -- vector addition
@@ -183,26 +189,26 @@
-------------------------------------------------------------------------------
--- Rotation (any axis).
+-- Rotation around a vector (counterclockwise if the vector points to us).
-- @function [parent=#TRANSFORM] rotate
-- @param #number angle
-- @param #Vector3 axis.
-- @return #Transform.
-------------------------------------------------------------------------------
--- X-axis rotation.
+-- X-axis rotation (equivalent to `rotate(angle, vector3(-1, 0, 0))`).
-- @function [parent=#TRANSFORM] rotateX
-- @param #number angle
-- @return #Transform.
-------------------------------------------------------------------------------
--- Y-axis rotation.
+-- Y-axis rotation (equivalent to `rotate(angle, vector3(0, -1, 0))`).
-- @function [parent=#TRANSFORM] rotateY
-- @param #number angle
-- @return #Transform.
-------------------------------------------------------------------------------
--- Z-axis rotation.
+-- Z-axis rotation (equivalent to `rotate(angle, vector3(0, 0, -1))`).
-- @function [parent=#TRANSFORM] rotateZ
-- @param #number angle
-- @return #Transform.
diff --git a/files/lua_api/openmw/world.lua b/files/lua_api/openmw/world.lua
index 90ea23b4f5..690f0bd566 100644
--- a/files/lua_api/openmw/world.lua
+++ b/files/lua_api/openmw/world.lua
@@ -29,6 +29,36 @@
-- @param #number gridY
-- @return openmw.core#Cell
+-------------------------------------------------------------------------------
+-- Simulation time in seconds.
+-- The number of simulation seconds passed in the game world since starting a new game.
+-- @function [parent=#core] getSimulationTime
+-- @return #number
+
+-------------------------------------------------------------------------------
+-- The scale of simulation time relative to real time.
+-- @function [parent=#core] getSimulationTimeScale
+-- @return #number
+
+-------------------------------------------------------------------------------
+-- Game time in seconds.
+-- @function [parent=#core] getGameTime
+-- @return #number
+
+-------------------------------------------------------------------------------
+-- The scale of game time relative to simulation time.
+-- @function [parent=#core] getGameTimeScale
+-- @return #number
+
+-------------------------------------------------------------------------------
+-- Set the ratio of game time speed to simulation time speed.
+-- @function [parent=#world] setGameTimeScale
+-- @param #number ratio
+
+-------------------------------------------------------------------------------
+-- Whether the world is paused (onUpdate doesn't work when the world is paused).
+-- @function [parent=#world] isWorldPaused
+-- @return #boolean
return nil
diff --git a/files/lua_api/os.doclua b/files/lua_api/os.doclua
new file mode 100644
index 0000000000..127ccd049a
--- /dev/null
+++ b/files/lua_api/os.doclua
@@ -0,0 +1,64 @@
+-------------------------------------------------------------------------------
+-- Operating System Facilities.
+-- This library is implemented through table os.
+-- @module os
+
+
+-------------------------------------------------------------------------------
+-- Returns a string or a table containing date and time, formatted according
+-- to the given string `format`.
+--
+-- If the `time` argument is present, this is the time to be formatted
+-- (see the `os.time` function for a description of this value). Otherwise,
+-- `date` formats the current time.
+--
+-- If `format` starts with '`!`', then the date is formatted in Coordinated
+-- Universal Time. After this optional character, if `format` is the string
+-- "`*t`", then `date` returns a table with the following fields:
+--
+-- * `year` (four digits)
+-- * `month` (1--12)
+-- * `day` (1--31)
+-- * `hour` (0--23)
+-- * `min` (0--59)
+-- * `sec` (0--61)
+-- * `wday` (weekday, Sunday is 1)
+-- * `yday` (day of the year)
+-- * `isdst` (daylight saving flag, a boolean).
+--
+-- If `format` is not "`*t`", then `date` returns the date as a string,
+-- formatted according to the same rules as the C function `strftime`.
+-- When called without arguments, `date` returns a reasonable date and time
+-- representation that depends on the host system and on the current locale
+-- (that is, `os.date()` is equivalent to `os.date("%c")`).
+-- @function [parent=#os] date
+-- @param #string format format of date. (optional)
+-- @param #number time time to format. (default value is current time)
+-- @return #string a formatted string representation of `time`.
+
+-------------------------------------------------------------------------------
+-- Returns the number of seconds from time `t1` to time `t2`. In POSIX,
+-- Windows, and some other systems, this value is exactly `t2`*-*`t1`.
+-- @function [parent=#os] difftime
+-- @param #number t2
+-- @param #number t1
+-- @return #number the number of seconds from time `t1` to time `t2`.
+
+-------------------------------------------------------------------------------
+-- Returns the current time when called without arguments, or a time
+-- representing the date and time specified by the given table. This table
+-- must have fields `year`, `month`, and `day`, and may have fields `hour`,
+-- `min`, `sec`, and `isdst` (for a description of these fields, see the
+-- `os.date` function).
+--
+-- The returned value is a number, whose meaning depends on your system. In
+-- POSIX, Windows, and some other systems, this number counts the number
+-- of seconds since some given start time (the "epoch"). In other systems,
+-- the meaning is not specified, and the number returned by `time` can be
+-- used only as an argument to `date` and `difftime`.
+-- @function [parent=#os] time
+-- @param #table table a table which describes a date.
+-- @return #number a number meaning a date.
+
+return nil
+
diff --git a/files/mygui/CMakeLists.txt b/files/mygui/CMakeLists.txt
index 49b833e382..bdf7558cf7 100644
--- a/files/mygui/CMakeLists.txt
+++ b/files/mygui/CMakeLists.txt
@@ -1,4 +1,4 @@
-if (NOT DEFINED OPENMW_MYGUI_FILES_ROOT)
+if (NOT DEFINED OPENMW_RESOURCES_ROOT)
return()
endif()
@@ -97,4 +97,4 @@ set(MYGUI_FILES
)
-copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_MYGUI_FILES_ROOT} ${DDIRRELATIVE} "${MYGUI_FILES}")
+copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "${MYGUI_FILES}")
diff --git a/files/mygui/openmw_hud.layout b/files/mygui/openmw_hud.layout
index b3e3cfb9df..a189075f44 100644
--- a/files/mygui/openmw_hud.layout
+++ b/files/mygui/openmw_hud.layout
@@ -31,22 +31,6 @@
</Widget>
</Widget>
- <!-- Drowning bar -->
- <Widget type="Window" skin="MW_Dialog" position="0 36 230 58" align="Center Top" name="DrowningFrame">
- <Property key="Visible" value="false"/>
- <Widget type="TextBox" skin="SandText" position="0 3 222 24" name="DrowningTitle" align="Center Top HStretch">
- <Property key="Caption" value="#{sBreath}"/>
- <Property key="TextAlign" value="Center"/>
- <Property key="TextShadow" value="true"/>
- <Property key="TextShadowColour" value="0 0 0"/>
- </Widget>
- <Widget type="Widget" skin="MW_Box" position="11 29 200 10" align="Stretch" name="BoundingBox"/>
- <Widget type="ProgressBar" skin="MW_Progress_Drowning_Full" position="13 31 196 6" align="Center Top" name="Drowning">
- <Property key="NeedMouse" value="false"/>
- </Widget>
- <Widget type="Widget" skin="MW_Progress_Drowning_Small" position="15 33 192 2" align="Center Top" name="Flash"/>
- </Widget>
-
<!-- Equipped weapon/selected spell name display for a few seconds after it changes -->
<Widget type="TextBox" skin="SandText" position="13 118 270 24" name="WeaponSpellName" align="Left Bottom HStretch">
<Property key="Visible" value="false"/>
@@ -128,4 +112,23 @@
</Widget>
</Widget>
-</MyGUI>
+
+ <!-- Drowning bar -->
+ <Widget type="Widget" layer="DrowningBar" position="0 0 300 200" name="DrowningBar" align="Center Top">
+ <Property key="Visible" value="false"/>
+ <Widget type="Window" skin="MW_Dialog" position="0 36 230 58" align="Center Top" name="DrowningFrame">
+ <Widget type="TextBox" skin="SandText" position="0 3 222 24" name="DrowningTitle" align="Center Top HStretch">
+ <Property key="Caption" value="#{sBreath}"/>
+ <Property key="TextAlign" value="Center"/>
+ <Property key="TextShadow" value="true"/>
+ <Property key="TextShadowColour" value="0 0 0"/>
+ </Widget>
+ <Widget type="Widget" skin="MW_Box" position="11 29 200 10" align="Stretch" name="BoundingBox"/>
+ <Widget type="ProgressBar" skin="MW_Progress_Drowning_Full" position="13 31 196 6" align="Center Top" name="Drowning">
+ <Property key="NeedMouse" value="false"/>
+ </Widget>
+ <Widget type="Widget" skin="MW_Progress_Drowning_Small" position="15 33 192 2" align="Center Top" name="Flash"/>
+ </Widget>
+ </Widget>
+
+</MyGUI> \ No newline at end of file
diff --git a/files/mygui/openmw_layers.xml b/files/mygui/openmw_layers.xml
index c288655027..4afa67f314 100644
--- a/files/mygui/openmw_layers.xml
+++ b/files/mygui/openmw_layers.xml
@@ -15,6 +15,7 @@
<Layer name="Popup" overlapped="true" pick="true"/>
<Layer name="Notification" overlapped="false" pick="false"/>
<Layer name="DragAndDrop" overlapped="false" pick="false"/>
+ <Layer name="DrowningBar" overlapped="false" pick="false"/>
<Layer name="LoadingScreen" overlapped="false" pick="true"/>
<Layer name="MessageBox" overlapped="false" pick="true"/>
<Layer name="InputBlocker" overlapped="false" pick="true"/>
diff --git a/files/mygui/openmw_resources.xml b/files/mygui/openmw_resources.xml
index 811fa4f7fa..ff4893a9b2 100644
--- a/files/mygui/openmw_resources.xml
+++ b/files/mygui/openmw_resources.xml
@@ -47,7 +47,13 @@
<Widget type="Widget" skin="" position="4 4 81 28" align="Left Top Stretch" name="TabItem"/>
</Widget>
<Widget type="Widget" skin="" position="0 0 89 23" align="HStretch Top" name="HeaderPlace">
- <Widget type="Widget" skin="" position="52 0 37 23" name="Controls">
+ <Widget type="Widget" skin="" position="0 0 60 23" name="Controls">
+ <Widget type="Widget" skin="MW_Box" position="10 0 15 14" align="Left VCenter">
+ <Widget type="Button" skin="MW_ArrowLeft" position="2 2 10 10" align="Left VStretch" name="Left" />
+ </Widget>
+ <Widget type="Widget" skin="MW_Box" position="35 0 15 14" align="Right VCenter">
+ <Widget type="Button" skin="MW_ArrowRight" position="2 2 10 10" align="Right VStretch" name="Right" />
+ </Widget>
</Widget>
</Widget>
</Widget>
diff --git a/files/mygui/openmw_settings_window.layout b/files/mygui/openmw_settings_window.layout
index a07d8125d5..8e81764f60 100644
--- a/files/mygui/openmw_settings_window.layout
+++ b/files/mygui/openmw_settings_window.layout
@@ -454,6 +454,16 @@
<Property key="Caption" value="Reflection shader detail"/>
</Widget>
</Widget>
+ <Widget type="HBox" skin="" position="4 84 350 24">
+ <Widget type="ComboBox" skin="MW_ComboBox" position="0 0 115 24" align="Left Top" name="WaterRainRippleDetail">
+ <Property key="AddItem" value="Simple"/>
+ <Property key="AddItem" value="Sparse"/>
+ <Property key="AddItem" value="Dense"/>
+ </Widget>
+ <Widget type="AutoSizedTextBox" skin="SandText" position="64 4 90 16" align="Left Top">
+ <Property key="Caption" value="Rain ripple detail/density"/>
+ </Widget>
+ </Widget>
</Widget>
</Widget>
diff --git a/files/openmw.cfg b/files/openmw.cfg
index afbf0810cc..d98097c3eb 100644
--- a/files/openmw.cfg
+++ b/files/openmw.cfg
@@ -2,6 +2,7 @@
# Modifications should be done on the user openmw.cfg file instead
# (see: https://openmw.readthedocs.io/en/master/reference/modding/paths.html)
+content=builtin.omwscripts
data=${MORROWIND_DATA_FILES}
data-local="?userdata?data"
resources=${OPENMW_RESOURCE_FILES}
diff --git a/files/openmw.cfg.local b/files/openmw.cfg.local
index 76f829379b..704ac68068 100644
--- a/files/openmw.cfg.local
+++ b/files/openmw.cfg.local
@@ -2,6 +2,7 @@
# Modifications should be done on the user openmw.cfg file instead
# (see: https://openmw.readthedocs.io/en/master/reference/modding/paths.html)
+content=builtin.omwscripts
data="?global?data"
data=./data
data-local="?userdata?data"
diff --git a/files/settings-default.cfg b/files/settings-default.cfg
index 27a9544ea6..f1f3206661 100644
--- a/files/settings-default.cfg
+++ b/files/settings-default.cfg
@@ -152,9 +152,6 @@ object paging min size merge factor = 0.3
# Controls how inexpensive an object needs to be to utilize 'min size merge factor'.
object paging min size cost multiplier = 25
-# Assign a random color to merged batches.
-object paging debug batches = false
-
[Fog]
# If true, use extended fog parameters for distant terrain not controlled by
@@ -376,6 +373,9 @@ allow actors to follow over water surface = true
# Default size of actor for navmesh generation
default actor pathfind half extents = 29.27999496459961 28.479997634887695 66.5
+# Enables use of day/night switch nodes
+day night switches = true
+
[General]
# Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16).
@@ -494,6 +494,9 @@ minimum interior brightness = 0.08
# When MSAA is off, this setting will have no visible effect, but might have a performance cost.
antialias alpha test = false
+# Soften intersection of blended particle systems with opaque geometry
+soft particles = false
+
[Input]
# Capture control of the cursor prevent movement outside the window.
@@ -649,6 +652,10 @@ refraction = false
# Draw objects on water reflections.
reflection detail = 2
+# Whether to use fully detailed raindrop ripples. (0, 1, 2).
+# 0 = rings only; 1 = sparse, high detail; 2 = dense, high detail
+rain ripple detail = 2
+
# Overrides the value in '[Camera] small feature culling pixel size' specifically for water reflection/refraction textures.
small feature culling pixel size = 20.0
@@ -869,10 +876,10 @@ max polygons per tile = 4096
max verts per poly = 6
# Any regions with a span count smaller than this value will, if possible, be merged with larger regions. (value >= 0)
-region merge size = 20
+region merge area = 400
# The minimum number of cells allowed to form isolated island areas. (value >= 0)
-region min size = 8
+region min area = 64
# Number of background threads to update nav mesh (value >= 1)
async nav mesh updater threads = 1
@@ -923,6 +930,16 @@ min update interval ms = 250
# Distance is measured in the number of tiles and can be only an integer value.
wait until min distance to player = 5
+# Version of navigation mesh generation algorithm.
+# Should be increased each time there is a difference between output of makeNavMeshTileData function for the same input.
+nav mesh version = 1
+
+# Use navigation mesh cache stored on disk (true, false)
+enable nav mesh disk cache = true
+
+# Cache navigation mesh tiles to disk (true, false)
+write to navmeshdb = false
+
[Shadows]
# Enable or disable shadows. Bear in mind that this will force OpenMW to use shaders as if "[Shaders]/force shaders" was set to true.
@@ -1099,7 +1116,14 @@ stomp intensity = 1
[Lua]
+# Enable performance-heavy debug features
+lua debug = false
+
# Set the maximum number of threads used for Lua scripts.
# If zero, Lua scripts are processed in the main thread.
lua num threads = 1
+# List of the preferred languages separated by comma.
+# For example "de,en" means German as the first prority and English as a fallback.
+i18n preferred languages = en
+
diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt
index d86719f318..73929486cd 100644
--- a/files/shaders/CMakeLists.txt
+++ b/files/shaders/CMakeLists.txt
@@ -1,4 +1,4 @@
-if (NOT DEFINED OPENMW_SHADERS_ROOT)
+if (NOT DEFINED OPENMW_RESOURCES_ROOT)
return()
endif()
@@ -39,6 +39,7 @@ set(SHADER_FILES
sky_vertex.glsl
sky_fragment.glsl
skypasses.glsl
+ softparticles.glsl
)
-copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_SHADERS_ROOT} ${DDIRRELATIVE} "${SHADER_FILES}")
+copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "${SHADER_FILES}")
diff --git a/files/shaders/nv_default_fragment.glsl b/files/shaders/nv_default_fragment.glsl
index 245f83b620..ff81a2b94d 100644
--- a/files/shaders/nv_default_fragment.glsl
+++ b/files/shaders/nv_default_fragment.glsl
@@ -39,12 +39,13 @@ varying vec3 passNormal;
#include "alpha.glsl"
uniform float emissiveMult;
+uniform float specStrength;
void main()
{
#if @diffuseMap
gl_FragData[0] = texture2D(diffuseMap, diffuseMapUV);
- gl_FragData[0].a *= coveragePreservingAlphaScale(diffuseMap, adjustedDiffuseUV);
+ gl_FragData[0].a *= coveragePreservingAlphaScale(diffuseMap, diffuseMapUV);
#else
gl_FragData[0] = vec4(1.0);
#endif
@@ -80,7 +81,7 @@ void main()
gl_FragData[0].xyz *= lighting;
float shininess = gl_FrontMaterial.shininess;
- vec3 matSpec = getSpecularColor().xyz;
+ vec3 matSpec = getSpecularColor().xyz * specStrength;
#if @normalMap
matSpec *= normalTex.a;
#endif
diff --git a/files/shaders/objects_fragment.glsl b/files/shaders/objects_fragment.glsl
index 6f6cede4e4..9b60e7c6a4 100644
--- a/files/shaders/objects_fragment.glsl
+++ b/files/shaders/objects_fragment.glsl
@@ -71,6 +71,7 @@ centroid varying vec3 shadowDiffuseLighting;
#else
uniform float emissiveMult;
#endif
+uniform float specStrength;
varying vec3 passViewPos;
varying vec3 passNormal;
@@ -80,6 +81,10 @@ varying vec3 passNormal;
#include "parallax.glsl"
#include "alpha.glsl"
+#if @softParticles
+#include "softparticles.glsl"
+#endif
+
void main()
{
#if @diffuseMap
@@ -141,7 +146,7 @@ void main()
#if @decalMap
vec4 decalTex = texture2D(decalMap, decalMapUV);
- gl_FragData[0].xyz = mix(gl_FragData[0].xyz, decalTex.xyz, decalTex.a);
+ gl_FragData[0].xyz = mix(gl_FragData[0].xyz, decalTex.xyz, decalTex.a * diffuseColor.a);
#endif
#if @envMap
@@ -200,6 +205,7 @@ void main()
vec3 matSpec = getSpecularColor().xyz;
#endif
+ matSpec *= specStrength;
if (matSpec != vec3(0.0))
{
#if (!@normalMap && !@parallax && !@forcePPL)
@@ -220,6 +226,10 @@ void main()
#endif
gl_FragData[0].xyz = mix(gl_FragData[0].xyz, gl_Fog.color.xyz, fogValue);
+#if !defined(FORCE_OPAQUE) && @softParticles
+ gl_FragData[0].a *= calcSoftParticleFade();
+#endif
+
#if defined(FORCE_OPAQUE) && FORCE_OPAQUE
// having testing & blending isn't enough - we need to write an opaque pixel to be opaque
gl_FragData[0].a = 1.0;
diff --git a/files/shaders/softparticles.glsl b/files/shaders/softparticles.glsl
new file mode 100644
index 0000000000..fa8b4de4c1
--- /dev/null
+++ b/files/shaders/softparticles.glsl
@@ -0,0 +1,32 @@
+uniform float near;
+uniform float far;
+uniform sampler2D opaqueDepthTex;
+uniform vec2 screenRes;
+uniform float particleSize;
+
+float viewDepth(float depth)
+{
+#if @reverseZ
+ depth = 1.0 - depth;
+#endif
+ return (near * far) / ((far - near) * depth - far);
+}
+
+float calcSoftParticleFade()
+{
+ const float falloffMultiplier = 0.33;
+ const float contrast = 1.30;
+
+ vec2 screenCoords = gl_FragCoord.xy / screenRes;
+ float sceneDepth = viewDepth(texture2D(opaqueDepthTex, screenCoords).x);
+ float particleDepth = viewDepth(gl_FragCoord.z);
+ float falloff = particleSize * falloffMultiplier;
+ float delta = particleDepth - sceneDepth;
+
+ if (delta < 0.0)
+ discard;
+
+ const float shift = 0.845;
+
+ return shift * pow(clamp(delta/falloff, 0.0, 1.0), contrast);
+}
diff --git a/files/shaders/water_fragment.glsl b/files/shaders/water_fragment.glsl
index 26f83e052c..eaf52c1189 100644
--- a/files/shaders/water_fragment.glsl
+++ b/files/shaders/water_fragment.glsl
@@ -9,6 +9,7 @@
#endif
#define REFRACTION @refraction_enabled
+#define RAIN_RIPPLE_DETAIL @rain_ripple_detail
// Inspired by Blender GLSL Water by martinsh ( https://devlog-martinsh.blogspot.de/2012/07/waterundewater-shader-wip.html )
@@ -55,14 +56,22 @@ const float WOBBLY_SHORE_FADE_DISTANCE = 6200.0; // fade out wobbly shores to
// ---------------- rain ripples related stuff ---------------------
-const float RAIN_RIPPLE_GAPS = 5.0;
-const float RAIN_RIPPLE_RADIUS = 0.1;
+const float RAIN_RIPPLE_GAPS = 10.0;
+const float RAIN_RIPPLE_RADIUS = 0.2;
-vec2 randOffset(vec2 c)
+float scramble(float x, float z)
{
- return fract(vec2(
- c.x * c.y / 8.0 + c.y * 0.3 + c.x * 0.2,
- c.x * c.y / 14.0 + c.y * 0.5 + c.x * 0.7));
+ return fract(pow(fract(x)*3.0+1.0, z));
+}
+
+vec2 randOffset(vec2 c, float time)
+{
+ time = fract(time/1000.0);
+ c = vec2(c.x * c.y / 8.0 + c.y * 0.3 + c.x * 0.2,
+ c.x * c.y / 14.0 + c.y * 0.5 + c.x * 0.7);
+ c.x *= scramble(scramble(time + c.x/1000.0, 4.0), 3.0) + 1.0;
+ c.y *= scramble(scramble(time + c.y/1000.0, 3.5), 3.0) + 1.0;
+ return fract(c);
}
float randPhase(vec2 c)
@@ -70,43 +79,104 @@ float randPhase(vec2 c)
return fract((c.x * c.y) / (c.x + c.y + 0.1));
}
-vec4 circle(vec2 coords, vec2 i_part, float phase)
+float blip(float x)
{
- vec2 center = vec2(0.5,0.5) + (0.5 - RAIN_RIPPLE_RADIUS) * (2.0 * randOffset(i_part) - 1.0);
- vec2 toCenter = coords - center;
- float d = length(toCenter);
+ x = max(0.0, 1.0-x*x);
+ return x*x*x;
+}
- float r = RAIN_RIPPLE_RADIUS * phase;
+float blipDerivative(float x)
+{
+ x = clamp(x, -1.0, 1.0);
+ float n = x*x-1.0;
+ return -6.0*x*n*n;
+}
- if (d > r)
- return vec4(0.0,0.0,1.0,0.0);
+const float RAIN_RING_TIME_OFFSET = 1.0/6.0;
- float sinValue = (sin(d / r * 1.2) + 0.7) / 2.0;
+vec4 circle(vec2 coords, vec2 corner, float adjusted_time)
+{
+ vec2 center = vec2(0.5,0.5) + (0.5 - RAIN_RIPPLE_RADIUS) * (2.0 * randOffset(corner, floor(adjusted_time)) - 1.0);
+ float phase = fract(adjusted_time);
+ vec2 toCenter = coords - center;
- float height = (1.0 - abs(phase)) * pow(sinValue,3.0);
+ float r = RAIN_RIPPLE_RADIUS;
+ float d = length(toCenter);
+ float ringfollower = (phase-d/r)/RAIN_RING_TIME_OFFSET-1.0; // -1.0 ~ +1.0 cover the breadth of the ripple's ring
+
+#if RAIN_RIPPLE_DETAIL > 0
+// normal mapped ripples
+ if(ringfollower < -1.0 || ringfollower > 1.0)
+ return vec4(0.0);
+
+ if(d > 1.0) // normalize center direction vector, but not for near-center ripples
+ toCenter /= d;
+
+ float height = blip(ringfollower*2.0+0.5); // brighten up outer edge of ring; for fake specularity
+ float range_limit = blip(min(0.0, ringfollower));
+ float energy = 1.0-phase;
+
+ vec2 normal2d = -toCenter*blipDerivative(ringfollower)*5.0;
+ vec3 normal = vec3(normal2d, 0.5);
+ vec4 ret = vec4(normal, height);
+ ret.xyw *= energy*energy;
+ // do energy adjustment here rather than later, so that we can use the w component for fake specularity
+ ret.xyz = normalize(ret.xyz) * energy*range_limit;
+ ret.z *= range_limit;
+ return ret;
+#else
+// ring-only ripples
+ if(ringfollower < -1.0 || ringfollower > 0.5)
+ return vec4(0.0);
- vec3 normal = normalize(mix(vec3(0.0,0.0,1.0),vec3(normalize(toCenter),0.0),height));
+ float energy = 1.0-phase;
+ float height = blip(ringfollower*2.0+0.5)*energy*energy; // fake specularity
- return vec4(normal,height);
+ return vec4(0.0, 0.0, 0.0, height);
+#endif
}
-
vec4 rain(vec2 uv, float time)
{
- vec2 i_part = floor(uv * RAIN_RIPPLE_GAPS);
- vec2 f_part = fract(uv * RAIN_RIPPLE_GAPS);
- return circle(f_part,i_part,fract(time * 1.2 + randPhase(i_part)));
+ uv *= RAIN_RIPPLE_GAPS;
+ vec2 f_part = fract(uv);
+ vec2 i_part = floor(uv);
+ float adjusted_time = time * 1.2 + randPhase(i_part);
+#if RAIN_RIPPLE_DETAIL > 0
+ vec4 a = circle(f_part, i_part, adjusted_time);
+ vec4 b = circle(f_part, i_part, adjusted_time - RAIN_RING_TIME_OFFSET);
+ vec4 c = circle(f_part, i_part, adjusted_time - RAIN_RING_TIME_OFFSET*2.0);
+ vec4 d = circle(f_part, i_part, adjusted_time - RAIN_RING_TIME_OFFSET*3.0);
+ vec4 ret;
+ ret.xy = a.xy - b.xy/2.0 + c.xy/4.0 - d.xy/8.0;
+ // z should always point up
+ ret.z = a.z + b.z /2.0 + c.z /4.0 + d.z /8.0;
+ //ret.xyz *= 1.5;
+ // fake specularity looks weird if we use every single ring, also if the inner rings are too bright
+ ret.w = (a.w + c.w /8.0)*1.5;
+ return ret;
+#else
+ return circle(f_part, i_part, adjusted_time) * 1.5;
+#endif
}
-vec4 rainCombined(vec2 uv, float time) // returns ripple normal in xyz and ripple height in w
+vec2 complex_mult(vec2 a, vec2 b)
+{
+ return vec2(a.x*b.x - a.y*b.y, a.x*b.y + a.y*b.x);
+}
+vec4 rainCombined(vec2 uv, float time) // returns ripple normal in xyz and fake specularity in w
{
return
- rain(uv,time) +
- rain(uv + vec2(10.5,5.7),time) +
- rain(uv * 0.75 + vec2(3.7,18.9),time) +
- rain(uv * 0.9 + vec2(5.7,30.1),time) +
- rain(uv * 0.8 + vec2(1.2,3.0),time);
+ rain(uv, time)
+ + rain(complex_mult(uv, vec2(0.4, 0.7)) + vec2(1.2, 3.0),time)
+ #if RAIN_RIPPLE_DETAIL == 2
+ + rain(uv * 0.75 + vec2( 3.7,18.9),time)
+ + rain(uv * 0.9 + vec2( 5.7,30.1),time)
+ + rain(uv * 1.0 + vec2(10.5 ,5.7),time)
+ #endif
+ ;
}
+
// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
float fresnel_dielectric(vec3 Incoming, vec3 Normal, float eta)
@@ -132,7 +202,6 @@ vec2 normalCoords(vec2 uv, float scale, float speed, float time, float timer1, f
return uv * (WAVE_SCALE * scale) + WIND_DIR * time * (WIND_SPEED * speed) -(previousNormal.xy/previousNormal.zz) * WAVE_CHOPPYNESS + vec2(time * timer1,time * timer2);
}
-varying vec3 screenCoordsPassthrough;
varying vec4 position;
varying float linearDepth;
@@ -152,6 +221,8 @@ uniform vec3 nodePosition;
uniform float rainIntensity;
+uniform vec2 screenRes;
+
#define PER_PIXEL_LIGHTING 0
#include "shadows_fragment.glsl"
@@ -178,8 +249,7 @@ void main(void)
float shadow = unshadowedLightRatio(linearDepth);
- vec2 screenCoords = screenCoordsPassthrough.xy / screenCoordsPassthrough.z;
- screenCoords.y = (1.0-screenCoords.y);
+ vec2 screenCoords = gl_FragCoord.xy / screenRes;
#define waterTimer osg_SimulationTime
@@ -193,11 +263,11 @@ void main(void)
vec4 rainRipple;
if (rainIntensity > 0.01)
- rainRipple = rainCombined(position.xy / 1000.0,waterTimer) * clamp(rainIntensity,0.0,1.0);
+ rainRipple = rainCombined(position.xy/1000.0, waterTimer) * clamp(rainIntensity, 0.0, 1.0);
else
rainRipple = vec4(0.0);
- vec3 rippleAdd = rainRipple.xyz * rainRipple.w * 10.0;
+ vec3 rippleAdd = rainRipple.xyz * 10.0;
vec2 bigWaves = vec2(BIG_WAVES_X,BIG_WAVES_Y);
vec2 midWaves = mix(vec2(MID_WAVES_X,MID_WAVES_Y),vec2(MID_WAVES_RAIN_X,MID_WAVES_RAIN_Y),rainIntensity);
@@ -245,7 +315,14 @@ void main(void)
vec4 sunSpec = lcalcSpecular(0);
+ // artificial specularity to make rain ripples more noticeable
+ vec3 skyColorEstimate = vec3(max(0.0, mix(-0.3, 1.0, sunFade)));
+ vec3 rainSpecular = abs(rainRipple.w)*mix(skyColorEstimate, vec3(1.0), 0.05)*0.5;
+
#if REFRACTION
+ // no alpha here, so make sure raindrop ripple specularity gets properly subdued
+ rainSpecular *= clamp(fresnel*6.0 + specular * sunSpec.w, 0.0, 1.0);
+
// refraction
vec3 refraction = texture2D(refractionMap, screenCoords - screenCoordsOffset).rgb;
vec3 rawRefraction = refraction;
@@ -265,7 +342,7 @@ void main(void)
vec3 scatterColour = mix(SCATTER_COLOUR*vec3(1.0,0.4,0.0), SCATTER_COLOUR, clamp(1.0-exp(-sunHeight*SUN_EXT), 0.0, 1.0));
vec3 lR = reflect(lVec, lNormal);
float lightScatter = clamp(dot(lVec,lNormal)*0.7+0.3, 0.0, 1.0) * clamp(dot(lR, vVec)*2.0-1.2, 0.0, 1.0) * SCATTER_AMOUNT * sunFade * clamp(1.0-exp(-sunHeight), 0.0, 1.0);
- gl_FragData[0].xyz = mix( mix(refraction, scatterColour, lightScatter), reflection, fresnel) + specular * sunSpec.xyz + vec3(rainRipple.w) * 0.2;
+ gl_FragData[0].xyz = mix( mix(refraction, scatterColour, lightScatter), reflection, fresnel) + specular * sunSpec.xyz + rainSpecular;
gl_FragData[0].w = 1.0;
// wobbly water: hard-fade into refraction texture at extremely low depth, with a wobble based on normal mapping
@@ -278,7 +355,7 @@ void main(void)
shoreOffset = clamp(mix(shoreOffset, 1.0, clamp(linearDepth / WOBBLY_SHORE_FADE_DISTANCE, 0.0, 1.0)), 0.0, 1.0);
gl_FragData[0].xyz = mix(rawRefraction, gl_FragData[0].xyz, shoreOffset);
#else
- gl_FragData[0].xyz = mix(reflection, waterColor, (1.0-fresnel)*0.5) + specular * sunSpec.xyz + vec3(rainRipple.w) * 0.7;
+ gl_FragData[0].xyz = mix(reflection, waterColor, (1.0-fresnel)*0.5) + specular * sunSpec.xyz + rainSpecular;
gl_FragData[0].w = clamp(fresnel*6.0 + specular * sunSpec.w, 0.0, 1.0); //clamp(fresnel*2.0 + specular * gl_LightSource[0].specular.w, 0.0, 1.0);
#endif
@@ -288,7 +365,7 @@ void main(void)
#else
float fogValue = clamp((linearDepth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0);
#endif
- gl_FragData[0].xyz = mix(gl_FragData[0].xyz, gl_Fog.color.xyz, fogValue);
+ gl_FragData[0].xyz = mix(gl_FragData[0].xyz, gl_Fog.color.xyz, fogValue);
applyShadowDebugOverlay();
}
diff --git a/files/shaders/water_vertex.glsl b/files/shaders/water_vertex.glsl
index 8e506a57f8..3a6c352ac8 100644
--- a/files/shaders/water_vertex.glsl
+++ b/files/shaders/water_vertex.glsl
@@ -2,7 +2,6 @@
uniform mat4 projectionMatrix;
-varying vec3 screenCoordsPassthrough;
varying vec4 position;
varying float linearDepth;
@@ -13,14 +12,6 @@ void main(void)
{
gl_Position = projectionMatrix * (gl_ModelViewMatrix * gl_Vertex);
- mat4 scalemat = mat4(0.5, 0.0, 0.0, 0.0,
- 0.0, -0.5, 0.0, 0.0,
- 0.0, 0.0, 0.5, 0.0,
- 0.5, 0.5, 0.5, 1.0);
-
- vec4 texcoordProj = ((scalemat) * ( gl_Position));
- screenCoordsPassthrough = texcoordProj.xyw;
-
position = gl_Vertex;
vec4 viewPos = gl_ModelViewMatrix * gl_Vertex;
diff --git a/files/ui/advancedpage.ui b/files/ui/advancedpage.ui
index 7964399319..8e976df42d 100644
--- a/files/ui/advancedpage.ui
+++ b/files/ui/advancedpage.ui
@@ -14,7 +14,7 @@
<item>
<widget class="QTabWidget" name="AdvancedTabWidget">
<property name="currentIndex">
- <number>0</number>
+ <number>1</number>
</property>
<widget class="QWidget" name="GameMechanics">
<attribute name="title">
@@ -281,8 +281,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>645</width>
- <height>413</height>
+ <width>665</width>
+ <height>525</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@@ -444,6 +444,26 @@
</property>
</widget>
</item>
+ <item row="3" column="0">
+ <widget class="QCheckBox" name="softParticlesCheckBox">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enables soft particles for particle effects. This technique softens the intersection between individual particles and other opaque geometry by blending between them.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Soft Particles</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QCheckBox" name="antialiasAlphaTestCheckBox">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Allows MSAA to work with alpha-tested meshes, producing better-looking edges without pixelation. Can negatively impact performance.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Use anti-alias alpha testing</string>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
</item>
@@ -538,6 +558,25 @@
</widget>
</item>
<item>
+ <widget class="QGroupBox" name="miscGroup">
+ <property name="title">
+ <string>Models</string>
+ </property>
+ <layout class="QGridLayout" name="miscLayout">
+ <item row="0" column="0">
+ <widget class="QCheckBox" name="nightDaySwitchesCheckBox">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If this setting is true, supporting models will make use of day night switch nodes.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Day night switch nodes</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
diff --git a/files/ui/datafilespage.ui b/files/ui/datafilespage.ui
index ccac5050ed..ff330391d2 100644
--- a/files/ui/datafilespage.ui
+++ b/files/ui/datafilespage.ui
@@ -2,12 +2,94 @@
<ui version="4.0">
<class>DataFilesPage</class>
<widget class="QWidget" name="DataFilesPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>571</width>
+ <height>384</height>
+ </rect>
+ </property>
<property name="contextMenuPolicy">
<enum>Qt::DefaultContextMenu</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
- <widget class="QWidget" name="contentSelectorWidget" native="true"/>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string>Data Files</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QWidget" name="contentSelectorWidget" native="true"/>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tab_2">
+ <attribute name="title">
+ <string>Navigation mesh cache</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="updateNavMeshButton">
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Generate navigation mesh cache for all content. Will be used by the engine to make cell loading faster.</string>
+ </property>
+ <property name="text">
+ <string>Update</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="navMeshProgressBar">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="cancelNavMeshButton">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="toolTip">
+ <string>Cancel navigation mesh generation. Already processed data will be saved.</string>
+ </property>
+ <property name="text">
+ <string>Cancel</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QPlainTextEdit" name="navMeshLogPlainTextEdit">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="lineWrapMode">
+ <enum>QPlainTextEdit::NoWrap</enum>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
</item>
<item>
<widget class="QGroupBox" name="profileGroupBox">
diff --git a/files/vfs/CMakeLists.txt b/files/vfs/CMakeLists.txt
index a97210d1df..15dcb80ec1 100644
--- a/files/vfs/CMakeLists.txt
+++ b/files/vfs/CMakeLists.txt
@@ -1,4 +1,4 @@
-if (NOT DEFINED OPENMW_MYGUI_FILES_ROOT)
+if (NOT DEFINED OPENMW_RESOURCES_ROOT)
return()
endif()
@@ -15,4 +15,4 @@ set(TEXTURE_FILES
textures/omw_menu_scroll_center_v.dds
)
-copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_MYGUI_FILES_ROOT} ${DDIRRELATIVE} "${TEXTURE_FILES}")
+copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "${TEXTURE_FILES}")
diff --git a/scripts/find_missing_merge_requests.py b/scripts/find_missing_merge_requests.py
index 09d3e9a581..1cca15c03b 100755
--- a/scripts/find_missing_merge_requests.py
+++ b/scripts/find_missing_merge_requests.py
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
import click
+import discord_webhook
import multiprocessing
+import os
import pathlib
import requests
import urllib.parse
@@ -12,6 +14,8 @@ import urllib.parse
help='Path to text file with Gitlab token.')
@click.option('--project_id', type=int, default=7107382,
help='Gitlab project id.')
+@click.option('--job_id', type=int, default=os.getenv('CI_JOB_ID'),
+ help='Gitlab job id.')
@click.option('--host', type=str, default='gitlab.com',
help='Gitlab host.')
@click.option('--workers', type=int, default=10,
@@ -24,18 +28,23 @@ import urllib.parse
help='End before given /merge_requests page.')
@click.option('--per_page', type=int, default=100,
help='Number of merge requests per page.')
-def main(token_path, project_id, host, workers, target_branch, begin_page, end_page, per_page):
- token = read_token(token_path)
+@click.option('--ignored_mrs_path', type=str,
+ help='Path to a list of ignored MRs.')
+def main(token_path, project_id, job_id, host, workers, target_branch, begin_page, end_page, per_page, ignored_mrs_path):
+ headers = make_headers(token_path)
base_url = f'https://{host}/api/v4/projects/{project_id}/'
+ discord_webhook_url = os.getenv('DISCORD_WEBHOOK_URL')
+ ignored_mrs = frozenset(read_ignored_mrs(ignored_mrs_path))
checked = 0
filtered = 0
missing = 0
+ missing_mrs = list()
for page in range(begin_page, end_page):
- merge_requests = requests.get(
+ merge_requests = parse_gitlab_response(requests.get(
url=urllib.parse.urljoin(base_url, 'merge_requests'),
- headers={'PRIVATE-TOKEN': token},
+ headers=headers,
params=dict(state='merged', per_page=per_page, page=page),
- ).json()
+ ))
if not merge_requests:
break
checked += len(merge_requests)
@@ -44,24 +53,65 @@ def main(token_path, project_id, host, workers, target_branch, begin_page, end_p
continue
filtered += len(merge_requests)
with multiprocessing.Pool(workers) as pool:
- missing_merge_requests = pool.map(FilterMissingMergeRequest(token, base_url), merge_requests)
+ missing_merge_requests = pool.map(FilterMissingMergeRequest(headers, base_url), merge_requests)
for mr in missing_merge_requests:
- if mr is not None:
- missing += 1
- print(f"MR {mr['reference']} ({mr['id']}) is missing from branch {mr['target_branch']},"
+ if mr is None:
+ continue
+ if mr['reference'] in ignored_mrs or mr['reference'].strip('!') in ignored_mrs:
+ print(f"Ignored MR {mr['reference']} ({mr['id']}) is missing from branch {mr['target_branch']},"
f" previously was merged as {mr['merge_commit_sha']}")
+ continue
+ missing += 1
+ missing_mrs.append(mr)
+ print(f"MR {mr['reference']} ({mr['id']}) is missing from branch {mr['target_branch']},"
+ f" previously was merged as {mr['merge_commit_sha']}")
print(f'Checked {checked} MRs ({filtered} with {target_branch} target branch), {missing} are missing')
+ if discord_webhook_url is not None and missing_mrs:
+ project_web_url = parse_gitlab_response(requests.get(url=base_url, headers=headers))['web_url'] + '/'
+ discord_message = format_discord_message(missing=missing, filtered=filtered, target_branch=target_branch,
+ project_web_url=project_web_url, missing_mrs=missing_mrs,
+ job_id=job_id)
+ print('Sending Discord notification...')
+ print(discord_message)
+ discord_webhook.DiscordWebhook(url=discord_webhook_url, content=discord_message, rate_limit_retry=True).execute()
+ if missing_mrs:
+ exit(-1)
+
+
+def format_discord_message(missing, filtered, target_branch, project_web_url, missing_mrs, job_id):
+ target_branch = format_link(target_branch, urllib.parse.urljoin(project_web_url, f'-/tree/{target_branch}'))
+ job = f' by job ' + format_link(job_id, urllib.parse.urljoin(project_web_url, f'-/jobs/{job_id}')) if job_id else ''
+ return (
+ f'Found {missing} missing MRs out of {filtered} from {target_branch} target branch{job}:\n'
+ + '\n'.join(format_missing_mr_message(v, project_web_url) for v in missing_mrs)
+ )
+
+
+def format_missing_mr_message(mr, project_web_url):
+ web_url = mr.get('web_url')
+ reference = mr['reference']
+ target_branch = mr['target_branch']
+ commit = mr['merge_commit_sha']
+ if web_url is not None:
+ reference = format_link(reference, web_url)
+ target_branch = format_link(target_branch, urllib.parse.urljoin(project_web_url, f'-/tree/{target_branch}'))
+ commit = format_link(commit, urllib.parse.urljoin(project_web_url, f'-/commit/{commit}'))
+ return f"MR {reference} is missing from branch {target_branch}, previously was merged as {commit}"
+
+
+def format_link(name, url):
+ return f'[{name}]({url})'
class FilterMissingMergeRequest:
- def __init__(self, token, base_url):
- self.token = token
+ def __init__(self, headers, base_url):
+ self.headers = headers
self.base_url = base_url
def __call__(self, merge_request):
commit_refs = requests.get(
url=urllib.parse.urljoin(self.base_url, f"repository/commits/{merge_request['merge_commit_sha']}/refs"),
- headers={'PRIVATE-TOKEN': self.token},
+ headers=self.headers,
).json()
if 'message' in commit_refs and commit_refs['message'] == '404 Commit Not Found':
return merge_request
@@ -73,10 +123,39 @@ def present_in_branch(commit_refs, branch):
return bool(next((v for v in commit_refs if v['type'] == 'branch' and v['name'] == branch), None))
+def make_headers(token_path):
+ job_token = os.environ.get('CI_JOB_TOKEN')
+ if job_token is not None:
+ print('Using auth token from CI_JOB_TOKEN env')
+ return {'JOB_TOKEN': job_token}
+ if not os.path.exists(token_path):
+ print(f'Ignore absent token path: {token_path}')
+ return dict()
+ print(f'Using auth token from: {token_path}')
+ return {'PRIVATE-TOKEN': read_token(token_path)}
+
+
+def read_ignored_mrs(path):
+ if path is None:
+ return
+ with open(path) as stream:
+ for line in stream:
+ yield line.strip()
+
+
def read_token(path):
with open(path) as stream:
return stream.readline().strip()
+def parse_gitlab_response(response):
+ response = response.json()
+ if isinstance(response, dict):
+ message = response.get('message')
+ if message is not None:
+ raise RuntimeError(f'Gitlab request has failed: {message}')
+ return response
+
+
if __name__ == '__main__':
main()
diff --git a/scripts/osg_stats.py b/scripts/osg_stats.py
index 037aafea00..275e70dcff 100755
--- a/scripts/osg_stats.py
+++ b/scripts/osg_stats.py
@@ -12,11 +12,14 @@ import numpy
import statistics
import sys
import termtables
+import re
@click.command()
@click.option('--print_keys', is_flag=True,
help='Print a list of all present keys in the input file.')
+@click.option('--regexp_match', is_flag=True,
+ help='Use all metric that match given key. Can be used with stats and timeseries.')
@click.option('--timeseries', type=str, multiple=True,
help='Show a graph for given metric over time.')
@click.option('--commulative_timeseries', type=str, multiple=True,
@@ -35,6 +38,8 @@ import termtables
'between Physics Actors and physics_time_taken. Format: --plot <x> <y> <function>.')
@click.option('--stats', type=str, multiple=True,
help='Print table with stats for a given metric containing min, max, mean, median etc.')
+@click.option('--precision', type=int,
+ help='Format floating point numbers with given precision')
@click.option('--timeseries_sum', is_flag=True,
help='Add a graph to timeseries for a sum per frame of all given timeseries metrics.')
@click.option('--commulative_timeseries_sum', is_flag=True,
@@ -54,7 +59,7 @@ import termtables
@click.option('--threshold_value', type=float, default=1.05/60,
help='Threshold for hist_over.')
@click.argument('path', type=click.Path(), nargs=-1)
-def main(print_keys, timeseries, hist, hist_ratio, stdev_hist, plot, stats,
+def main(print_keys, regexp_match, timeseries, hist, hist_ratio, stdev_hist, plot, stats, precision,
timeseries_sum, stats_sum, begin_frame, end_frame, path,
commulative_timeseries, commulative_timeseries_sum, frame_number_name,
hist_threshold, threshold_name, threshold_value):
@@ -68,10 +73,10 @@ def main(print_keys, timeseries, hist, hist_ratio, stdev_hist, plot, stats,
for v in keys:
print(v)
if timeseries:
- draw_timeseries(sources=frames, keys=timeseries, add_sum=timeseries_sum,
+ draw_timeseries(sources=frames, keys=matching_keys(keys, timeseries, regexp_match), add_sum=timeseries_sum,
begin_frame=begin_frame, end_frame=end_frame)
if commulative_timeseries:
- draw_commulative_timeseries(sources=frames, keys=commulative_timeseries, add_sum=commulative_timeseries_sum,
+ draw_commulative_timeseries(sources=frames, keys=matching_keys(keys, commulative_timeseries, regexp_match), add_sum=commulative_timeseries_sum,
begin_frame=begin_frame, end_frame=end_frame)
if hist:
draw_hists(sources=frames, keys=hist)
@@ -82,7 +87,7 @@ def main(print_keys, timeseries, hist, hist_ratio, stdev_hist, plot, stats,
if plot:
draw_plots(sources=frames, plots=plot)
if stats:
- print_stats(sources=frames, keys=stats, stats_sum=stats_sum)
+ print_stats(sources=frames, keys=matching_keys(keys, stats, regexp_match), stats_sum=stats_sum, precision=precision)
if hist_threshold:
draw_hist_threshold(sources=frames, keys=hist_threshold, begin_frame=begin_frame,
threshold_name=threshold_name, threshold_value=threshold_value)
@@ -140,6 +145,12 @@ def collect_unique_keys(sources):
return sorted(result)
+def matching_keys(keys, patterns, regexp_match):
+ if regexp_match:
+ return { key for pattern in patterns for key in keys if re.search(pattern, key) }
+ return keys
+
+
def draw_timeseries(sources, keys, add_sum, begin_frame, end_frame):
fig, ax = matplotlib.pyplot.subplots()
x = numpy.array(range(begin_frame, end_frame))
@@ -242,13 +253,13 @@ def draw_plots(sources, plots):
fig.canvas.set_window_title('plots')
-def print_stats(sources, keys, stats_sum):
+def print_stats(sources, keys, stats_sum, precision):
stats = list()
for name, frames in sources.items():
for key in keys:
- stats.append(make_stats(source=name, key=key, values=filter_not_none(frames[key])))
+ stats.append(make_stats(source=name, key=key, values=filter_not_none(frames[key]), precision=precision))
if stats_sum:
- stats.append(make_stats(source=name, key='sum', values=sum_multiple(frames, keys)))
+ stats.append(make_stats(source=name, key='sum', values=sum_multiple(frames, keys), precision=precision))
metrics = list(stats[0].keys())
termtables.print(
[list(v.values()) for v in stats],
@@ -282,6 +293,10 @@ def filter_not_none(values):
return [v for v in values if v is not None]
+def fixed_float(value, precision):
+ return '{v:.{p}f}'.format(v=value, p=precision) if precision else value
+
+
def sum_multiple(frames, keys):
result = collections.Counter()
for key in keys:
@@ -292,17 +307,17 @@ def sum_multiple(frames, keys):
return numpy.array([result[k] for k in sorted(result.keys())])
-def make_stats(source, key, values):
+def make_stats(source, key, values, precision):
return collections.OrderedDict(
source=source,
key=key,
number=len(values),
- min=min(values),
- max=max(values),
- mean=statistics.mean(values),
- median=statistics.median(values),
- stdev=statistics.stdev(values),
- q95=numpy.quantile(values, 0.95),
+ min=fixed_float(min(values), precision),
+ max=fixed_float(max(values), precision),
+ mean=fixed_float(statistics.mean(values), precision),
+ median=fixed_float(statistics.median(values), precision),
+ stdev=fixed_float(statistics.stdev(values), precision),
+ q95=fixed_float(numpy.quantile(values, 0.95), precision),
)